]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Merge pull request #2325 in SNORT/snort3 from ~MIALTIZE/snort3:wizardry2 to master
authorMichael Altizer (mialtize) <mialtize@cisco.com>
Tue, 21 Jul 2020 17:58:16 +0000 (17:58 +0000)
committerMichael Altizer (mialtize) <mialtize@cisco.com>
Tue, 21 Jul 2020 17:58:16 +0000 (17:58 +0000)
Squashed commit of the following:

commit 5b1527473e3a55457a3a091e1a5e718abd9a584b
Author: Michael Altizer <mialtize@cisco.com>
Date:   Thu Jul 16 17:07:22 2020 -0400

    wizard: Improve wizard tracing to indicate direction and abandonment

commit c2cba2ec1205251803b3e501e59113e6a92737eb
Author: Michael Altizer <mialtize@cisco.com>
Date:   Thu Jul 9 18:12:48 2020 -0400

    wizard: Add peg counts for abandoned searches per protocol

commit 558df5a45cfbfee4b783d84973f77a9d95dfb710
Author: Michael Altizer <mialtize@cisco.com>
Date:   Thu Jul 9 18:05:20 2020 -0400

    wizard: Abort the splitter once we've hit the max PDU size

commit 04dbc4e5c9949316c70f4faf26b1c37e10da312b
Author: Michael Altizer <mialtize@cisco.com>
Date:   Tue Jul 7 18:19:18 2020 -0400

    dce_rpc: Improve PAF autodetection for heavily segmented TCP traffic

commit 76b0e4f6c5faf77fa28ed45472d1ca9476e37a99
Author: Michael Altizer <mialtize@cisco.com>
Date:   Tue Jul 7 12:25:51 2020 -0400

    snort_defaults: Remove the NOTIFY, SUBSCRIBE, and UPDATE HTTP methods

    These methods overlap with SIP methods, where they are much more
    commonly found.  Until there is a priority/fallback mechanism for the
    Wizard, these patterns will be retired from the HTTP spell.

commit f5561a1697ec6ac38981e0af094bb225b70910ca
Author: Michael Altizer <mialtize@cisco.com>
Date:   Mon Jul 6 18:33:27 2020 -0400

    wizard: Abandon the wizard on UDP flows after the first packet

commit 7f65256f9b6a7470ebf5737273e360fe6a1491c6
Author: Michael Altizer <mialtize@cisco.com>
Date:   Tue Nov 5 17:27:10 2019 -0500

    wizard: Report spell and hex configuration errors and warnings

commit 1b08923942d23744a6291cce0d39b4f24c12edbb
Author: Michael Altizer <mialtize@cisco.com>
Date:   Tue Nov 5 12:58:07 2019 -0500

    wizard: Properly terminate hex matching

lua/snort_defaults.lua
src/service_inspectors/dce_rpc/dce_tcp_paf.cc
src/service_inspectors/dce_rpc/dce_tcp_paf.h
src/service_inspectors/wizard/hexes.cc
src/service_inspectors/wizard/magic.h
src/service_inspectors/wizard/spells.cc
src/service_inspectors/wizard/wiz_module.cc
src/service_inspectors/wizard/wiz_module.h
src/service_inspectors/wizard/wizard.cc

index c05a47e833d3280bc7b93e5b78656563e2a72f98..5a41e12ac4dbf872466c1eba0dff743f939185c1 100644 (file)
@@ -299,14 +299,13 @@ http_methods =  -- build from default_http_methods
 {
     'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT',
     'VERSION_CONTROL', 'REPORT', 'CHECKOUT', 'CHECKIN', 'UNCHECKOUT',
-    'MKWORKSPACE', 'UPDATE', 'LABEL', 'MERGE', 'BASELINE_CONTROL',
+    'MKWORKSPACE', 'LABEL', 'MERGE', 'BASELINE_CONTROL',
     'MKACTIVITY', 'ORDERPATCH', 'ACL', 'PATCH', 'BIND', 'LINK',
     'MKCALENDAR', 'MKREDIRECTREF', 'REBIND', 'UNBIND', 'UNLINK',
     'UPDATEREDIRECTREF', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY',
     'MOVE', 'LOCK', 'UNLOCK', 'SEARCH', 'BCOPY', 'BDELETE', 'BMOVE',
-    'BPROPFIND', 'BPROPPATCH', 'NOTIFY', 'POLL', 'SUBSCRIBE',
-    'UNSUBSCRIBE', 'X_MS_ENUMATTS',
-    --'OPTIONS',
+    'BPROPFIND', 'BPROPPATCH', 'POLL', 'UNSUBSCRIBE', 'X_MS_ENUMATTS',
+    --'NOTIFY', 'OPTIONS', 'SUBSCRIBE', 'UPDATE'
 }
 
 sip_methods =
index fea8cb9a3887993aea7c0e217764ea3e4f3cfb6c..f27d3bf3988c7f5a03b2b4a66f9b84fa20f35e03 100644 (file)
 
 using namespace snort;
 
+Dce2TcpSplitter::Dce2TcpSplitter(bool c2s) : StreamSplitter(c2s)
+{
+    state.paf_state = DCE2_PAF_TCP_STATES__0;
+    state.byte_order = DCERPC_BO_FLAG__NONE;
+    state.frag_len = 0;
+    state.autodetected = false;
+}
+
 /*********************************************************************
- * Function: dce2_tcp_paf()
- *
  * Purpose: The DCE/RPC over TCP PAF callback.
  *          Inspects a byte at a time changing state.  At state 4
  *          gets byte order of PDU.  At states 8 and 9 gets
@@ -41,115 +47,98 @@ using namespace snort;
  *          be multiple PDUs in a single TCP segment (evasion case).
  *
  *********************************************************************/
-static StreamSplitter::Status dce2_tcp_paf(DCE2_PafTcpData* ds, Flow* flow, const uint8_t* data,
-    uint32_t len, uint32_t flags, uint32_t* fp)
+StreamSplitter::Status Dce2TcpSplitter::scan(
+    Packet* pkt, const uint8_t* data, uint32_t len,
+    uint32_t flags, uint32_t* fp)
 {
-    DCE2_TcpSsnData* sd = get_dce2_tcp_session_data(flow);
-    if ( dce2_paf_abort((DCE2_SsnData*)sd) )
+    DCE2_TcpSsnData* sd = get_dce2_tcp_session_data(pkt->flow);
+
+    if (dce2_paf_abort((DCE2_SsnData*) sd))
         return StreamSplitter::ABORT;
 
-    if ( !sd )
+    uint32_t n = 0;
+    uint32_t new_fp = 0;
+    int start_state = (uint8_t) state.paf_state;
+    int num_requests = 0;
+
+    while (n < len)
     {
-        bool autodetected = false;
-        if ( len >= sizeof(DceRpcCoHdr) )
+        switch (state.paf_state)
         {
-            const DceRpcCoHdr* co_hdr = (const DceRpcCoHdr*)data;
-
-            if ( (DceRpcCoVersMaj(co_hdr) == DCERPC_PROTO_MAJOR_VERS__5)
-                && (DceRpcCoVersMin(co_hdr) == DCERPC_PROTO_MINOR_VERS__0)
-                && (((flags & PKT_FROM_CLIENT)
-                        && DceRpcCoPduType(co_hdr) == DCERPC_PDU_TYPE__BIND)
-                    || ((flags & PKT_FROM_SERVER)
-                        && DceRpcCoPduType(co_hdr) == DCERPC_PDU_TYPE__BIND_ACK))
-                && (DceRpcCoFragLen(co_hdr) >= sizeof(DceRpcCoHdr)) )
+        case DCE2_PAF_TCP_STATES__0:    // Major version
+            if (!sd && !state.autodetected) // Autodetection validation
             {
-                autodetected = true;
+                if (data[n] != DCERPC_PROTO_MAJOR_VERS__5)
+                    return StreamSplitter::ABORT;
+                if (len < sizeof(DceRpcCoHdr) && (flags & PKT_FROM_CLIENT))
+                    state.autodetected = true;
             }
-        }
-        else if ( (*data == DCERPC_PROTO_MAJOR_VERS__5) && (flags & PKT_FROM_CLIENT) )
-        {
-            autodetected = true;
-        }
-
-        if ( !autodetected )
-            return StreamSplitter::ABORT;
-    }
-
-    int start_state = (uint8_t)ds->paf_state;
-    int num_requests = 0;
-    uint32_t tmp_fp = 0;
-    uint32_t n = 0;
-    while ( n < len )
-    {
-        switch ( ds->paf_state )
-        {
-        case DCE2_PAF_TCP_STATES__4:      // Get byte order
-            ds->byte_order = DceRpcByteOrder(data[n]);
-            ds->paf_state = (DCE2_PafTcpStates)(((int)ds->paf_state) + 1);
             break;
-
-        case DCE2_PAF_TCP_STATES__8:
-            if ( ds->byte_order == DCERPC_BO_FLAG__LITTLE_ENDIAN )
-                ds->frag_len = data[n];
+        case DCE2_PAF_TCP_STATES__1:    // Minor version
+            if (!sd && !state.autodetected) // Autodetection validation
+            {
+                if (data[n] != DCERPC_PROTO_MINOR_VERS__0)
+                    return StreamSplitter::ABORT;
+            }
+            break;
+        case DCE2_PAF_TCP_STATES__2:    // PDU type
+            if (!sd && !state.autodetected) // Autodetection validation
+            {
+                if (((flags & PKT_FROM_CLIENT) && data[n] != DCERPC_PDU_TYPE__BIND) ||
+                    ((flags & PKT_FROM_SERVER) && data[n] != DCERPC_PDU_TYPE__BIND_ACK))
+                    return StreamSplitter::ABORT;
+            }
+            break;
+        case DCE2_PAF_TCP_STATES__4:    // Byte order
+            state.byte_order = DceRpcByteOrder(data[n]);
+            break;
+        case DCE2_PAF_TCP_STATES__8:    // First byte of fragment length
+            if (state.byte_order == DCERPC_BO_FLAG__LITTLE_ENDIAN)
+                state.frag_len = data[n];
             else
-                ds->frag_len = data[n] << 8;
-            ds->paf_state = (DCE2_PafTcpStates)(((int)ds->paf_state) + 1);
+                state.frag_len = data[n] << 8;
             break;
-
-        case DCE2_PAF_TCP_STATES__9:
-            if ( ds->byte_order == DCERPC_BO_FLAG__LITTLE_ENDIAN )
-                ds->frag_len |= data[n] << 8;
+        case DCE2_PAF_TCP_STATES__9:    // Second byte of fragment length
+            if (state.byte_order == DCERPC_BO_FLAG__LITTLE_ENDIAN)
+                state.frag_len |= data[n] << 8;
             else
-                ds->frag_len |= data[n];
+                state.frag_len |= data[n];
 
-            /* If we get a bad frag length abort */
-            if ( ds->frag_len < sizeof(DceRpcCoHdr) )
+            /* Abort if we get a bad frag length */
+            if (state.frag_len < sizeof(DceRpcCoHdr))
             {
-               if ( sd )
+                if (sd)
                     dce_alert(GID_DCE2, DCE2_CO_FRAG_LEN_LT_HDR, (dce2CommonStats*)&dce2_tcp_stats, *(DCE2_SsnData*)sd);
-               return StreamSplitter::ABORT;
+                return StreamSplitter::ABORT;
             }
 
+            /* In the non-degenerate case, we can now declare that we think this looks like DCE */
+            if (!state.autodetected)
+                state.autodetected = true;
+
             /* Increment n here so we can continue */
-            n += ds->frag_len - (uint8_t)ds->paf_state;
+            n += state.frag_len - (uint8_t) state.paf_state;
             num_requests++;
             /* Might have multiple PDUs in one segment.  If the last PDU is partial,
              * flush just before it */
-            if ( (num_requests == 1) || (n <= len) )
-                tmp_fp += ds->frag_len;
-
-            ds->paf_state = DCE2_PAF_TCP_STATES__0;
-            continue;      // we incremented n already
+            if ((num_requests == 1) || (n <= len))
+                new_fp += state.frag_len;
 
+            state.paf_state = DCE2_PAF_TCP_STATES__0;
+            continue;      // we incremented n and set the state already
         default:
-            ds->paf_state = (DCE2_PafTcpStates)(((int)ds->paf_state) + 1);
             break;
         }
 
+        state.paf_state = (DCE2_PafTcpStates)(((int)state.paf_state) + 1);
         n++;
     }
 
-    if ( tmp_fp != 0 )
+    if (new_fp != 0)
     {
-        *fp = tmp_fp - start_state;
+        *fp = new_fp - start_state;
         return StreamSplitter::FLUSH;
     }
 
     return StreamSplitter::SEARCH;
 }
-
-Dce2TcpSplitter::Dce2TcpSplitter(bool c2s) : StreamSplitter(c2s)
-{
-    state.paf_state = DCE2_PAF_TCP_STATES__0;
-    state.byte_order = DCERPC_BO_FLAG__NONE;
-    state.frag_len = 0;
-}
-
-StreamSplitter::Status Dce2TcpSplitter::scan(
-    Packet* pkt, const uint8_t* data, uint32_t len,
-    uint32_t flags, uint32_t* fp)
-{
-    DCE2_PafTcpData* pfdata = &state;
-    return dce2_tcp_paf(pfdata, pkt->flow, data, len, flags, fp);
-}
-
index 667f23c57dac9c8a8a5aecbdd2e3b027664b9a94..da3cf5986d3cb95c5653c18fc2b0eb442cc69f37 100644 (file)
 #include "dce_common.h"
 #include "stream/stream_splitter.h"
 
-#define DCE2_DEBUG__PAF_START_MSG_TCP  "DCE/RPC over TCP PAF ====================================="
-
 enum DCE2_PafTcpStates
 {
     DCE2_PAF_TCP_STATES__0 = 0,
-    DCE2_PAF_TCP_STATES__1,
-    DCE2_PAF_TCP_STATES__2,
-    DCE2_PAF_TCP_STATES__3,
-    DCE2_PAF_TCP_STATES__4,   // Byte order
+    DCE2_PAF_TCP_STATES__1,     // Major version
+    DCE2_PAF_TCP_STATES__2,     // Minor version
+    DCE2_PAF_TCP_STATES__3,     // PDU type
+    DCE2_PAF_TCP_STATES__4,     // Byte order
     DCE2_PAF_TCP_STATES__5,
     DCE2_PAF_TCP_STATES__6,
     DCE2_PAF_TCP_STATES__7,
-    DCE2_PAF_TCP_STATES__8,   // First byte of fragment length
-    DCE2_PAF_TCP_STATES__9    // Second byte of fragment length
+    DCE2_PAF_TCP_STATES__8,     // First byte of fragment length
+    DCE2_PAF_TCP_STATES__9      // Second byte of fragment length
 };
 
 // State tracker for DCE/RPC over TCP PAF
@@ -47,6 +45,7 @@ struct DCE2_PafTcpData
     DCE2_PafTcpStates paf_state;
     DceRpcBoFlag byte_order;
     uint16_t frag_len;
+    bool autodetected;
 };
 
 class Dce2TcpSplitter : public snort::StreamSplitter
@@ -57,12 +56,9 @@ public:
     Status scan(snort::Packet*, const uint8_t* data, uint32_t len,
         uint32_t flags, uint32_t* fp) override;
 
-    bool is_paf() override
-    {
-        return true;
-    }
+    bool is_paf() override { return true; }
 
-public:
+private:
     DCE2_PafTcpData state;
 };
 
index 3880025d3f2d7c45a34f478dafdbd9e815f8de6a..ad581c4dfe439a81a50cc6f9c386e629509ccd06 100644 (file)
@@ -94,12 +94,15 @@ void HexBook::add_spell(
     p->value = val;
 }
 
-bool HexBook::add_spell(const char* key, const char* val)
+bool HexBook::add_spell(const char* key, const char*& val)
 {
     HexVector hv;
 
     if ( !translate(key, hv) )
+    {
+        val = nullptr;
         return false;
+    }
 
     unsigned i = 0;
     MagicPage* p = root;
@@ -120,7 +123,10 @@ bool HexBook::add_spell(const char* key, const char* val)
         ++i;
     }
     if ( p->key == key )
+    {
+        val = p->value.c_str();
         return false;
+    }
 
     add_spell(key, val, hv, i, p);
     return true;
@@ -152,7 +158,7 @@ const MagicPage* HexBook::find_spell(
             if ( const MagicPage* q = find_spell(s, n, p->any, i+1) )
                 return q;
         }
-        break;
+        return p->value.empty() ? nullptr : p;
     }
     return p;
 }
@@ -162,7 +168,7 @@ const char* HexBook::find_spell(
 {
     p = find_spell(data, len, p, 0);
 
-    if ( !p->value.empty() )
+    if ( p and !p->value.empty() )
         return p->value.c_str();
 
     return nullptr;
index 3c548623a7de97a937c70e3349bd5204a1181ff1..d81d27aa44d1ac00e72b143d60ae90324eff6b20 100644 (file)
@@ -51,7 +51,7 @@ public:
     MagicBook(const MagicBook&) = delete;
     MagicBook& operator=(const MagicBook&) = delete;
 
-    virtual bool add_spell(const char* key, const char* val) = 0;
+    virtual bool add_spell(const char* key, const char*& val) = 0;
     virtual const char* find_spell(const uint8_t*, unsigned len, const MagicPage*&) const = 0;
 
     const MagicPage* page1()
@@ -72,7 +72,7 @@ class SpellBook : public MagicBook
 public:
     SpellBook();
 
-    bool add_spell(const char*, const char*) override;
+    bool add_spell(const char*, const char*&) override;
     const char* find_spell(const uint8_t*, unsigned len, const MagicPage*&) const override;
 
 private:
@@ -91,7 +91,7 @@ class HexBook : public MagicBook
 public:
     HexBook() = default;
 
-    bool add_spell(const char*, const char*) override;
+    bool add_spell(const char*, const char*&) override;
     const char* find_spell(const uint8_t*, unsigned len, const MagicPage*&) const override;
 
 private:
index 7634a36e79ed9f1ac0ffb05acf25db1903731a89..dd990ad387bccc7bd29602aad98365b13d84f2f2 100644 (file)
@@ -45,6 +45,9 @@ bool SpellBook::translate(const char* in, HexVector& out)
 
     while ( in[i] )
     {
+        if ( !isprint(in[i]) )
+            return false;
+
         if ( wild )
         {
             if ( in[i] != '*' )
@@ -84,12 +87,15 @@ void SpellBook::add_spell(
     p->value = val;
 }
 
-bool SpellBook::add_spell(const char* key, const char* val)
+bool SpellBook::add_spell(const char* key, const char*& val)
 {
     HexVector hv;
 
     if ( !translate(key, hv) )
+    {
+        val = nullptr;
         return false;
+    }
 
     unsigned i = 0;
     MagicPage* p = root;
@@ -111,7 +117,10 @@ bool SpellBook::add_spell(const char* key, const char* val)
         ++i;
     }
     if ( p->key == key )
+    {
+        val = p->value.c_str();
         return false;
+    }
 
     add_spell(key, val, hv, i, p);
     return true;
index da4828a1bac92678996a22b74b3d5b99ec984eb3..c2acf44db435141164e02295211bfd3cd063e017 100644 (file)
@@ -24,6 +24,7 @@
 
 #include "wiz_module.h"
 
+#include "log/messages.h"
 #include "trace/trace.h"
 
 #include "curses.h"
@@ -176,60 +177,75 @@ bool WizardModule::begin(const char* fqn, int, SnortConfig*)
 
         curses = new CurseBook;
     }
-    else if ( !strcmp(fqn, "wizard.hexes") )
-        hex = true;
-
-    else if ( !strcmp(fqn, "wizard.spells") )
-        hex = false;
-
-    else if ( !strcmp(fqn, "wizard.hexes.to_client") )
-        c2s = false;
-
-    else if ( !strcmp(fqn, "wizard.spells.to_client") )
-        c2s = false;
-
-    else if ( !strcmp(fqn, "wizard.hexes.to_server") )
-        c2s = true;
-
-    else if ( !strcmp(fqn, "wizard.spells.to_server") )
-        c2s = true;
+    else if ( !strcmp(fqn, "wizard.hexes") || !strcmp(fqn, "wizard.spells") )
+    {
+        service.clear();
+    }
+    else if ( !strcmp(fqn, "wizard.hexes.to_client") || !strcmp(fqn, "wizard.hexes.to_server") ||
+        !strcmp(fqn, "wizard.spells.to_client") || !strcmp(fqn, "wizard.spells.to_server") )
+    {
+        spells.clear();
+    }
 
     return true;
 }
 
-void WizardModule::add_spells(MagicBook* b, string& service)
+bool WizardModule::add_spells(MagicBook* b, string& service, bool hex)
 {
     for ( const auto& p : spells )
-        b->add_spell(p.c_str(), service.c_str());
+    {
+        const char* val = service.c_str();
+        if ( !b->add_spell(p.c_str(), val) )
+        {
+            if ( !val )
+            {
+                ParseError("Invalid %s '%s' for service '%s'",
+                    hex ? "hex" : "spell", service.c_str(), p.c_str());
+                return false;
+            }
+            else if ( service != val )
+            {
+                ParseWarning(WARN_CONF, "%s '%s' for service '%s' already exists for service '%s'",
+                    hex ? "Hex" : "Spell", p.c_str(), service.c_str(), val);
+            }
+            else
+            {
+                ParseWarning(WARN_CONF, "Duplicate %s '%s' for service '%s'",
+                    hex ? "hex" : "spell", p.c_str(), val);
+            }
+        }
+    }
+    return true;
 }
 
-bool WizardModule::end(const char* fqn, int idx, SnortConfig*)
+bool WizardModule::end(const char* fqn, int, SnortConfig*)
 {
-    if ( idx )
+    if ( !strcmp(fqn, "wizard") )
     {
         service.clear();
-        return true;
+        spells.clear();
     }
-    if ( !strstr(fqn, "to_client") and !strstr(fqn, "to_server") )
+    else if ( !strcmp(fqn, "wizard.hexes.to_client") )
     {
-        return true;
+        if ( !add_spells(s2c_hexes, service, true) )
+            return false;
+    }
+    else if ( !strcmp(fqn, "wizard.spells.to_client") )
+    {
+        if ( !add_spells(s2c_spells, service, false) )
+            return false;
     }
-    if ( hex )
+    else if ( !strcmp(fqn, "wizard.hexes.to_server") )
     {
-        if ( c2s )
-            add_spells(c2s_hexes, service);
-        else
-            add_spells(s2c_hexes, service);
+        if ( !add_spells(c2s_hexes, service, true) )
+            return false;
     }
-    else
+    else if ( !strcmp(fqn, "wizard.spells.to_server") )
     {
-        if ( c2s )
-            add_spells(c2s_spells, service);
-        else
-            add_spells(s2c_spells, service);
+        if ( !add_spells(c2s_spells, service, false) )
+            return false;
     }
 
-    spells.clear();
     return true;
 }
 
index 26ca6ba6f244766ea248853f434a555b1e041a6d..62db7629d6a64a865f93f4694e5179265fa29b23 100644 (file)
@@ -65,11 +65,9 @@ public:
     const snort::TraceOption* get_trace_options() const override;
 
 private:
-    void add_spells(MagicBook*, std::string&);
+    bool add_spells(MagicBook*, std::string&, bool hex);
 
 private:
-    bool hex;
-    bool c2s;
     std::string service;
     std::vector<std::string> spells;
 
index 3585b1c21865202651a0efd93a386e3f8e029028..c6fad20e128aa967feb2b48216cda124c65b8bb1 100644 (file)
@@ -41,20 +41,26 @@ struct WizStats
 {
     PegCount tcp_scans;
     PegCount tcp_hits;
+    PegCount tcp_misses;
     PegCount udp_scans;
     PegCount udp_hits;
+    PegCount udp_misses;
     PegCount user_scans;
     PegCount user_hits;
+    PegCount user_misses;
 };
 
 const PegInfo wiz_pegs[] =
 {
     { CountType::SUM, "tcp_scans", "tcp payload scans" },
     { CountType::SUM, "tcp_hits", "tcp identifications" },
+    { CountType::SUM, "tcp_misses", "tcp searches abandoned" },
     { CountType::SUM, "udp_scans", "udp payload scans" },
     { CountType::SUM, "udp_hits", "udp identifications" },
+    { CountType::SUM, "udp_misses", "udp searches abandoned" },
     { CountType::SUM, "user_scans", "user payload scans" },
     { CountType::SUM, "user_hits", "user identifications" },
+    { CountType::SUM, "user_misses", "user searches abandoned" },
     { CountType::END, nullptr, nullptr }
 };
 
@@ -107,9 +113,18 @@ private:
             ++tstats.user_hits;
     }
 
+    void count_miss(const Flow* f)
+    {
+        if ( f->pkt_type == PktType::TCP )
+            ++tstats.tcp_misses;
+        else
+            ++tstats.user_misses;
+    }
+
 private:
     Wizard* wizard;
     Wand wand;
+    unsigned bytes_scanned = 0;
 };
 
 class Wizard : public Inspector
@@ -169,14 +184,20 @@ StreamSplitter::Status MagicSplitter::scan(
     Profile profile(wizPerfStats);
     count_scan(pkt->flow);
 
+    bytes_scanned += len;
     if ( wizard->cast_spell(wand, pkt->flow, data, len) )
     {
-        trace_logf(wizard_trace, pkt, "service set to %s\n", pkt->flow->service);
+        trace_logf(wizard_trace, pkt, "%s streaming search found service %s\n",
+            to_server() ? "c2s" : "s2c", pkt->flow->service);
         count_hit(pkt->flow);
     }
 
-    else if ( wizard->finished(wand) )
+    else if ( wizard->finished(wand) || bytes_scanned >= max(pkt->flow) )
+    {
+        count_miss(pkt->flow);
+        trace_logf(wizard_trace, pkt, "%s streaming search abandoned\n", to_server() ? "c2s" : "s2c");
         return ABORT;
+    }
 
     // ostensibly continue but splitter will be swapped out upon hit
     return SEARCH;
@@ -244,16 +265,23 @@ void Wizard::eval(Packet* p)
     if ( !p->data || !p->dsize )
         return;
 
+    bool c2s = p->is_from_client();
     Wand wand;
-    reset(wand, false, p->is_from_client());
+    reset(wand, false, c2s);
 
+    ++tstats.udp_scans;
     if ( cast_spell(wand, p->flow, p->data, p->dsize) )
     {
-        trace_logf(wizard_trace, p, "service set to %s\n", p->flow->service);
+        trace_logf(wizard_trace, p, "%s datagram search found service %s\n",
+            c2s ? "c2s" : "s2c", p->flow->service);
         ++tstats.udp_hits;
     }
-
-    ++tstats.udp_scans;
+    else
+    {
+        p->flow->clear_clouseau();
+        trace_logf(wizard_trace, p, "%s datagram search abandoned\n", c2s ? "c2s" : "s2c");
+        ++tstats.udp_misses;
+    }
 }
 
 StreamSplitter* Wizard::get_splitter(bool c2s)