]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Merge pull request #1789 in SNORT/snort3 from ~JIAWU2/snort3:service_inspector_cip_po...
authorShravan Rangarajuvenkata (shrarang) <shrarang@cisco.com>
Fri, 8 Nov 2019 14:11:53 +0000 (09:11 -0500)
committerShravan Rangarajuvenkata (shrarang) <shrarang@cisco.com>
Fri, 8 Nov 2019 14:11:53 +0000 (09:11 -0500)
Squashed commit of the following:

commit 4777c5b25a30d46c1f79488488c9a4c731f48971
Author: Jian Wu <jiawu2@cisco.com>
Date:   Tue Oct 8 18:19:43 2019 -0400

    cip: ips rule support for Common Industrial Protocol (CIP)

30 files changed:
src/pub_sub/CMakeLists.txt
src/pub_sub/cip_events.cc [new file with mode: 0644]
src/pub_sub/cip_events.h [new file with mode: 0644]
src/service_inspectors/CMakeLists.txt
src/service_inspectors/cip/CMakeLists.txt [new file with mode: 0644]
src/service_inspectors/cip/cip.cc [new file with mode: 0644]
src/service_inspectors/cip/cip.h [new file with mode: 0644]
src/service_inspectors/cip/cip_definitions.h [new file with mode: 0644]
src/service_inspectors/cip/cip_module.cc [new file with mode: 0644]
src/service_inspectors/cip/cip_module.h [new file with mode: 0644]
src/service_inspectors/cip/cip_paf.cc [new file with mode: 0644]
src/service_inspectors/cip/cip_paf.h [new file with mode: 0644]
src/service_inspectors/cip/cip_parsing.cc [new file with mode: 0644]
src/service_inspectors/cip/cip_parsing.h [new file with mode: 0644]
src/service_inspectors/cip/cip_session.cc [new file with mode: 0644]
src/service_inspectors/cip/cip_session.h [new file with mode: 0644]
src/service_inspectors/cip/cip_util.h [new file with mode: 0644]
src/service_inspectors/cip/dev_notes.txt [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_attribute.cc [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_class.cc [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_connpathclass.cc [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_enipcommand.cc [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_enipreq.cc [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_eniprsp.cc [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_instance.cc [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_req.cc [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_rsp.cc [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_service.cc [new file with mode: 0644]
src/service_inspectors/cip/ips_cip_status.cc [new file with mode: 0644]
src/service_inspectors/service_inspectors.cc

index 4f0cb9c5307161bad430003e0b90013c190f822a..290e4d7aaee3ba4680156acf13bc9192071aa1da 100644 (file)
@@ -1,5 +1,6 @@
 set (PUB_SUB_INCLUDES
     appid_events.h
+    cip_events.h
     daq_message_event.h
     expect_events.h
     finalize_packet_event.h
@@ -9,6 +10,7 @@ set (PUB_SUB_INCLUDES
 
 add_library( pub_sub OBJECT
     ${PUB_SUB_INCLUDES}
+    cip_events.cc
     http_events.cc
     sip_events.cc
 )
diff --git a/src/pub_sub/cip_events.cc b/src/pub_sub/cip_events.cc
new file mode 100644 (file)
index 0000000..1d40dd3
--- /dev/null
@@ -0,0 +1,36 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// sip_events.cc author Jian Wu <jiawu2@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "cip_events.h"
+
+#include "service_inspectors/cip/cip.h"
+
+using namespace snort;
+using namespace std;
+
+CipEvent::CipEvent(const Packet* p, const CipEventData* EventData)
+{
+    this->p = p;
+    this->EventData = EventData;
+}
+
diff --git a/src/pub_sub/cip_events.h b/src/pub_sub/cip_events.h
new file mode 100644 (file)
index 0000000..804020f
--- /dev/null
@@ -0,0 +1,58 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// cip_events.h author Jian Wu <jiawu2@cisco.com>
+
+#ifndef CIP_EVENTS_H
+#define CIP_EVENTS_H
+
+// This event conveys data published by the CIP service inspector to be consumed
+// by data bus subscribers
+
+#include <list>
+
+#include "framework/data_bus.h"
+
+#define CIP_EVENT_TYPE_CIP_DATA_KEY "cip_event_type_cip_data"
+
+enum CipEventType
+{
+    CIP_EVENT_TYPE_CIP_DATA
+};
+
+namespace snort
+{
+struct Packet;
+struct SfIp;
+}
+
+struct CipEventData;
+
+class CipEvent : public snort::DataEvent
+{
+public:
+    CipEvent(const snort::Packet*, const CipEventData*);
+
+    const snort::Packet* get_packet() override
+    { return p; }
+
+private:
+    const snort::Packet* p;
+    const CipEventData* EventData;
+};
+
+#endif
index 962f60433e43d75117a807fd1ba7644b2fcb8dcc..f1a831eca3a3e4ea34118bc45c932a5cedcf7993 100644 (file)
@@ -1,5 +1,6 @@
 
 add_subdirectory(back_orifice)
+add_subdirectory(cip)
 add_subdirectory(dce_rpc)
 add_subdirectory(dnp3)
 add_subdirectory(dns)
@@ -21,6 +22,7 @@ add_subdirectory(s7commplus)
 if (STATIC_INSPECTORS)
     set (STATIC_INSPECTOR_OBJS
         $<TARGET_OBJECTS:back_orifice>
+        $<TARGET_OBJECTS:cip>
         $<TARGET_OBJECTS:dce_rpc>
         $<TARGET_OBJECTS:dnp3>
         $<TARGET_OBJECTS:dns>
diff --git a/src/service_inspectors/cip/CMakeLists.txt b/src/service_inspectors/cip/CMakeLists.txt
new file mode 100644 (file)
index 0000000..4ca03de
--- /dev/null
@@ -0,0 +1,34 @@
+set( FILE_LIST
+    cip.cc
+    cip.h
+    cip_definitions.h
+    cip_module.cc
+    cip_module.h
+    cip_paf.cc
+    cip_paf.h
+    cip_parsing.cc
+    cip_parsing.h
+    cip_session.cc
+    cip_session.h
+    cip_util.h
+    ips_cip_attribute.cc
+    ips_cip_class.cc
+    ips_cip_connpathclass.cc  
+    ips_cip_enipcommand.cc
+    ips_cip_enipreq.cc
+    ips_cip_eniprsp.cc   
+    ips_cip_instance.cc
+    ips_cip_req.cc
+    ips_cip_rsp.cc
+    ips_cip_service.cc
+    ips_cip_status.cc
+)
+
+if (STATIC_INSPECTORS)
+    add_library(cip OBJECT ${FILE_LIST})
+
+else (STATIC_INSPECTORS)
+    add_dynamic_module(cip inspectors ${FILE_LIST})
+
+endif (STATIC_INSPECTORS)
+
diff --git a/src/service_inspectors/cip/cip.cc b/src/service_inspectors/cip/cip.cc
new file mode 100644 (file)
index 0000000..2352423
--- /dev/null
@@ -0,0 +1,406 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: service inspector for the CIP protocol. */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "cip.h"
+
+#include "detection/detection_engine.h"
+#include "events/event_queue.h"
+#include "log/messages.h"
+#include "managers/inspector_manager.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+#include "pub_sub/cip_events.h"
+#include "stream/stream_splitter.h"
+#include "utils/util.h"              // for snort_calloc
+
+#include "cip_module.h"
+#include "cip_paf.h"
+#include "cip_parsing.h"
+
+using namespace snort;
+
+THREAD_LOCAL ProfileStats cip_perf_stats;
+
+unsigned CipFlowData::inspector_id = 0;
+
+static void free_cip_data(void* data);
+
+CipFlowData::CipFlowData() : FlowData(inspector_id)
+{
+    memset(&session, 0, sizeof(session));
+    cip_stats.sessions++;
+    cip_stats.concurrent_sessions++;
+    if (cip_stats.max_concurrent_sessions < cip_stats.concurrent_sessions)
+        cip_stats.max_concurrent_sessions = cip_stats.concurrent_sessions;
+}
+
+CipFlowData::~CipFlowData()
+{
+    free_cip_data(&session);
+    assert(cip_stats.concurrent_sessions > 0);
+    cip_stats.concurrent_sessions--;
+}
+
+CipSessionData* get_cip_session_data(const Flow* flow)
+{
+    CipFlowData* fd = static_cast<CipFlowData*>(flow->get_flow_data(CipFlowData::inspector_id));
+    return fd ? &fd->session : nullptr;
+}
+
+static CipSessionData* set_new_cip_session_data(CipProtoConf* config, Packet* p)
+{
+    CipFlowData* fd = new CipFlowData;
+    CipSessionData* css = &fd->session;
+
+    p->flow->set_flow_data(fd);
+
+    /* Only allocate global state data for TCP connections. */
+    if (p->has_tcp_data())
+    {
+        css->global_data.connection_list.list_size = config->max_cip_connections;
+        css->global_data.connection_list.list
+            = static_cast<CipConnection*>(snort_calloc(config->max_cip_connections,
+            sizeof(CipConnection)));
+
+        css->global_data.unconnected_list.list_size = config->max_unconnected_messages;
+        css->global_data.unconnected_list.list
+            = static_cast<CipUnconnectedMessage*>(snort_calloc(config->max_unconnected_messages,
+            sizeof(CipUnconnectedMessage)));
+    }
+
+    return &fd->session;
+}
+
+static void free_cip_data(void* data)
+{
+    CipSessionData* css = static_cast<CipSessionData*>(data);
+
+    if ( css->global_data.connection_list.list )
+    {
+        snort_free(css->global_data.connection_list.list);
+        css->global_data.connection_list.list = nullptr;
+    }
+
+    if ( css->global_data.unconnected_list.list )
+    {
+        snort_free(css->global_data.unconnected_list.list);
+        css->global_data.unconnected_list.list = nullptr;
+    }
+}
+
+static void print_cip_conf(CipProtoConf* config)
+{
+    if (config == nullptr)
+        return;
+    LogMessage("CIP config: \n");
+    LogMessage("    Embedded Enabled: %s\n",
+        config->embedded_cip_enabled ? "ENABLED" : "DISABLED");
+    if (config->embedded_cip_enabled)
+    {
+        LogMessage("    Embedded Class: 0x%x\n", config->embedded_cip_class_id);
+        LogMessage("    Embedded Service: 0x%x\n", config->embedded_cip_service_id);
+    }
+
+    LogMessage("    Unconnected Timeout: %d (seconds)\n", config->unconnected_timeout);
+    LogMessage("    Max CIP connections per TCP connection: %d\n",
+        static_cast<int>(config->max_cip_connections));
+    LogMessage("    Max unconnected messages per TCP connection: %d\n",
+        static_cast<int>(config->max_unconnected_messages));
+
+    LogMessage("\n");
+}
+
+static CipPacketDirection get_packet_direction(Packet* p)
+{
+    if (!p->has_tcp_data())
+    {
+        return CIP_FROM_UNKNOWN;
+    }
+    if (p->packet_flags & PKT_FROM_CLIENT)
+    {
+        return CIP_FROM_CLIENT;
+    }
+    return CIP_FROM_SERVER;
+}
+
+static void publish_data_to_appId(Packet* packet, CipCurrentData& current_data)
+{
+    CipEventData cip_event_data;
+    CipEvent cip_event(packet, &cip_event_data);
+
+    bool publish_appid = true;
+
+    // Set one specific matching type for this PDU, in order of priority.
+    if (current_data.invalid_fatal)
+    {
+        cip_event_data.type = CIP_DATA_TYPE_MALFORMED;
+    }
+    else if (current_data.cip_message_type == CipMessageTypeExplicit)
+    {
+        if (current_data.cip_msg.is_cip_request)
+        {
+            /* Just Cip implement this function in parsing.cc  */
+            pack_cip_request_event(&current_data.cip_msg.request, &cip_event_data);
+        }
+        else
+        {
+            // Do not attempt to set applications for CIP responses.
+            publish_appid = false;
+        }
+    }
+    else if (current_data.cip_message_type == CipMessageTypeImplicit)
+    {
+        cip_event_data.type = CIP_DATA_TYPE_IMPLICIT;
+        cip_event_data.class_id = current_data.enip_data.connection_class_id;
+    }
+    else if (current_data.enip_data.enip_decoded)
+    {
+        cip_event_data.type = CIP_DATA_TYPE_ENIP_COMMAND;
+        cip_event_data.enip_command_id = current_data.enip_data.enip_header.command;
+    }
+    else
+    {
+        cip_event_data.type = CIP_DATA_TYPE_OTHER;
+    }
+
+    if (publish_appid)
+    {
+        DataBus::publish(CIP_EVENT_TYPE_CIP_DATA_KEY, cip_event, packet->flow);
+    }
+}
+
+static void log_cip_validity_errors(const CipCurrentData& current_data,
+    CipGlobalSessionData& global_data)
+{
+    if (current_data.invalid_fatal)
+    {
+        /* what is engine  */
+        DetectionEngine::queue_event(GID_CIP, CIP_MALFORMED);
+    }
+    else if (current_data.enip_data.enip_invalid_nonfatal != 0
+        || current_data.cip_msg.request.cip_req_invalid_nonfatal != 0)
+    {
+        DetectionEngine::queue_event(GID_CIP, CIP_NON_CONFORMING);
+    }
+
+    if (global_data.connection_list.connection_pruned)
+    {
+        DetectionEngine::queue_event(GID_CIP, CIP_CONNECTION_LIMIT);
+        global_data.connection_list.connection_pruned = false;
+    }
+
+    if (global_data.unconnected_list.request_pruned)
+    {
+        DetectionEngine::queue_event(GID_CIP, CIP_REQUEST_LIMIT);
+        global_data.unconnected_list.request_pruned = false;
+    }
+}
+
+static void cip_current_data_process(CipSessionData* css, CipCurrentData& current_data,
+    CipProtoConf* config, Packet* p)
+{
+    /* Current Data should be implemented as the same with c files  */
+    memset(&current_data, 0, sizeof(CipCurrentData));
+    current_data.direction = get_packet_direction(p);
+
+    css->global_data.config = config;
+    css->global_data.snort_packet = p;
+
+    /* parse_enip_layer should be implemented specifically  */
+    current_data.invalid_fatal = !parse_enip_layer(p->data,
+        p->dsize,
+        p->has_tcp_data(),
+        &current_data,
+        &css->global_data);
+
+    if (!current_data.invalid_fatal
+        && (p->dsize != current_data.enip_data.enip_header.length + ENIP_HEADER_SIZE))
+    {
+        current_data.enip_data.enip_invalid_nonfatal |= ENIP_INVALID_PAYLOAD_SIZE;
+    }
+}
+
+static void snort_cip(CipProtoConf* config, Packet* p)
+{
+    Profile profile(cip_perf_stats);
+
+    if (p->has_tcp_data() && !p->is_full_pdu())
+        return;
+
+    p->packet_flags |= PKT_ALLOW_MULTIPLE_DETECT;
+    CipSessionData* css = get_cip_session_data(p->flow);
+
+    if (css == nullptr)
+    {
+        css = set_new_cip_session_data(config, p);
+    }
+
+    CipCurrentData& current_data = css->current_data;
+    cip_current_data_process(css, current_data, config, p);
+    publish_data_to_appId(p, current_data);
+    log_cip_validity_errors(current_data, css->global_data);
+}
+
+//-------------------------------------------------------------------------
+// class stuff
+//-------------------------------------------------------------------------
+
+class Cip : public Inspector
+{
+public:
+    Cip(CipProtoConf*);
+    ~Cip() override;
+
+    void show(SnortConfig*) override;
+    void eval(Packet*) override;
+
+    class StreamSplitter* get_splitter(bool c2s) override
+    {
+        return new CipSplitter(c2s);
+    }
+
+private:
+    CipProtoConf* config;
+};
+
+Cip::Cip(CipProtoConf* pc)
+{
+    config = pc;
+}
+
+Cip::~Cip()
+{
+    if (config)
+    {
+        delete config;
+    }
+}
+
+void Cip::show(SnortConfig*)
+{
+    /* defined in module.cc */
+    print_cip_conf(config);
+}
+
+void Cip::eval(Packet* p)
+{
+    assert(p->has_tcp_data() || p->has_udp_data());
+    assert(p->flow);
+    cip_stats.packets++;
+    snort_cip(config, p);
+}
+
+//-------------------------------------------------------------------------
+// api stuff
+//-------------------------------------------------------------------------
+
+static Module* mod_ctor()
+{
+    return new CipModule;
+}
+
+static void mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static void cip_init()
+{
+    CipFlowData::init();
+}
+
+static Inspector* cip_ctor(Module* m)
+{
+    CipModule* mod = static_cast<CipModule*>(m);
+    return new Cip(mod->get_data());
+}
+
+static void cip_dtor(Inspector* p)
+{
+    delete p;
+}
+
+const InspectApi cip_api =
+{
+    {
+        PT_INSPECTOR,
+        sizeof(InspectApi),
+        INSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        CIP_NAME,
+        CIP_HELP,
+        mod_ctor,
+        mod_dtor
+    },
+    IT_SERVICE,
+    PROTO_BIT__PDU,
+    nullptr,  // buffers
+    "cip",
+    cip_init,
+    nullptr, // pterm
+    nullptr, // tinit
+    nullptr, // tterm
+    cip_ctor,
+    cip_dtor,
+    nullptr, // ssn
+    nullptr  // reset
+};
+
+extern const BaseApi* ips_cip_attribute;
+extern const BaseApi* ips_cip_class;
+extern const BaseApi* ips_cip_connpathclass;
+extern const BaseApi* ips_cip_enipcommand;
+extern const BaseApi* ips_cip_enipreq;
+extern const BaseApi* ips_cip_eniprsp;
+extern const BaseApi* ips_cip_instance;
+extern const BaseApi* ips_cip_req;
+extern const BaseApi* ips_cip_rsp;
+extern const BaseApi* ips_cip_service;
+extern const BaseApi* ips_cip_status;
+
+#ifdef BUILDING_SO
+SO_PUBLIC const BaseApi* snort_plugins[] =
+#else
+const BaseApi* sin_cip[] =
+#endif
+{
+    &cip_api.base,
+    ips_cip_attribute,
+    ips_cip_class,
+    ips_cip_connpathclass,
+    ips_cip_enipcommand,
+    ips_cip_enipreq,
+    ips_cip_eniprsp,
+    ips_cip_instance,
+    ips_cip_req,
+    ips_cip_rsp,
+    ips_cip_service,
+    ips_cip_status,
+    nullptr
+};
+
diff --git a/src/service_inspectors/cip/cip.h b/src/service_inspectors/cip/cip.h
new file mode 100644 (file)
index 0000000..5a4b6cc
--- /dev/null
@@ -0,0 +1,114 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip.h author RA/Cisco Jian Wu <jiawu2@cisco.com>
+
+#ifndef CIP_H
+#define CIP_H
+// Implementation header with definitions, datatypes and flowdata class for CIP service inspector.
+
+#include "flow/flow.h"
+#include "framework/counts.h"
+#include "main/thread.h"
+#include "protocols/packet.h"
+
+#include "cip_definitions.h"
+
+namespace snort
+{
+struct Packet;
+}
+
+enum CipDataType
+{
+    CIP_DATA_TYPE_PATH_CLASS = 0,
+    CIP_DATA_TYPE_PATH_EXT_SYMBOL,
+    CIP_DATA_TYPE_SET_ATTRIBUTE,
+    CIP_DATA_TYPE_CONNECTION,
+    CIP_DATA_TYPE_IMPLICIT,
+    CIP_DATA_TYPE_OTHER,
+    CIP_DATA_TYPE_ENIP_COMMAND,
+    CIP_DATA_TYPE_MALFORMED
+};
+
+struct CipEventData
+{
+    // Specify the type of CIP data.
+    CipDataType type;
+
+    // Used for:
+    //  CIP_DATA_TYPE_ENIP_COMMAND
+    uint16_t enip_command_id;
+
+    // Used for:
+    //  CIP_DATA_TYPE_PATH_CLASS
+    //  CIP_DATA_TYPE_PATH_EXT_SYMBOL
+    //  CIP_DATA_TYPE_SET_ATTRIBUTE
+    uint8_t service_id;
+
+    // Used for:
+    //  CIP_DATA_TYPE_PATH_CLASS: This represents the Request Path Class.
+    //  CIP_DATA_TYPE_SET_ATTRIBUTE: This represents the Request Path Class.
+    //  CIP_DATA_TYPE_CONNECTION: This represents the Connection Path Class.
+    //  CIP_DATA_TYPE_IMPLICIT: This represents the Connection Path Class from
+    //      the original connection request, for this connection.
+    uint32_t class_id;
+
+    // Used for:
+    //  CIP_DATA_TYPE_SET_ATTRIBUTE: This represents the Request Path Instance.
+    uint32_t instance_id;
+
+    // Used for:
+    //  CIP_DATA_TYPE_SET_ATTRIBUTE: This represents the Request Path Attribute.
+    uint32_t attribute_id;
+
+    // Pointer to snort::Packet
+    const snort::Packet* snort_packet;
+};
+
+class CipFlowData : public snort::FlowData
+{
+public:
+    CipFlowData();
+    ~CipFlowData() override;
+
+    static void init()
+    { inspector_id = snort::FlowData::create_flow_data_id(); }
+
+    size_t size_of() override
+    { return sizeof(*this); }
+
+public:
+    static unsigned inspector_id;
+    CipSessionData session;
+};
+
+CipSessionData* get_cip_session_data(const snort::Flow*);
+
+struct CipStats
+{
+    PegCount packets;
+    PegCount sessions;
+    PegCount concurrent_sessions;
+    PegCount max_concurrent_sessions;
+};
+
+extern THREAD_LOCAL CipStats cip_stats;
+
+#endif
+
diff --git a/src/service_inspectors/cip/cip_definitions.h b/src/service_inspectors/cip/cip_definitions.h
new file mode 100644 (file)
index 0000000..b9bba15
--- /dev/null
@@ -0,0 +1,524 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip_definitions.h author RA/Cisco
+
+/* Description: Common types for the CIP preprocessor. */
+
+#ifndef CIP_DEFINITIONS_H
+#define CIP_DEFINITIONS_H
+
+namespace snort
+{
+struct Packet;
+}
+
+#define MSEC_PER_SEC (1000)
+#define USEC_PER_SEC (1000000)
+
+// CIP preprocessor configuration
+struct CipProtoConf
+{
+    // Unconnected timeout, seconds.
+    uint32_t unconnected_timeout;
+
+    // Maximum number of unconnected requests per TCP connection.
+    size_t max_unconnected_messages;
+
+    // Maximum number of CIP connections per TCP connection.
+    size_t max_cip_connections;
+
+    // Custom embedded packet parameters.
+    bool embedded_cip_enabled;
+    uint32_t embedded_cip_class_id;
+    uint8_t embedded_cip_service_id;
+};
+
+/// CIP Request/Response Management
+enum CipRequestType
+{
+    CipRequestTypeOther = 0,
+    CipRequestTypeForwardOpen,
+    CipRequestTypeForwardClose,
+    CipRequestTypeUnconnectedSend,
+    CipRequestTypeMultipleServiceRequest,
+
+    // Special case to represent when no request is found for a given response.
+    CipRequestTypeNoMatchFound
+};
+
+struct CipStatus
+{
+    uint8_t general_status;
+    size_t extended_status_size;
+};
+
+enum CipPacketDirection
+{
+    CIP_FROM_CLIENT,
+    CIP_FROM_SERVER,
+    CIP_FROM_UNKNOWN
+};
+
+/// EtherNet/IP encapsulation layer definitions.
+
+// EtherNet/IP encapsulation header
+struct EnipHeader
+{
+    uint16_t command;
+    uint16_t length;
+    uint32_t session_handle;
+    uint32_t status;
+    uint64_t sender_context;
+    uint32_t options;
+};
+
+// This is an EtherNet/IP encapsulation layer common packet format item.
+struct EnipCpfItem
+{
+    uint16_t type;
+    uint16_t length;
+
+    // Used if length > 0. Data starts after the Length field.
+    const uint8_t* data;
+};
+
+// Largest number of allowed CPF items for standard EtherNet/IP commands.
+#define MAX_NUM_CPF_ITEMS 4
+
+// This is an EtherNet/IP encapsulation layer common packet format.
+struct EnipCpf
+{
+    uint16_t item_count;
+
+    // All CPF items in the list are valid up to and including array index item_count.
+    EnipCpfItem item_list[MAX_NUM_CPF_ITEMS];
+};
+
+/// CIP layer definitions.
+enum CipMessageType
+{
+    // Unknown CIP data type
+    CipMessageTypeUnknown,
+
+    // CIP Explicit Data
+    CipMessageTypeExplicit,
+
+    // CIP Implicit Data
+    CipMessageTypeImplicit
+};
+
+enum CipSegmentType
+{
+    CipSegment_Type_PORT_LINK_ADDRESS,
+    CipSegment_Type_PORT_LINK_ADDRESS_EXTENDED,
+
+    CipSegment_Type_LOGICAL_CLASS,
+    CipSegment_Type_LOGICAL_INSTANCE,
+    CipSegment_Type_LOGICAL_MEMBER,
+    CipSegment_Type_LOGICAL_CONN_POINT,
+    CipSegment_Type_LOGICAL_ATTRIBUTE,
+    CipSegment_Type_LOGICAL_ELECTRONIC_KEY,
+    CipSegment_Type_LOGICAL_EXTENDED,
+    CipSegment_Type_LOGICAL_SERVICE_ID,
+
+    CipSegment_Type_NETWORK,
+
+    CipSegment_Type_SYMBOLIC,
+
+    CipSegment_Type_DATA_SIMPLE,
+    CipSegment_Type_DATA_EXT_SYMBOL,
+
+    CipSegment_Type_UNKNOWN
+};
+
+#define CIP_STATUS_SUCCESS 0
+#define ENIP_STATUS_SUCCESS 0
+
+// CIP Classes
+#define MESSAGE_ROUTER_CLASS_ID 0x02
+#define CONNECTION_MANAGER_CLASS_ID 0x06
+
+// CIP Services
+#define SERVICE_SET_ATTRIBUTE_SINGLE 0x10
+#define SERVICE_MULTIPLE_SERVICE_PACKET 0x0A
+
+// CIP Connection Manager Services
+#define CONNECTION_MANAGER_UNCONNECTED_SEND 0x52
+#define CONNECTION_MANAGER_FORWARD_OPEN 0x54
+#define CONNECTION_MANAGER_LARGE_FORWARD_OPEN 0x5B
+#define CONNECTION_MANAGER_FORWARD_CLOSE 0x4E
+
+#define CIP_WORD_TO_BYTES 2
+
+struct CipSegment
+{
+    CipSegmentType type;
+
+    // Total size of this segment.
+    size_t size;
+
+    // When type = CipSegment_Type_PORT_LINK_ADDRESS
+    // When type = CipSegment_Type_PORT_LINK_ADDRESS_EXTENDED
+    uint16_t port_id;
+
+    // When type = CipSegment_Type_PORT_LINK_ADDRESS
+    uint8_t link_address;
+
+    // When type = CipSegment_Type_LOGICAL_CLASS
+    // When type = CipSegment_Type_LOGICAL_INSTANCE
+    // When type = CipSegment_Type_LOGICAL_MEMBER
+    // When type = CipSegment_Type_LOGICAL_CONN_POINT
+    // When type = CipSegment_Type_LOGICAL_ATTRIBUTE
+    uint32_t logical_value;
+
+    // When type = CipSegment_Type_PORT_LINK_ADDRESS_EXTENDED, this is the link address.
+    // When type = CipSegment_Type_DATA_EXT_SYMBOL, this is the symbol string.
+    // When type = CipSegment_Type_DATA_SIMPLE, this is the start of the data words.
+    // When type = CipSegment_Type_SYMBOLIC, this is the symbol string.
+    const uint8_t* data;
+    size_t data_size;
+};
+
+struct CipPath
+{
+    // Size of the entire path.
+    size_t full_path_size;
+
+    // True if path has been decoded successfully.
+    bool decoded;
+
+    // Main segment type for this path, which drives message target.
+    CipSegmentType primary_segment_type;
+
+    bool has_class_id;
+    uint32_t class_id;
+
+    bool has_instance_id;
+    uint32_t instance_id;
+
+    bool has_attribute_id;
+    uint32_t attribute_id;
+
+    bool has_unknown_segment;
+};
+
+// Matching pair of CIP Connection IDs.
+struct ConnectionIdPair
+{
+    uint32_t ot_connection_id;
+    uint32_t to_connection_id;
+};
+
+// RPI and Network Connection Parameters from a Forward Open Request.
+struct CipConnectionParameters
+{
+    uint32_t rpi;
+    uint32_t network_connection_parameters;
+    bool is_null_connection;
+};
+
+// Unique Connection Signature. This is unique to each CIP connection. This
+//  tuple is unique on a given EtherNet/IP session.
+struct CipConnectionSignature
+{
+    uint16_t connection_serial_number;
+    uint16_t vendor_id;
+    uint32_t originator_serial_number;
+};
+
+#define TRANSPORT_CLASS_MASK 0x0F
+struct CipForwardOpenRequest
+{
+    // Unconnected request timeout, milliseconds.
+    uint32_t timeout_ms;
+
+    // Connection timeouts, microseconds.
+    uint64_t ot_connection_timeout_us;
+    uint64_t to_connection_timeout_us;
+
+    CipConnectionSignature connection_signature;
+
+    CipConnectionParameters ot_parameters;
+    CipConnectionParameters to_parameters;
+    uint8_t transport_class;
+
+    CipPath connection_path;
+
+    bool is_null_forward_open;
+
+    // Timestamp for message request.
+    struct timeval timestamp;
+};
+
+struct CipForwardOpenResponse
+{
+    // True if this was a successful Forward Open Response.
+    bool success;
+
+    // Properties for Success or Fail.
+    CipConnectionSignature connection_signature;
+
+    // Properties for a Forward Open Response Success.
+    ConnectionIdPair connection_pair;
+
+    size_t application_reply_size;
+
+    // Timestamp for message response.
+    struct timeval timestamp;
+};
+
+struct CipForwardCloseRequest
+{
+    // Unconnected request timeout, milliseconds.
+    uint32_t timeout_ms;
+
+    CipConnectionSignature connection_signature;
+
+    CipPath connection_path;
+};
+
+// Used to set error flags in enip_invalid_nonfatal.
+#define ENIP_INVALID_COMMAND (1 << 0)
+#define ENIP_INVALID_DUPLICATE_SESSION (1 << 1)
+#define ENIP_INVALID_SESSION_HANDLE (1 << 2)
+#define ENIP_INVALID_INTERFACE_HANDLE (1 << 3)
+#define ENIP_INVALID_CONNECTION_ID (1 << 4)
+#define ENIP_INVALID_PAYLOAD_SIZE (1 << 5)
+#define ENIP_INVALID_ENIP_COMMAND_CPF_MISMATCH (1 << 6)
+#define ENIP_INVALID_RESERVED_FUTURE_CPF_TYPE (1 << 7)
+#define ENIP_INVALID_STATUS (1 << 8)
+#define ENIP_INVALID_ENIP_TCP_ONLY (1 << 9)
+
+struct EnipSessionData
+{
+    // True if the ENIP Header was parsed and is valid.
+    bool enip_decoded;
+
+    // Full ENIP header.
+    EnipHeader enip_header;
+
+    // Error states for non-fatal ENIP errors. Error conditions that could trigger this:
+    //  - Command code was not valid according to CIP Volume 2, Section 2-3.2.
+    //  - RegisterSession attempted when a session was already active.
+    //  - Session Handle did not match an active session.
+    //  - Interface Handle != 0
+    //  - Connection ID does not match an active connection.
+    //  - Larger amount of ENIP data than specific in ENIP length.
+    //  - Invalid CPF data item for a particular ENIP command.
+    //  - CPF Item Type ID was found in the Reserved for future expansion range.
+    //  - ENIP Status != 0, for a Request.
+    //  - Attempting to send an ENIP command that is TCP only on a UDP connection.
+    uint32_t enip_invalid_nonfatal;
+
+    // True if the Common Packet Format was parsed and is valid.
+    bool cpf_decoded;
+
+    // True if the required CPF items are present for this EtherNet/IP command.
+    bool required_cpf_items_present;
+
+    // Common Packet Format data.
+    EnipCpf enip_cpf;
+
+    // Connection Class from original connection request, for connected messages.
+    uint32_t connection_class_id;
+};
+
+// Used to set error flags in cip_req_invalid_nonfatal.
+#define CIP_REQ_INVALID_CONNECTION_ADD_FAILED (1 << 0)
+#define CIP_REQ_INVALID_UNKNOWN_SEGMENT (1 << 1)
+#define CIP_REQ_INVALID_TIMEOUT_MULTIPLIER (1 << 2)
+struct CipRequest
+{
+    // CIP Service code.
+    uint8_t service;
+
+    CipPath request_path;
+
+    CipRequestType request_type;
+
+    // This is only valid for Unconnected Send messages.
+    CipPath route_path;
+
+    // CIP application payload data. This starts after the Request Path.
+    const uint8_t* cip_data;
+    size_t cip_data_size;
+
+    // Unconnected request timeout, milliseconds.
+    bool has_timeout;
+    uint32_t timeout_ms;
+
+    // True if this request was a Forward Open Request.
+    bool is_forward_open_request;
+
+    // Class ID in the Forward Open Request Connection Path.
+    // Used only when is_forward_open_request is true.
+    uint32_t connection_path_class_id;
+
+    // Error states for non-fatal CIP errors. Error conditions that could trigger this:
+    //  - Forward Open Request received but couldn't add the connection to the list because a
+    //      connection already existed with that signature.
+    //  - Unknown segment type in request path.
+    //  - Forward Open Request contained invalid Connection Timeout Multiplier.
+    uint32_t cip_req_invalid_nonfatal;
+};
+
+struct CipResponse
+{
+    // CIP Service code. This does not include the first bit set (0x80).
+    uint8_t service;
+
+    CipStatus status;
+};
+
+struct CipMessage
+{
+    // True if this is a CIP request (vs response).
+    bool is_cip_request;
+
+    // Used if is_cip_request is true.
+    CipRequest request;
+
+    // Used if is_cip_request is false.
+    CipResponse response;
+};
+
+struct CipCurrentData
+{
+    CipPacketDirection direction;
+
+    // ENIP layer data.
+    EnipSessionData enip_data;
+
+    // CIP layer data.
+    CipMessageType cip_message_type;
+
+    // Used if cip_message_type is CipMessageTypeExplicit
+    CipMessage cip_msg;
+
+    // True if the packet was not able to be fully parsed.
+    bool invalid_fatal;
+};
+
+struct EnipSession
+{
+    // ENIP session handle.
+    uint32_t session_handle;
+
+    // True if this session is active.
+    bool active;
+};
+
+// This represents an Unconnected message request.
+//  Sender Context -> Request Type
+struct CipUnconnectedMessage
+{
+    uint64_t sender_context;
+
+    CipRequestType request_type;
+
+    // Unconnected request timeout, milliseconds.
+    uint32_t timeout_ms;
+
+    // Timestamp for message request.
+    struct timeval timestamp;
+
+    // True if this entry is in use.
+    bool slot_active;
+};
+
+struct CipUnconnectedMessageList
+{
+    CipUnconnectedMessage* list;
+    size_t list_size;
+
+    // True if an active request was forced to be pruned.
+    bool request_pruned;
+
+    size_t count;
+};
+
+// This represents a CIP connection.
+// This is used to:
+//  a) Get the connection IDs, during a Forward Close.
+//  b) Get Class ID from the Connection Path.
+struct CipConnection
+{
+    CipConnectionSignature signature;
+
+    ConnectionIdPair connection_id_pair;
+
+    // Class ID from the Connection Path
+    uint32_t class_id;
+
+    // True if the connection is fully established.
+    bool established;
+
+    // Connection timeouts, seconds.
+    uint32_t ot_connection_timeout_sec;
+    uint32_t to_connection_timeout_sec;
+
+    // Timestamp for last time connection was active.
+    struct timeval ot_timestamp;
+    struct timeval to_timestamp;
+
+    // True if this entry is in use.
+    bool slot_active;
+};
+
+struct CipConnectionList
+{
+    CipConnection* list;
+    size_t list_size;
+
+    // True if an active connection was forced to be pruned.
+    bool connection_pruned;
+
+    size_t count;
+};
+
+struct CipGlobalSessionData
+{
+    // ENIP Session for this TCP connection.
+    EnipSession enip_session;
+
+    // List of CIP connections.
+    CipConnectionList connection_list;
+
+    // List of outstanding unconnected messages (SendRRData).
+    CipUnconnectedMessageList unconnected_list;
+
+    // Current configuration for use in lower-level parsing functions.
+    const CipProtoConf* config;
+
+    snort::Packet* snort_packet;
+};
+
+// This is the overall structure used by Snort to store current and global data
+//  for a particular stream.
+struct CipSessionData
+{
+    // Current data for this packet.
+    CipCurrentData current_data;
+
+    // Overall data for this session.
+    CipGlobalSessionData global_data;
+};
+
+#endif  // CIP_DEFINITIONS_H
+
diff --git a/src/service_inspectors/cip/cip_module.cc b/src/service_inspectors/cip/cip_module.cc
new file mode 100644 (file)
index 0000000..e6fa313
--- /dev/null
@@ -0,0 +1,157 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip_module.cc author Jian Wu <jiawu2@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "cip_module.h"
+
+#include <cassert>
+
+#include "cip.h"
+
+using namespace snort;
+using namespace std;
+
+#define CIP_MALFORMED_STR "CIP data is malformed."
+#define CIP_NON_CONFORMING_STR "CIP data is non-conforming to ODVA standard."
+#define CIP_CONNECTION_LIMIT_STR \
+    "CIP connection limit exceeded. Least recently used connection removed."
+#define CIP_REQUEST_LIMIT_STR "CIP unconnected request limit exceeded. Oldest request removed."
+
+static const Parameter c_params[] =
+{
+    { "embedded_cip_path", Parameter::PT_STRING, nullptr, "false",
+      "check embedded CIP path" },
+    { "unconnected_timeout", Parameter::PT_INT, "0:360", "300",
+      "unconnected timeout in seconds" },
+    { "max_cip_connections", Parameter::PT_INT, "1:10000", "100",
+      "max cip connections" },
+    { "max_unconnected_messages", Parameter::PT_INT, "1:10000", "100",
+      "max unconnected cip messages" },
+    { nullptr, Parameter::PT_STRING, nullptr, nullptr, nullptr }
+};
+
+static const RuleMap cip_rules[] =
+{
+    { CIP_MALFORMED, CIP_MALFORMED_STR },
+    { CIP_NON_CONFORMING, CIP_NON_CONFORMING_STR },
+    { CIP_CONNECTION_LIMIT, CIP_CONNECTION_LIMIT_STR },
+    { CIP_REQUEST_LIMIT, CIP_REQUEST_LIMIT_STR },
+    { 0, nullptr }
+};
+
+THREAD_LOCAL CipStats cip_stats;
+
+static const PegInfo cip_pegs[] =
+{
+    { CountType::SUM, "packets", "total packets" },
+    { CountType::SUM, "session", "total sessions" },
+    { CountType::NOW, "concurrent_sessions", "total concurrent SIP sessions" },
+    { CountType::MAX, "max_concurrent_sessions", "maximum concurrent SIP sessions" },
+    { CountType::END, nullptr, nullptr },
+};
+
+//-------------------------------------------------------------------------
+// cip module
+//-------------------------------------------------------------------------
+
+CipModule::CipModule() : Module(CIP_NAME, CIP_HELP, c_params)
+{
+    conf = nullptr;
+}
+
+CipModule::~CipModule()
+{
+    if ( conf )
+        delete conf;
+}
+
+const RuleMap* CipModule::get_rules() const
+{ return cip_rules; }
+
+const PegInfo* CipModule::get_pegs() const
+{ return cip_pegs; }
+
+PegCount* CipModule::get_counts() const
+{ return (PegCount*)&cip_stats; }
+
+ProfileStats* CipModule::get_profile() const
+{ return &cip_perf_stats; }
+
+bool CipModule::set(const char*, Value& v, SnortConfig*)
+{
+    if ( v.is("embedded_cip_path") )
+    {
+        conf->embedded_cip_enabled = true;
+        embedded_path = v.get_string();
+    }
+    else if ( v.is("unconnected_timeout") )
+        conf->unconnected_timeout = v.get_uint32();
+
+    else if ( v.is("max_cip_connections") )
+        conf->max_cip_connections = v.get_uint32();
+
+    else if ( v.is("max_unconnected_messages") )
+        conf->max_unconnected_messages = v.get_uint32();
+
+    else
+        return false;
+
+    return true;
+}
+
+CipProtoConf* CipModule::get_data()
+{
+    CipProtoConf* tmp = conf;
+    conf = nullptr;
+    return tmp;
+}
+
+bool CipModule::begin(const char*, int, SnortConfig*)
+{
+    assert(!conf);
+    conf = new CipProtoConf;
+
+    conf->embedded_cip_enabled = false;
+
+    return true;
+}
+
+bool CipModule::end(const char*, int, SnortConfig*)
+{
+    Value v(embedded_path.c_str());
+    std::string tok;
+    v.set_first_token();
+
+    if ( v.get_next_token(tok) )
+    {
+        conf->embedded_cip_class_id = static_cast<uint32_t>(::strtol(tok.c_str(), nullptr, 0));
+    }
+
+    if (v.get_next_token(tok) )
+    {
+        conf->embedded_cip_service_id = static_cast<uint8_t>(::strtol(tok.c_str(), nullptr, 0));
+    }
+
+    return true;
+}
+
diff --git a/src/service_inspectors/cip/cip_module.h b/src/service_inspectors/cip/cip_module.h
new file mode 100644 (file)
index 0000000..52eb5a9
--- /dev/null
@@ -0,0 +1,71 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip_module.h author Jian Wu <jiawu2@cisco.com>
+
+#ifndef CIP_MODULE_H
+#define CIP_MODULE_H
+
+// Interface to the CIP service inspector
+
+#include "framework/module.h"
+
+#include "cip_definitions.h"
+
+#define GID_CIP 148
+
+#define CIP_MALFORMED         1
+#define CIP_NON_CONFORMING    2
+#define CIP_CONNECTION_LIMIT  3
+#define CIP_REQUEST_LIMIT     4
+
+#define CIP_NAME "cip"
+#define CIP_HELP "cip inspection"
+
+extern THREAD_LOCAL snort::ProfileStats cip_perf_stats;
+
+class CipModule : public snort::Module
+{
+public:
+    CipModule();
+    ~CipModule() override;
+
+    bool set(const char*, snort::Value&, snort::SnortConfig*) override;
+    bool begin(const char*, int, snort::SnortConfig*) override;
+    bool end(const char*, int, snort::SnortConfig*) override;
+
+    unsigned get_gid() const override
+    { return GID_CIP; }
+
+    const snort::RuleMap* get_rules() const override;
+    const PegInfo* get_pegs() const override;
+    PegCount* get_counts() const override;
+    snort::ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return INSPECT; }
+
+    CipProtoConf* get_data();
+
+private:
+    CipProtoConf* conf;
+    std::string embedded_path;
+};
+
+#endif
+
diff --git a/src/service_inspectors/cip/cip_paf.cc b/src/service_inspectors/cip/cip_paf.cc
new file mode 100644 (file)
index 0000000..712f1b9
--- /dev/null
@@ -0,0 +1,104 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip_paf.cc author RA/Cisco
+
+/* Description: Protocol-Aware Flushing (PAF) code for the CIP preprocessor.*/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "cip_paf.h"
+#include "cip_parsing.h"              // For ENIP constants
+
+// PAF will skip over the ENIP Command and Length fields.
+static const uint16_t ENIP_PAF_FIELD_SIZE = 4;
+
+using namespace snort;
+
+/* Function: CIPPaf()
+
+   Purpose: CIP PAF callback.
+            Statefully inspects CIP traffic from the start of a session.
+            Reads up until the length octet is found, then sets a flush point.
+            The flushed PDU is a ENIP frame.
+*/
+
+static StreamSplitter::Status cip_paf(cip_paf_data* pafdata, const uint8_t* data,
+    uint32_t len, uint32_t* fp)
+{
+    uint32_t bytes_processed = 0;
+
+    /* Process this packet 1 byte at a time */
+    while (bytes_processed < len)
+    {
+        switch (pafdata->paf_state)
+        {
+        case CIP_PAF_STATE__COMMAND_1:
+            // Skip ENIP command.
+            pafdata->paf_state = CIP_PAF_STATE__COMMAND_2;
+            break;
+
+        case CIP_PAF_STATE__COMMAND_2:
+            // Skip ENIP command.
+            pafdata->paf_state = CIP_PAF_STATE__LENGTH_1;
+            break;
+
+        case CIP_PAF_STATE__LENGTH_1:
+            pafdata->enip_length = *(data + bytes_processed);
+            pafdata->paf_state = CIP_PAF_STATE__LENGTH_2;
+            break;
+
+        case CIP_PAF_STATE__LENGTH_2:
+            pafdata->enip_length |= (*(data + bytes_processed) << 8);
+            pafdata->paf_state = CIP_PAF_STATE__SET_FLUSH;
+            break;
+
+        case CIP_PAF_STATE__SET_FLUSH:
+            *fp = bytes_processed +
+                pafdata->enip_length + (ENIP_HEADER_SIZE - ENIP_PAF_FIELD_SIZE);
+
+            pafdata->paf_state = CIP_PAF_STATE__COMMAND_1;
+            return StreamSplitter::FLUSH;
+
+        default:
+            // Will not happen.
+            break;
+        }
+
+        bytes_processed++;
+    }
+
+    return StreamSplitter::SEARCH;
+}
+
+CipSplitter::CipSplitter(bool c2s) : StreamSplitter(c2s)
+{
+    state.paf_state = CIP_PAF_STATE__COMMAND_1;
+    state.enip_length = 0;
+}
+
+StreamSplitter::Status CipSplitter::scan(
+    Packet*, const uint8_t* data, uint32_t len,
+    uint32_t, uint32_t* fp)
+{
+    cip_paf_data* pfdata = &state;
+    return cip_paf(pfdata, data, len, fp);
+}
+
diff --git a/src/service_inspectors/cip/cip_paf.h b/src/service_inspectors/cip/cip_paf.h
new file mode 100644 (file)
index 0000000..12d522d
--- /dev/null
@@ -0,0 +1,64 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip_paf.h author RA/Cisco
+
+/* Description: Protocol-Aware Flushing (PAF) code for the CIP preprocessor. */
+
+#ifndef CIP_PAF_H
+#define CIP_PAF_H
+
+#include "stream/stream_splitter.h"
+
+#include "cip.h"
+
+/* State-tracking structs */
+enum cip_paf_state
+{
+    CIP_PAF_STATE__COMMAND_1 = 0,
+    CIP_PAF_STATE__COMMAND_2,
+    CIP_PAF_STATE__LENGTH_1,
+    CIP_PAF_STATE__LENGTH_2,
+    CIP_PAF_STATE__SET_FLUSH
+};
+
+struct cip_paf_data
+{
+    cip_paf_state paf_state;
+    uint16_t enip_length;
+};
+
+class CipSplitter : public snort::StreamSplitter
+{
+public:
+    CipSplitter(bool c2s);
+
+    Status scan(snort::Packet*, const uint8_t* data, uint32_t len, uint32_t flags,
+        uint32_t* fp) override;
+
+    bool is_paf() override
+    {
+        return true;
+    }
+
+public:
+    cip_paf_data state;
+};
+
+#endif /* CIP_PAF_H */
+
diff --git a/src/service_inspectors/cip/cip_parsing.cc b/src/service_inspectors/cip/cip_parsing.cc
new file mode 100644 (file)
index 0000000..6703474
--- /dev/null
@@ -0,0 +1,2107 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip_parsing.cc author RA/Cisco
+
+/* Description: Data parsing for EtherNet/IP and CIP formats.
+   Note: No pointer parameters to these functions can be passed as NULL. */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "cip_parsing.h"
+
+#include "framework/data_bus.h"
+
+#include "cip.h"
+#include "cip_session.h"  // For CIP connection tracking
+#include "cip_util.h"     // For GetLEUint16/32
+
+using namespace snort;
+
+/// EtherNet/IP Parsing Constants
+
+// Common Packet Format Item IDs.
+enum CpfItemId
+{
+    CPF_NULL_ADDRESS_ITEM_ID = 0x0000,
+    CPF_LIST_IDENTITY_ITEM_ID = 0x000C,
+    CPF_CONNECTED_ADDRESS_ITEM_ID = 0x00A1,
+    CPF_CONNECTED_DATA_ITEM_ID = 0x00B1,
+    CPF_UNCONNECTED_DATA_ITEM_ID = 0x00B2,
+    CPF_LIST_SERVICES_ITEM_ID = 0x0100,
+    CPF_SOCKADDR_INFO_OT_ITEM_ID = 0x8000,
+    CPF_SOCKADDR_INFO_TO_ITEM_ID = 0x8001,
+    CPF_SEQUENCED_ADDRESS_ITEM_ID = 0x8002
+};
+
+#define CPF_ADDRESS_ITEM_SLOT 0
+#define CPF_DATA_ITEM_SLOT 1
+#define CPF_LIST_REPLY_SLOT 0
+
+// Some ENIP command ranges are reserved for future range.
+#define ENIP_COMMAND_RESERVED1_START 0x0006
+#define ENIP_COMMAND_RESERVED1_END 0x0062
+#define ENIP_COMMAND_RESERVED2_START 0x00C8
+
+// Some CPF Item IDs are reserved for future range.
+#define ENIP_CPF_ITEM_RESERVED1_START 0x0086
+#define ENIP_CPF_ITEM_RESERVED1_END 0x0090
+#define ENIP_CPF_ITEM_RESERVED2_START 0x0092
+#define ENIP_CPF_ITEM_RESERVED2_END 0x00A0
+#define ENIP_CPF_ITEM_RESERVED3_START 0x00A5
+#define ENIP_CPF_ITEM_RESERVED3_END 0x00B0
+#define ENIP_CPF_ITEM_RESERVED4_START 0x00B3
+#define ENIP_CPF_ITEM_RESERVED4_END 0x00FF
+#define ENIP_CPF_ITEM_RESERVED5_START 0x0110
+#define ENIP_CPF_ITEM_RESERVED5_END 0x7FFF
+#define ENIP_CPF_ITEM_RESERVED6_START 0x8004
+
+#define REGISTER_SESSION_DATA_SIZE 4
+
+/// CIP Layer Parsing Constants
+const size_t CIP_PATH_SEGMENT_MIN_SIZE_BYTES = sizeof(uint16_t);
+
+// Typical payload size offset in CIP segment
+#define CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET 1
+
+// Offset to Segment Type/Format byte
+#define CIP_PATH_TYPE_OFFSET 0
+
+// Logical segment format mask and types.
+#define CIP_PATH_LOGICAL_FORMAT_MASK 0x03
+
+enum LogicalValueType
+{
+    CIP_PATH_LOGICAL_8_BIT = 0x00,
+    CIP_PATH_LOGICAL_16_BIT = 0x01,
+    CIP_PATH_LOGICAL_32_BIT = 0x02
+};
+
+// Logical segment types
+enum LogicalSegmentType
+{
+    CIP_PATH_LOGICAL_CLASS = 0x00,
+    CIP_PATH_LOGICAL_INSTANCE = 0x04,
+    CIP_PATH_LOGICAL_MEMBER = 0x08,
+    CIP_PATH_LOGICAL_CONN_POINT = 0x0c,
+    CIP_PATH_LOGICAL_ATTRIBUTE = 0x10,
+    CIP_PATH_LOGICAL_SPECIAL = 0x14,
+    CIP_PATH_LOGICAL_SERVICE_ID = 0x18,
+    CIP_PATH_LOGICAL_EXTENDED = 0x1C
+};
+
+enum SegmentType
+{
+    CIP_PATH_SEGMENT_PORT = 0x00,
+    CIP_PATH_SEGMENT_LOGICAL = 0x20,
+    CIP_PATH_SEGMENT_NETWORK = 0x40,
+    CIP_PATH_SEGMENT_SYMBOLIC = 0x60,
+    CIP_PATH_SEGMENT_DATA = 0x80
+};
+
+enum ExtendedStringType
+{
+    EXTENDED_STRING_DOUBLE = 0x20,
+    EXTENDED_STRING_TRIPLE = 0x40,
+    EXTENDED_STRING_NUMERIC = 0xC0
+};
+
+#define MESSAGE_ROUTER_RESPONSE_MASK 0x80
+
+#define CIP_STATUS_MIN_SIZE 2
+
+/// Prototypes
+static bool parse_logical_address_format(const uint8_t* data,
+    size_t data_length,
+    bool logical_extended,
+    CipSegment* segment);
+
+static bool parse_message_router_request(const uint8_t* data,
+    size_t data_length,
+    CipRequest* cip_request,
+    CipGlobalSessionData* global_data);
+
+/// Functions
+static bool enip_command_valid(uint16_t command)
+{
+    if ((ENIP_COMMAND_RESERVED1_START <= command && command <= ENIP_COMMAND_RESERVED1_END)
+        || (ENIP_COMMAND_RESERVED2_START <= command))
+    {
+        return false;
+    }
+
+    return true;
+}
+
+static bool enip_command_tcp_only(uint16_t command)
+{
+    // Allocated command codes.
+    if (command == ENIP_COMMAND_NOP
+        || command == ENIP_COMMAND_REGISTER_SESSION
+        || command == ENIP_COMMAND_UNREGISTER_SESSION
+        || command == ENIP_COMMAND_SEND_RR_DATA
+        || command == ENIP_COMMAND_SEND_UNIT_DATA)
+    {
+        return true;
+    }
+
+    return false;
+}
+
+static bool parse_enip_header(const uint8_t* data,
+    size_t data_length,
+    EnipSessionData* enip_session)
+{
+    EnipHeader* enip_header = &enip_session->enip_header;
+
+    if (data_length < ENIP_HEADER_SIZE)
+    {
+        return false;
+    }
+
+    #define ENIP_HEADER_OFFSET_COMMAND 0
+    #define ENIP_HEADER_OFFSET_LENGTH 2
+    #define ENIP_HEADER_OFFSET_HANDLE 4
+    #define ENIP_HEADER_OFFSET_STATUS 8
+    #define ENIP_HEADER_OFFSET_CONTEXT 12
+    #define ENIP_HEADER_OFFSET_OPTIONS 20
+
+    enip_header->command = GetLEUint16(&data[ENIP_HEADER_OFFSET_COMMAND]);
+    enip_header->length = GetLEUint16(&data[ENIP_HEADER_OFFSET_LENGTH]);
+    enip_header->session_handle = GetLEUint32(&data[ENIP_HEADER_OFFSET_HANDLE]);
+    enip_header->status = GetLEUint32(&data[ENIP_HEADER_OFFSET_STATUS]);
+    memcpy(&enip_header->sender_context,
+        &data[ENIP_HEADER_OFFSET_CONTEXT],
+        sizeof(enip_header->sender_context));
+    enip_header->options = GetLEUint32(&data[ENIP_HEADER_OFFSET_OPTIONS]);
+
+    if (!enip_command_valid(enip_header->command))
+    {
+        enip_session->enip_invalid_nonfatal |= ENIP_INVALID_COMMAND;
+    }
+
+    return true;
+}
+
+static bool cpf_item_id_valid(uint16_t item_id)
+{
+    if ((ENIP_CPF_ITEM_RESERVED1_START <= item_id && item_id <= ENIP_CPF_ITEM_RESERVED1_END)
+        || (ENIP_CPF_ITEM_RESERVED2_START <= item_id && item_id <= ENIP_CPF_ITEM_RESERVED2_END)
+        || (ENIP_CPF_ITEM_RESERVED3_START <= item_id && item_id <= ENIP_CPF_ITEM_RESERVED3_END)
+        || (ENIP_CPF_ITEM_RESERVED4_START <= item_id && item_id <= ENIP_CPF_ITEM_RESERVED4_END)
+        || (ENIP_CPF_ITEM_RESERVED5_START <= item_id && item_id <= ENIP_CPF_ITEM_RESERVED5_END)
+        || (ENIP_CPF_ITEM_RESERVED6_START <= item_id))
+    {
+        return false;
+    }
+
+    return true;
+}
+
+static bool cpf_item_length_valid(uint16_t item_id, size_t item_length)
+{
+    #define NULL_ADDRESS_ITEM_DATA_SIZE 0
+    #define CONNECTED_ADDRESS_ITEM_DATA_SIZE 4
+    #define SOCKADDR_INFO_ITEM_DATA_SIZE 16
+    #define SEQUENCED_ADDRESS_ITEM_DATA_SIZE 8
+
+    // Minimum data size for Connected and Unconnected Data Items when used with
+    //  CIP Class 3 / Explicit data.
+    #define MIN_CPF_CIP_DATA_SIZE 2
+
+    bool valid = true;
+
+    switch (item_id)
+    {
+    case CPF_NULL_ADDRESS_ITEM_ID:
+        if (item_length != NULL_ADDRESS_ITEM_DATA_SIZE)
+        {
+            valid = false;
+        }
+        break;
+    case CPF_CONNECTED_ADDRESS_ITEM_ID:
+        if (item_length != CONNECTED_ADDRESS_ITEM_DATA_SIZE)
+        {
+            valid = false;
+        }
+        break;
+    case CPF_CONNECTED_DATA_ITEM_ID:
+        if (item_length < MIN_CPF_CIP_DATA_SIZE)
+        {
+            valid = false;
+        }
+        break;
+    case CPF_UNCONNECTED_DATA_ITEM_ID:
+        if (item_length < MIN_CPF_CIP_DATA_SIZE)
+        {
+            valid = false;
+        }
+        break;
+    case CPF_SOCKADDR_INFO_OT_ITEM_ID:
+    case CPF_SOCKADDR_INFO_TO_ITEM_ID:
+        if (item_length != SOCKADDR_INFO_ITEM_DATA_SIZE)
+        {
+            valid = false;
+        }
+        break;
+    case CPF_SEQUENCED_ADDRESS_ITEM_ID:
+        if (item_length != SEQUENCED_ADDRESS_ITEM_DATA_SIZE)
+        {
+            valid = false;
+        }
+        break;
+    case CPF_LIST_IDENTITY_ITEM_ID:
+    case CPF_LIST_SERVICES_ITEM_ID:
+    default:
+        // No length checks for anything else.
+        break;
+    }
+
+    return valid;
+}
+
+static bool enip_command_cpf_valid(uint16_t command, const EnipCpf* enip_cpf)
+{
+    #define MIN_CPF_ITEMS_CIP_MESSAGE 2
+    #define MIN_CPF_ITEMS_LIST_REPLY 1
+
+    bool valid = true;
+
+    switch (command)
+    {
+    case ENIP_COMMAND_SEND_RR_DATA:
+        if (enip_cpf->item_count < MIN_CPF_ITEMS_CIP_MESSAGE
+            || enip_cpf->item_list[CPF_ADDRESS_ITEM_SLOT].type != CPF_NULL_ADDRESS_ITEM_ID
+            || enip_cpf->item_list[CPF_DATA_ITEM_SLOT].type != CPF_UNCONNECTED_DATA_ITEM_ID)
+        {
+            valid = false;
+        }
+        break;
+    case ENIP_COMMAND_SEND_UNIT_DATA:
+        if (enip_cpf->item_count != MIN_CPF_ITEMS_CIP_MESSAGE
+            || enip_cpf->item_list[CPF_ADDRESS_ITEM_SLOT].type != CPF_CONNECTED_ADDRESS_ITEM_ID
+            || enip_cpf->item_list[CPF_DATA_ITEM_SLOT].type != CPF_CONNECTED_DATA_ITEM_ID)
+        {
+            valid = false;
+        }
+        break;
+    case ENIP_COMMAND_LIST_SERVICES:
+        // Used in Reply only.
+        if (enip_cpf->item_count < MIN_CPF_ITEMS_LIST_REPLY
+            || enip_cpf->item_list[CPF_LIST_REPLY_SLOT].type != CPF_LIST_SERVICES_ITEM_ID)
+        {
+            valid = false;
+        }
+        break;
+    case ENIP_COMMAND_LIST_IDENTITY:
+        // Used in Reply only.
+        if (enip_cpf->item_count < MIN_CPF_ITEMS_LIST_REPLY
+            || enip_cpf->item_list[CPF_LIST_REPLY_SLOT].type != CPF_LIST_IDENTITY_ITEM_ID)
+        {
+            valid = false;
+        }
+        break;
+    default:
+        // Ignore commands without defined CPF items.
+        break;
+    }
+
+    return valid;
+}
+
+// Returns the CIP message type based on packet and session data. The data must already:
+//  1. Be ENIP_COMMAND_SEND_UNIT_DATA or ENIP_COMMAND_SEND_RR_DATA
+//  2. Have the required CPF items for that ENIP command.
+// This also saves connection related data for the given packet and updates connection timestamps.
+static CipMessageType get_cip_message_type(CipCurrentData* current_data,
+    CipGlobalSessionData* global_data)
+{
+    CipMessageType cip_message_type = CipMessageTypeUnknown;
+
+    if (current_data->enip_data.enip_header.command == ENIP_COMMAND_SEND_RR_DATA)
+    {
+        cip_message_type = CipMessageTypeExplicit;
+    }
+    else  // ENIP_COMMAND_SEND_UNIT_DATA
+    {
+        const EnipCpf* enip_cpf = &current_data->enip_data.enip_cpf;
+
+        if (enip_cpf->item_list[CPF_ADDRESS_ITEM_SLOT].length > 0)
+        {
+            uint32_t connection_id = GetLEUint32(enip_cpf->item_list[CPF_ADDRESS_ITEM_SLOT].data);
+
+            // Validate connected messages against CIP Connection List.
+            CipConnection* connection = cip_find_connection_by_id(
+                &global_data->connection_list,
+                current_data->direction,
+                connection_id,
+                true);
+            if (connection)
+            {
+                if (current_data->direction == CIP_FROM_CLIENT)
+                {
+                    connection->ot_timestamp = global_data->snort_packet->pkth->ts;
+                }
+                else
+                {
+                    connection->to_timestamp = global_data->snort_packet->pkth->ts;
+                }
+
+                current_data->enip_data.connection_class_id = connection->class_id;
+
+                if (connection->class_id == MESSAGE_ROUTER_CLASS_ID)
+                {
+                    cip_message_type = CipMessageTypeExplicit;
+                }
+                else
+                {
+                    cip_message_type = CipMessageTypeImplicit;
+                }
+            }
+            else
+            {
+                current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_CONNECTION_ID;
+                cip_message_type = CipMessageTypeUnknown;
+            }
+        }
+    }
+
+    return cip_message_type;
+}
+
+static bool parse_common_packet_format(const uint8_t* data,
+    size_t data_length,
+    EnipCpf* enip_cpf,
+    CipCurrentData* current_data)
+{
+    // The total item count is always first.
+    #define CPF_ITEM_COUNT_SIZE 2
+    if (data_length < CPF_ITEM_COUNT_SIZE)
+    {
+        return false;
+    }
+
+    #define CPF_OFFSET_ITEM_COUNT 0
+    #define CPF_ITEM_OFFSET_TYPE 0
+    #define CPF_ITEM_OFFSET_LENGTH 2
+    #define CPF_ITEM_OFFSET_DATA 4
+
+    enip_cpf->item_count = GetLEUint16(&data[CPF_OFFSET_ITEM_COUNT]);
+    data_length -= CPF_ITEM_COUNT_SIZE;
+
+    bool valid = true;
+
+    size_t current_item_offset = CPF_ITEM_COUNT_SIZE;
+
+    int i;
+    for (i = 0; i < enip_cpf->item_count; ++i)
+    {
+        uint16_t item_type;
+        uint16_t item_length;
+        const uint8_t* item_data;
+        /* This contains Type ID and Length. */
+        #define CPF_ITEM_HEADER_SIZE 4
+        if (data_length < CPF_ITEM_HEADER_SIZE)
+        {
+            valid = false;
+            break;
+        }
+
+        item_type = GetLEUint16(&data[current_item_offset + CPF_ITEM_OFFSET_TYPE]);
+        item_length = GetLEUint16(&data[current_item_offset + CPF_ITEM_OFFSET_LENGTH]);
+        item_data = nullptr;
+        if (item_length > 0)
+        {
+            item_data = &data[current_item_offset + CPF_ITEM_OFFSET_DATA];
+        }
+
+        data_length -= CPF_ITEM_HEADER_SIZE;
+
+        if (!cpf_item_id_valid(item_type))
+        {
+            current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_RESERVED_FUTURE_CPF_TYPE;
+        }
+
+        if (!cpf_item_length_valid(item_type, item_length))
+        {
+            valid = false;
+            break;
+        }
+
+        // Check that there is enough data left for the Item Length.
+        if (data_length < item_length)
+        {
+            valid = false;
+            break;
+        }
+
+        // Validate every CPF item, but only store data for a set amount.
+        if (i < MAX_NUM_CPF_ITEMS)
+        {
+            enip_cpf->item_list[i].type = item_type;
+            enip_cpf->item_list[i].length = item_length;
+            enip_cpf->item_list[i].data = item_data;
+        }
+
+        // Get data for the next item.
+        current_item_offset = current_item_offset + CPF_ITEM_HEADER_SIZE + item_length;
+        data_length -= item_length;
+    }
+
+    if (valid)
+    {
+        current_data->enip_data.required_cpf_items_present
+            = enip_command_cpf_valid(current_data->enip_data.enip_header.command, enip_cpf);
+        if (!current_data->enip_data.required_cpf_items_present)
+        {
+            current_data->enip_data.enip_invalid_nonfatal |=
+                ENIP_INVALID_ENIP_COMMAND_CPF_MISMATCH;
+        }
+    }
+
+    return valid;
+}
+
+// If there in an unknown segment type, then just set this segment to include
+//  all of the data left.
+static void set_unknown_segment_type(size_t data_length,
+    CipSegment* segment)
+{
+    segment->type = CipSegment_Type_UNKNOWN;
+    segment->size = data_length;
+}
+
+// Return the timeout, in milliseconds, based on the priority/time_tick and time-out_ticks fields
+//  that are common to: Unconnected Send, Forward Open, Forward Close.
+//  This requires that enough data is available to read 2 bytes.
+static uint32_t get_unconnected_timeout(const uint8_t* data)
+{
+    #define UNCONNECTED_OFFSET_PRIORITY_TIME_TICK 0
+    #define UNCONNECTED_OFFSET_TIMEOUT_TICKS 1
+    #define TICK_TIME_MASK 0xF
+
+    uint8_t tick_time = data[UNCONNECTED_OFFSET_PRIORITY_TIME_TICK] & TICK_TIME_MASK;
+    uint8_t timeout_ticks = data[UNCONNECTED_OFFSET_TIMEOUT_TICKS];
+
+    return (1 << tick_time) * timeout_ticks;
+}
+
+// Parses RPI and Network Connection Parameters from a Forward Open Request.
+// Note: Assumes there is enough data to parse the RPI and Network Connection Parameters.
+static void parse_connection_parameters(const uint8_t* data,
+    bool large_forward_open,
+    CipConnectionParameters* connection_parameters)
+{
+    #define NULL_CONNECTION_TYPE_MASK 0x6000
+    static const uint32_t LARGE_NULL_CONNECTION_TYPE_MASK = 0x60000000;
+
+    // Offsets to the RPI and Network Connection Parameters data, from the RPI data.
+    #define OFFSET_RPI 0
+    #define OFFSET_NETWORK_PARAMETERS 4
+
+    connection_parameters->rpi = GetLEUint32(&data[OFFSET_RPI]);
+
+    if (!large_forward_open)
+    {
+        uint16_t network_connection_parameters = GetLEUint16(&data[OFFSET_NETWORK_PARAMETERS]);
+        connection_parameters->network_connection_parameters = network_connection_parameters;
+
+        if ((network_connection_parameters & NULL_CONNECTION_TYPE_MASK) == 0)
+        {
+            connection_parameters->is_null_connection = true;
+        }
+    }
+    else  // SERVICE_LARGE_FORWARD_OPEN
+    {
+        uint32_t network_connection_parameters = GetLEUint32(&data[OFFSET_NETWORK_PARAMETERS]);
+        connection_parameters->network_connection_parameters = network_connection_parameters;
+
+        if ((network_connection_parameters & LARGE_NULL_CONNECTION_TYPE_MASK) == 0)
+        {
+            connection_parameters->is_null_connection = true;
+        }
+    }
+}
+
+// Note: Assumes there is enough data for a full Connection Signature.
+static void parse_connection_signature(const uint8_t* data,
+    CipConnectionSignature* connection_signature)
+{
+    #define OFFSET_CONNECTION_SERIAL 0
+    #define OFFSET_VENDOR 2
+    #define OFFSET_ORIGINATOR_SERIAL 4
+
+    connection_signature->connection_serial_number = GetLEUint16(&data[OFFSET_CONNECTION_SERIAL]);
+    connection_signature->vendor_id = GetLEUint16(&data[OFFSET_VENDOR]);
+    connection_signature->originator_serial_number = GetLEUint32(&data[OFFSET_ORIGINATOR_SERIAL]);
+}
+
+static bool parse_segment_electronic_key(const uint8_t* data,
+    size_t data_length,
+    CipSegment* segment)
+{
+    #define ELECTRONIC_KEY_FORMAT_TABLE 0x04
+    #define  ELECTRONIC_KEY_FORMAT_TABLE_SIZE 10
+
+    // Check that there is enough size for the Key Format Table.
+    if (ELECTRONIC_KEY_FORMAT_TABLE_SIZE > data_length)
+    {
+        return false;
+    }
+
+    // Currently, the only supported Key Format is the Key Format Table.
+    #define ELECTRONIC_KEY_OFFSET_FORMAT_TABLE 1
+    if (data[ELECTRONIC_KEY_OFFSET_FORMAT_TABLE] != ELECTRONIC_KEY_FORMAT_TABLE)
+    {
+        return false;
+    }
+
+    segment->type = CipSegment_Type_LOGICAL_ELECTRONIC_KEY;
+    segment->size = ELECTRONIC_KEY_FORMAT_TABLE_SIZE;
+
+    return true;
+}
+
+static bool parse_segment_extended_symbol(const uint8_t* data,
+    size_t data_length,
+    CipSegment* segment)
+{
+    size_t symbol_size_bytes;
+    size_t segment_size_bytes;
+    symbol_size_bytes = data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET];
+
+    // calculate expected size
+    segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES + symbol_size_bytes;
+
+    // add padding
+    segment_size_bytes += segment_size_bytes % 2;
+
+    // Exit early, if we know we won't fit.
+    if (segment_size_bytes > data_length)
+    {
+        return false;
+    }
+
+    segment->type = CipSegment_Type_DATA_EXT_SYMBOL;
+    segment->data = &data[CIP_PATH_SEGMENT_MIN_SIZE_BYTES];
+    segment->data_size = symbol_size_bytes;
+    segment->size = segment_size_bytes;
+
+    return true;
+}
+
+static bool parse_segment_logical(const uint8_t* data,
+    size_t data_length,
+    CipSegment* segment)
+{
+    uint8_t segment_type = data[CIP_PATH_TYPE_OFFSET];
+
+    // parse particular logical type
+    bool valid = true;
+    #define CIP_PATH_LOGICAL_TYPE_MASK 0x1C
+    switch (segment_type & CIP_PATH_LOGICAL_TYPE_MASK)
+    {
+    case CIP_PATH_LOGICAL_CLASS:
+        segment->type = CipSegment_Type_LOGICAL_CLASS;
+        valid = parse_logical_address_format(data, data_length, false, segment);
+        break;
+    case CIP_PATH_LOGICAL_INSTANCE:
+        segment->type = CipSegment_Type_LOGICAL_INSTANCE;
+        valid = parse_logical_address_format(data, data_length, false, segment);
+        break;
+    case CIP_PATH_LOGICAL_MEMBER:
+        segment->type = CipSegment_Type_LOGICAL_MEMBER;
+        valid = parse_logical_address_format(data, data_length, false, segment);
+        break;
+    case CIP_PATH_LOGICAL_CONN_POINT:
+        segment->type = CipSegment_Type_LOGICAL_CONN_POINT;
+        valid = parse_logical_address_format(data, data_length, false, segment);
+        break;
+    case CIP_PATH_LOGICAL_ATTRIBUTE:
+        segment->type = CipSegment_Type_LOGICAL_ATTRIBUTE;
+        valid = parse_logical_address_format(data, data_length, false, segment);
+        break;
+    case CIP_PATH_LOGICAL_EXTENDED:
+        segment->type = CipSegment_Type_LOGICAL_EXTENDED;
+        valid = parse_logical_address_format(data, data_length, true, segment);
+        break;
+    case CIP_PATH_LOGICAL_SPECIAL:
+    {
+        // Logical Segment Electronic Key Logical Format.
+        #define CIP_PATH_SEGMENT_ELECTRONIC_KEY 0x34
+
+        if (segment_type == CIP_PATH_SEGMENT_ELECTRONIC_KEY)
+        {
+            valid = parse_segment_electronic_key(data,
+                data_length,
+                segment);
+        }
+        else
+        {
+            set_unknown_segment_type(data_length, segment);
+        }
+
+        break;
+    }
+    case CIP_PATH_LOGICAL_SERVICE_ID:
+    {
+        #define CIP_PATH_SEGMENT_SERVICE_ID 0x38
+        if (segment_type == CIP_PATH_SEGMENT_SERVICE_ID)
+        {
+            segment->type = CipSegment_Type_LOGICAL_SERVICE_ID;
+            valid = parse_logical_address_format(data, data_length, false, segment);
+        }
+        else
+        {
+            set_unknown_segment_type(data_length, segment);
+        }
+
+        break;
+    }
+    default:
+        // Can't happen.
+        set_unknown_segment_type(data_length, segment);
+        break;
+    }
+
+    return valid;
+}
+
+static bool parse_segment_network(const uint8_t* data,
+    size_t data_length,
+    CipSegment* segment)
+{
+    #define CIP_PATH_NETWORK_FORMAT_MASK 0xF0
+    #define CIP_PATH_NETWORK_ONE_BYTE 0x40
+
+    size_t segment_size_bytes = 0;
+
+    uint8_t segment_type = data[CIP_PATH_TYPE_OFFSET];
+    uint8_t network_segment_type = segment_type & CIP_PATH_NETWORK_FORMAT_MASK;
+    if (network_segment_type == CIP_PATH_NETWORK_ONE_BYTE)
+    {
+        segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES;
+    }
+    else  // Variable length network segment (0x50)
+    {
+        size_t data_size_bytes = data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET] * CIP_WORD_TO_BYTES;
+        segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES + data_size_bytes;
+        if (segment_size_bytes > data_length)
+        {
+            return false;
+        }
+    }
+
+    segment->type = CipSegment_Type_NETWORK;
+    segment->size = segment_size_bytes;
+
+    return true;
+}
+
+static bool parse_segment_port(const uint8_t* data,
+    size_t data_length,
+    CipSegment* segment)
+{
+    uint8_t segment_type = data[CIP_PATH_TYPE_OFFSET];
+
+    // set minimal expected segment size
+    size_t segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES;
+
+    // port segment extended port threshold
+    #define CIP_PATH_PORT_EXTENDED 0x0F
+
+    // calculate simple port (extended port is also a mask)
+    uint16_t port_number = segment_type & CIP_PATH_PORT_EXTENDED;
+
+    bool is_port_extended = port_number == CIP_PATH_PORT_EXTENDED;
+
+    #define CIP_PATH_PORT_EXTENDED_LINK_ADDRESS_MASK 0x10
+    bool is_long_address = (segment_type & CIP_PATH_PORT_EXTENDED_LINK_ADDRESS_MASK) != 0;
+
+    if (is_long_address)
+    {
+        // add length of address
+        segment_size_bytes += data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET];
+
+        // add padding
+        segment_size_bytes += segment_size_bytes % 2;
+    }
+
+    if (is_port_extended)
+    {
+        // add length of extended port
+        segment_size_bytes += sizeof(uint16_t);
+    }
+
+    // Exit early, if we know we won't fit.
+    if (segment_size_bytes > data_length)
+    {
+        return false;
+    }
+
+    if (is_port_extended)
+    {
+        size_t extended_port_offset = CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET;
+
+        if (is_long_address)
+        {
+            extended_port_offset += sizeof(uint8_t);
+        }
+
+        port_number = GetLEUint16(&data[extended_port_offset]);
+    }
+
+    segment->port_id = port_number;
+
+    if (!is_long_address)
+    {
+        size_t link_address_offset = CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET;
+
+        if (is_port_extended)
+        {
+            link_address_offset += sizeof(uint16_t);
+        }
+
+        segment->type = CipSegment_Type_PORT_LINK_ADDRESS;
+        segment->link_address = data[link_address_offset];
+    }
+    else
+    {
+        size_t link_address_offset = CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET + sizeof(uint8_t);
+
+        if (is_port_extended)
+        {
+            link_address_offset += sizeof(uint16_t);
+        }
+
+        segment->type = CipSegment_Type_PORT_LINK_ADDRESS_EXTENDED;
+        segment->data = &data[link_address_offset];
+        segment->data_size = data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET];
+    }
+
+    segment->size = segment_size_bytes;
+
+    return true;
+}
+
+static bool parse_segment_simple_data(const uint8_t* data,
+    size_t data_length,
+    CipSegment* segment)
+{
+    size_t data_size_bytes
+        = data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET] * CIP_WORD_TO_BYTES;
+
+    // calculate expected size
+    size_t segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES + data_size_bytes;
+
+    // Exit early, if we know we won't fit.
+    if (segment_size_bytes > data_length)
+    {
+        return false;
+    }
+
+    segment->type = CipSegment_Type_DATA_SIMPLE;
+    segment->data = &data[CIP_PATH_SEGMENT_MIN_SIZE_BYTES];
+    segment->data_size = data_size_bytes;
+    segment->size = segment_size_bytes;
+
+    return true;
+}
+
+static bool parse_segment_symbolic_extended_string(const uint8_t* data,
+    size_t data_length,
+    CipSegment* segment)
+{
+    #define EXTENDED_STRING_SIZE_MASK 0x1F
+    #define EXTENDED_STRING_FORMAT_MASK 0xE0
+
+    #define NUMERIC_SYMBOL_USINT 6
+    #define NUMERIC_SYMBOL_UINT 7
+    #define NUMERIC_SYMBOL_UDINT 8
+
+    #define DOUBLE_BYTE 2
+    #define TRIPLE_BYTE 3
+
+    uint8_t extended_format_byte = data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET];
+    uint8_t extended_format_size = extended_format_byte & EXTENDED_STRING_SIZE_MASK;
+
+    bool valid = true;
+    size_t data_size = 0;
+    switch (extended_format_byte & EXTENDED_STRING_FORMAT_MASK)
+    {
+    case EXTENDED_STRING_DOUBLE:
+        data_size = extended_format_size * DOUBLE_BYTE;
+        break;
+    case EXTENDED_STRING_TRIPLE:
+        data_size = extended_format_size * TRIPLE_BYTE;
+        break;
+    case EXTENDED_STRING_NUMERIC:
+        if (extended_format_size == NUMERIC_SYMBOL_USINT)
+        {
+            data_size = sizeof(uint8_t);
+        }
+        else if (extended_format_size == NUMERIC_SYMBOL_UINT)
+        {
+            data_size = sizeof(uint16_t);
+        }
+        else if (extended_format_size == NUMERIC_SYMBOL_UDINT)
+        {
+            data_size = sizeof(uint32_t);
+        }
+        else
+        {
+            valid = false;
+        }
+        break;
+    default:
+        valid = false;
+        break;
+    }
+
+    size_t segment_size_bytes = CIP_PATH_SEGMENT_MIN_SIZE_BYTES + data_size;
+
+    // Add padding.
+    segment_size_bytes += segment_size_bytes % 2;
+
+    if (data_length < segment_size_bytes)
+    {
+        return false;
+    }
+
+    segment->type = CipSegment_Type_SYMBOLIC;
+    segment->data = &data[CIP_PATH_SEGMENT_MIN_SIZE_BYTES];
+    segment->data_size = data_size;
+    segment->size = segment_size_bytes;
+
+    return valid;
+}
+
+static bool parse_segment_symbolic(const uint8_t* data,
+    size_t data_length,
+    CipSegment* segment)
+{
+    #define CIP_PATH_SYMBOLIC_SIZE_MASK 0x1F
+
+    bool valid = true;
+
+    uint8_t symbol_size_bytes = data[CIP_PATH_TYPE_OFFSET] & CIP_PATH_SYMBOLIC_SIZE_MASK;
+    if (symbol_size_bytes == 0)
+    {
+        valid = parse_segment_symbolic_extended_string(data, data_length, segment);
+    }
+    else  // Size 1 - 31.
+    {
+        size_t expected_segment_size = CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET + symbol_size_bytes;
+
+        // Add padding
+        expected_segment_size += expected_segment_size % 2;
+
+        if (expected_segment_size > data_length)
+        {
+            valid = false;
+        }
+        else
+        {
+            segment->type = CipSegment_Type_SYMBOLIC;
+            segment->data = &data[CIP_PATH_SEGMENT_PAYLOAD_SIZE_OFFSET];
+            segment->data_size = symbol_size_bytes;
+            segment->size = expected_segment_size;
+        }
+    }
+
+    return valid;
+}
+
+static bool parse_cip_segment(const uint8_t* data,
+    size_t data_length,
+    CipSegment* segment)
+{
+    bool valid = true;
+
+    #define CIP_PATH_SEGMENT_TYPE_MASK 0xE0
+
+    uint8_t segment_type = data[CIP_PATH_TYPE_OFFSET];
+    switch (segment_type & CIP_PATH_SEGMENT_TYPE_MASK)
+    {
+    case CIP_PATH_SEGMENT_PORT:
+        valid = parse_segment_port(
+            data,
+            data_length,
+            segment);
+        break;
+    case CIP_PATH_SEGMENT_LOGICAL:
+        valid = parse_segment_logical(
+            data,
+            data_length,
+            segment);
+        break;
+    case CIP_PATH_SEGMENT_DATA:
+    {
+        #define CIP_PATH_SEGMENT_SIMPLE_DATA 0x80
+        #define CIP_PATH_SEGMENT_EXT_SYMBOL 0x91
+
+        if (segment_type == CIP_PATH_SEGMENT_EXT_SYMBOL)
+        {
+            valid = parse_segment_extended_symbol(
+                data,
+                data_length,
+                segment);
+        }
+        else if (segment_type == CIP_PATH_SEGMENT_SIMPLE_DATA)
+        {
+            valid = parse_segment_simple_data(
+                data,
+                data_length,
+                segment);
+        }
+        else
+        {
+            set_unknown_segment_type(data_length, segment);
+        }
+        break;
+    }
+    case CIP_PATH_SEGMENT_NETWORK:
+        valid = parse_segment_network(data, data_length, segment);
+        break;
+    case CIP_PATH_SEGMENT_SYMBOLIC:
+        valid = parse_segment_symbolic(data, data_length, segment);
+        break;
+    default:
+        set_unknown_segment_type(data_length, segment);
+        break;
+    }
+
+    return valid;
+}
+
+static bool parse_cip_segments(const uint8_t* data,
+    size_t data_length,
+    CipPath* path)
+{
+    bool valid = true;
+
+    // Parse all CIP segments.
+    while (data_length > 0)
+    {
+        CipSegment segment;
+        /* Check that there is enough data to start. */
+        if (data_length < CIP_PATH_SEGMENT_MIN_SIZE_BYTES)
+        {
+            valid = false;
+            break;
+        }
+
+        memset(&segment, 0, sizeof(segment));
+        if (!parse_cip_segment(data, data_length, &segment))
+        {
+            valid = false;
+            break;
+        }
+
+        // Save off key data in this segment for later use.
+        if (segment.type == CipSegment_Type_LOGICAL_CLASS)
+        {
+            path->has_class_id = true;
+            path->class_id = segment.logical_value;
+
+            path->primary_segment_type = CipSegment_Type_LOGICAL_CLASS;
+        }
+        else if (segment.type == CipSegment_Type_DATA_EXT_SYMBOL)
+        {
+            path->primary_segment_type = CipSegment_Type_DATA_EXT_SYMBOL;
+        }
+        else if (segment.type == CipSegment_Type_LOGICAL_INSTANCE)
+        {
+            path->has_instance_id = true;
+            path->instance_id = segment.logical_value;
+        }
+        else if (segment.type == CipSegment_Type_LOGICAL_ATTRIBUTE)
+        {
+            path->has_attribute_id = true;
+            path->attribute_id = segment.logical_value;
+        }
+        else if (segment.type == CipSegment_Type_UNKNOWN)
+        {
+            path->has_unknown_segment = true;
+        }
+
+        // Move to the next segment.
+        data_length -= segment.size;
+        data += segment.size;
+    }
+
+    return valid;
+}
+
+static bool parse_cip_epath(const uint8_t* data,
+    size_t data_length,
+    bool path_contains_reserved_byte,
+    CipPath* path)
+{
+    #define PATH_SIZE_FIELD_BYTES 1
+    #define PATH_SIZE_OFFSET 0
+    size_t path_size_bytes;
+    size_t path_header_size;
+
+    // There is a size byte and optionally a padding byte before the actual path data.
+    path_header_size = PATH_SIZE_FIELD_BYTES;
+    if (path_contains_reserved_byte)
+    {
+        path_header_size++;
+    }
+
+    // Validate/Get the Path Size.
+    if (data_length < path_header_size)
+    {
+        return false;
+    }
+
+    path_size_bytes = data[PATH_SIZE_OFFSET] * CIP_WORD_TO_BYTES;
+    if (data_length - path_header_size < path_size_bytes)
+    {
+        return false;
+    }
+
+    if (!parse_cip_segments(data + path_header_size, path_size_bytes, path))
+    {
+        return false;
+    }
+
+    path->full_path_size = path_header_size + path_size_bytes;
+    path->decoded = true;
+
+    return true;
+}
+
+// Parse the logical addressing format which is common to all logical segment
+//  types, except Special and Service ID.
+static bool parse_logical_address_format(const uint8_t* data,
+    size_t data_length,
+    bool logical_extended,
+    CipSegment* segment)
+{
+    #define LOGICAL_8_BIT_SIZE 2
+    #define LOGICAL_8_BIT_EXTENDED_SIZE 4
+    #define LOGICAL_16_BIT_SIZE 4
+    #define LOGICAL_32_BIT_SIZE 6
+    #define LOGICAL_DEFAULT_DATA_OFFSET 2
+    #define LOGICAL_8_BIT_DATA_OFFSET 1
+
+    uint32_t logical_value;
+    bool valid = true;
+
+    uint8_t segment_type = data[CIP_PATH_TYPE_OFFSET];
+
+    // Get the expected segment size and data offset.
+    size_t segment_size = 0;
+    size_t data_offset = LOGICAL_DEFAULT_DATA_OFFSET;
+    switch (segment_type & CIP_PATH_LOGICAL_FORMAT_MASK)
+    {
+    case CIP_PATH_LOGICAL_32_BIT:
+        segment_size = LOGICAL_32_BIT_SIZE;
+        break;
+    case CIP_PATH_LOGICAL_16_BIT:
+        segment_size = LOGICAL_16_BIT_SIZE;
+        break;
+    case CIP_PATH_LOGICAL_8_BIT:
+        if (logical_extended)
+        {
+            segment_size = LOGICAL_8_BIT_EXTENDED_SIZE;
+        }
+        else
+        {
+            segment_size = LOGICAL_8_BIT_SIZE;
+            data_offset = LOGICAL_8_BIT_DATA_OFFSET;
+        }
+        break;
+    default:
+        valid = false;
+        break;
+    }
+
+    // Exit early, if we know we won't fit.
+    if (segment_size > data_length)
+    {
+        return false;
+    }
+
+    // Get the logical value.
+    logical_value = 0;
+    switch (segment_type & CIP_PATH_LOGICAL_FORMAT_MASK)
+    {
+    case CIP_PATH_LOGICAL_32_BIT:
+        logical_value = GetLEUint32(&data[data_offset]);
+        break;
+    case CIP_PATH_LOGICAL_16_BIT:
+        logical_value = GetLEUint16(&data[data_offset]);
+        break;
+    case CIP_PATH_LOGICAL_8_BIT:
+        logical_value = data[data_offset];
+        break;
+    default:
+        valid = false;
+        break;
+    }
+
+    segment->logical_value = logical_value;
+    segment->size = segment_size;
+
+    return valid;
+}
+
+static bool parse_cip_status(const uint8_t* data,
+    size_t data_length,
+    CipStatus* status)
+{
+    if (data_length < CIP_STATUS_MIN_SIZE)
+    {
+        return false;
+    }
+
+    #define CIP_STATUS_OFFSET_GEN_STATUS 0
+    status->general_status = data[CIP_STATUS_OFFSET_GEN_STATUS];
+
+    #define CIP_STATUS_OFFSET_EXT_STATUS_SIZE 1
+    status->extended_status_size = data[CIP_STATUS_OFFSET_EXT_STATUS_SIZE] * CIP_WORD_TO_BYTES;
+
+    // extended status size does not fit the response
+    if (data_length < (CIP_STATUS_MIN_SIZE + status->extended_status_size))
+    {
+        return false;
+    }
+
+    return true;
+}
+
+/// Forward Open/Close parsing.
+static bool parse_forward_open_request(const uint8_t* data,
+    size_t data_length,
+    bool large_forward_open,
+    CipForwardOpenRequest* forward_open_request,
+    CipRequest* cip_request)
+{
+    // This includes all data up to, but not including, the Connection Path Size.
+    #define CIP_FORWARD_OPEN_PREFIX_SIZE 35
+    #define CIP_LARGE_FORWARD_OPEN_PREFIX_SIZE 39
+    #define FWD_OPEN_OFFSET_CONN_SIGNATURE 10
+    #define DEFAULT_CONNECTION_TIMEOUT (10 * USEC_PER_SEC)
+    size_t forward_open_prefix_size;
+
+    // Size of the common connection-related parameters fields. This includes
+    //  the RPI and the Network Connection Parameters.
+    size_t connection_parameters_size;
+    size_t offset_to_parameters;
+    size_t offset_transport_type_trigger;
+    size_t offset_connection_path_size;
+    uint8_t connection_timeout_multiplier;
+    const bool NO_PATH_RESERVED_BYTE = false;
+
+    if (large_forward_open)
+    {
+        connection_parameters_size = sizeof(uint32_t) + sizeof(uint32_t);
+        forward_open_prefix_size = CIP_LARGE_FORWARD_OPEN_PREFIX_SIZE;
+    }
+    else
+    {
+        connection_parameters_size = sizeof(uint32_t) + sizeof(uint16_t);
+        forward_open_prefix_size = CIP_FORWARD_OPEN_PREFIX_SIZE;
+    }
+
+    // Ensure that there is enough data for the common part of a Forward Open.
+    if (data_length < forward_open_prefix_size)
+    {
+        return false;
+    }
+    data_length -= forward_open_prefix_size;
+
+    #define FWD_OPEN_OFFSET_TIMEOUT_MULTIPLIER 18
+    #define FWD_OPEN_OFFSET_OT_RPI 22
+    offset_to_parameters = FWD_OPEN_OFFSET_OT_RPI + connection_parameters_size;
+    offset_transport_type_trigger = offset_to_parameters + connection_parameters_size;
+    offset_connection_path_size = offset_transport_type_trigger + 1;
+
+    forward_open_request->timeout_ms = get_unconnected_timeout(data);
+    parse_connection_signature(&data[FWD_OPEN_OFFSET_CONN_SIGNATURE],
+        &forward_open_request->connection_signature);
+    parse_connection_parameters(&data[FWD_OPEN_OFFSET_OT_RPI],
+        large_forward_open,
+        &forward_open_request->ot_parameters);
+    parse_connection_parameters(&data[offset_to_parameters],
+        large_forward_open,
+        &forward_open_request->to_parameters);
+
+    // Get the overall connection timeouts.
+    connection_timeout_multiplier = data[FWD_OPEN_OFFSET_TIMEOUT_MULTIPLIER];
+    #define MULTIPLIER_DEFAULT 2
+    #define MAX_TIMEOUT_MULTIPLIER 7
+    if (connection_timeout_multiplier <= MAX_TIMEOUT_MULTIPLIER)
+    {
+        uint16_t actual_multiplier = 1 << (connection_timeout_multiplier + MULTIPLIER_DEFAULT);
+        forward_open_request->ot_connection_timeout_us
+            = forward_open_request->ot_parameters.rpi * actual_multiplier;
+        forward_open_request->to_connection_timeout_us
+            = forward_open_request->to_parameters.rpi * actual_multiplier;
+    }
+    else
+    {
+        cip_request->cip_req_invalid_nonfatal |= CIP_REQ_INVALID_TIMEOUT_MULTIPLIER;
+        forward_open_request->ot_connection_timeout_us = DEFAULT_CONNECTION_TIMEOUT;
+        forward_open_request->to_connection_timeout_us = DEFAULT_CONNECTION_TIMEOUT;
+    }
+
+    if (forward_open_request->ot_parameters.is_null_connection
+        && forward_open_request->to_parameters.is_null_connection)
+    {
+        forward_open_request->is_null_forward_open = true;
+    }
+
+    uint8_t transport_type_trigger = data[offset_transport_type_trigger];
+    forward_open_request->transport_class = transport_type_trigger & TRANSPORT_CLASS_MASK;
+
+    // Parse out the Connection Path. This is a variable length section.
+    bool valid = parse_cip_epath(&data[offset_connection_path_size],
+        data_length,
+        NO_PATH_RESERVED_BYTE,
+        &forward_open_request->connection_path);
+
+    return valid;
+}
+
+static bool parse_forward_open_response_success(const uint8_t* data,
+    size_t data_length,
+    CipForwardOpenResponse* forward_open_response)
+{
+    #define FWD_OPEN_OFFSET_CON_SIGNATURE 8
+    #define CIP_FORWARD_OPEN_RESPONSE_PREFIX_SIZE 26
+    if (data_length < CIP_FORWARD_OPEN_RESPONSE_PREFIX_SIZE)
+    {
+        return false;
+    }
+
+    #define FWD_OPEN_OFFSET_OT_CONNECTION 0
+    #define FWD_OPEN_OFFSET_TO_CONNECTION 4
+    #define FWD_OPEN_OFFSET_REPLY_SIZE 24
+
+    forward_open_response->connection_pair.ot_connection_id
+        = GetLEUint32(&data[FWD_OPEN_OFFSET_OT_CONNECTION]);
+    forward_open_response->connection_pair.to_connection_id
+        = GetLEUint32(&data[FWD_OPEN_OFFSET_TO_CONNECTION]);
+    parse_connection_signature(&data[FWD_OPEN_OFFSET_CON_SIGNATURE],
+        &forward_open_response->connection_signature);
+    forward_open_response->application_reply_size
+        = data[FWD_OPEN_OFFSET_REPLY_SIZE] * CIP_WORD_TO_BYTES;
+
+    data_length -= CIP_FORWARD_OPEN_RESPONSE_PREFIX_SIZE;
+    if (data_length < forward_open_response->application_reply_size)
+    {
+        return false;
+    }
+
+    forward_open_response->success = true;
+
+    return true;
+}
+
+static bool parse_forward_open_response_fail(const uint8_t* data,
+    size_t data_length,
+    CipForwardOpenResponse* forward_open_response)
+{
+    #define CIP_FORWARD_OPEN_RESPONSE_FAIL_SIZE 10
+    if (data_length < CIP_FORWARD_OPEN_RESPONSE_FAIL_SIZE)
+    {
+        return false;
+    }
+
+    parse_connection_signature(data, &forward_open_response->connection_signature);
+
+    forward_open_response->success = false;
+
+    return true;
+}
+
+static bool parse_forward_open_response(const uint8_t* data,
+    size_t data_length,
+    uint8_t response_status,
+    CipForwardOpenResponse* forward_open_response)
+{
+    bool valid = true;
+
+    // Forward Open Success and Failure cases have different formats.
+    if (response_status == CIP_STATUS_SUCCESS)
+    {
+        valid = parse_forward_open_response_success(data, data_length, forward_open_response);
+    }
+    else
+    {
+        valid = parse_forward_open_response_fail(data, data_length, forward_open_response);
+    }
+
+    return valid;
+}
+
+static bool parse_forward_close_request(const uint8_t* data,
+    size_t data_length,
+    CipForwardCloseRequest* forward_close_request)
+{
+    bool valid;
+    const bool PATH_RESERVED_BYTE = true;
+    #define CIP_FORWARD_CLOSE_PREFIX_SIZE 10
+    if (data_length < CIP_FORWARD_CLOSE_PREFIX_SIZE)
+    {
+        return false;
+    }
+
+    #define FWD_CLOSE_OFFSET_CONNECTION_SIGNATURE 2
+
+    forward_close_request->timeout_ms = get_unconnected_timeout(data);
+    parse_connection_signature(&data[FWD_CLOSE_OFFSET_CONNECTION_SIGNATURE],
+        &forward_close_request->connection_signature);
+
+    // Parse out the Connection Path. This is a variable length section.
+    valid = parse_cip_epath(data + CIP_FORWARD_CLOSE_PREFIX_SIZE,
+        data_length - CIP_FORWARD_CLOSE_PREFIX_SIZE,
+        PATH_RESERVED_BYTE,
+        &forward_close_request->connection_path);
+
+    return valid;
+}
+
+// Returns size of the CIP Request Header that was parsed.
+static bool parse_cip_request_header(const uint8_t* data,
+    size_t data_length,
+    size_t* header_size,
+    CipRequest* cip_request)
+{
+    bool valid;
+    CipPath* path;
+    const bool NO_PATH_RESERVED_BYTE = false;
+    #define CIP_SERVICE_SIZE 1
+    if (data_length < CIP_SERVICE_SIZE)
+    {
+        return false;
+    }
+
+    #define CIP_SERVICE_OFFSET 0
+    cip_request->service = data[CIP_SERVICE_OFFSET];
+
+    // Reset all path information.
+    memset(&cip_request->request_path, 0, sizeof(cip_request->request_path));
+    path = &cip_request->request_path;
+
+    valid = parse_cip_epath(data + CIP_SERVICE_SIZE,
+        data_length - CIP_SERVICE_SIZE,
+        NO_PATH_RESERVED_BYTE,
+        path);
+    if (!valid)
+    {
+        return false;
+    }
+
+    if (path->has_unknown_segment)
+    {
+        cip_request->cip_req_invalid_nonfatal |= CIP_REQ_INVALID_UNKNOWN_SEGMENT;
+    }
+
+    *header_size = CIP_SERVICE_SIZE + path->full_path_size;
+
+    return true;
+}
+
+static size_t cip_status_size(const CipStatus* status)
+{
+    return CIP_STATUS_MIN_SIZE + status->extended_status_size;
+}
+
+static bool parse_multiple_service_packet(const uint8_t* data,
+    size_t data_length,
+    CipRequest* cip_request,
+    CipGlobalSessionData* global_data)
+{
+    // Save the original data length for use in handling the offsets of embedded services.
+    uint16_t number_services;
+    size_t total_offset_size;
+    size_t data_offset;
+    size_t first_offset;
+    bool valid;
+    uint16_t i;
+    size_t original_data_length = data_length;
+
+    // Check that the number of services will fit.
+    #define CIP_MSP_NUMBER_SERVICES_FIELD_SIZE 2
+    if (data_length < CIP_MSP_NUMBER_SERVICES_FIELD_SIZE)
+    {
+        return false;
+    }
+
+    #define CIP_MSP_OFFSET_NUMBER_SERVICES 0
+    number_services = data[CIP_MSP_OFFSET_NUMBER_SERVICES];
+    data_length -= CIP_MSP_NUMBER_SERVICES_FIELD_SIZE;
+
+    // Check that the offsets will fit.
+    #define CIP_MSP_OFFSET_FIELD_SIZE 2
+    total_offset_size = number_services * CIP_MSP_OFFSET_FIELD_SIZE;
+    if (data_length < total_offset_size)
+    {
+        return false;
+    }
+
+    // Length of actual data left after the offsets.
+    data_length -= total_offset_size;
+
+    // Check that offset data starts after the last offset.
+    data_offset = CIP_MSP_NUMBER_SERVICES_FIELD_SIZE + total_offset_size;
+    first_offset = GetLEUint16(&data[CIP_MSP_NUMBER_SERVICES_FIELD_SIZE]);
+    if (first_offset < data_offset)
+    {
+        return false;
+    }
+
+    valid = true;
+
+    // Process each embedded service.
+    for (i = 1; i <= number_services; ++i)
+    {
+        size_t msp_length;
+        CipRequest embedded_request;
+        CipEventData cip_event_data;
+        CipEvent cip_event(global_data->snort_packet, &cip_event_data);
+
+        /* This if the offset from the Number of Services field, to the Offset field. */
+        uint16_t buffer_offset = i * CIP_MSP_OFFSET_FIELD_SIZE;
+
+        /* This if the offset from the Number of Services field to the data. */
+        size_t msp_offset = GetLEUint16(&data[buffer_offset]);
+
+        /* There is no end offset specified, so the next offset needs checked
+           to find the length of the current service. For the last packet,
+           this needs to use the total length of the Multiple Service Packet. */
+        size_t msp_offset_end = 0;
+        if (i == number_services)
+        {
+            msp_offset_end = original_data_length;
+        }
+        else
+        {
+            uint16_t next_buffer_offset = buffer_offset + CIP_MSP_OFFSET_FIELD_SIZE;
+            msp_offset_end = GetLEUint16(&data[next_buffer_offset]);
+        }
+
+        // Check that offsets are increasing.
+        if (msp_offset >= msp_offset_end)
+        {
+            valid = false;
+            break;
+        }
+
+        // Check embedded length against the data size left.
+        msp_length = msp_offset_end - msp_offset;
+        if (data_length < msp_length)
+        {
+            valid = false;
+            break;
+        }
+
+        data_length -= msp_length;
+
+        memset(&embedded_request, 0, sizeof(embedded_request));
+        if (!parse_message_router_request(data + msp_offset,
+            msp_length,
+            &embedded_request,
+            global_data))
+        {
+            valid = false;
+            break;
+        }
+
+        // Store embedded packet errors in the parent request.
+        cip_request->cip_req_invalid_nonfatal |= embedded_request.cip_req_invalid_nonfatal;
+
+        // Publish embedded CIP data to appid.
+        memset(&cip_event_data, 0, sizeof(cip_event_data));
+
+        pack_cip_request_event(&embedded_request, &cip_event_data);
+
+        DataBus::publish(CIP_EVENT_TYPE_CIP_DATA_KEY, cip_event, global_data->snort_packet->flow);
+    }
+
+    return valid;
+}
+
+static bool parse_unconnected_send_request(const uint8_t* data,
+    size_t data_length,
+    CipRequest* cip_request,
+    CipGlobalSessionData* global_data)
+{
+    bool valid;
+    uint16_t message_request_size;
+    const bool PATH_RESERVED_BYTE = true;
+    // This includes: Timeout data, embedded message size.
+    #define UNCONNECTED_SEND_HEADER_SIZE 4
+    if (data_length < UNCONNECTED_SEND_HEADER_SIZE)
+    {
+        return false;
+    }
+
+    cip_request->timeout_ms = get_unconnected_timeout(data);
+    cip_request->has_timeout = true;
+
+    #define UNCONNECTED_SEND_OFFSET_MESSAGE_SIZE 2
+    message_request_size = GetLEUint16(&data[UNCONNECTED_SEND_OFFSET_MESSAGE_SIZE]);
+
+    data += UNCONNECTED_SEND_HEADER_SIZE;
+    data_length -= UNCONNECTED_SEND_HEADER_SIZE;
+
+    // Verify that expected length of embedded request will fit in actual data.
+    if (message_request_size > data_length)
+    {
+        return false;
+    }
+
+    if (!parse_message_router_request(data, message_request_size, cip_request, global_data))
+    {
+        return false;
+    }
+
+    // Parse the Route Path.
+    valid = parse_cip_epath(data + message_request_size,
+        data_length - message_request_size,
+        PATH_RESERVED_BYTE,
+        &cip_request->route_path);
+
+    return valid;
+}
+
+static bool parse_cip_command_specific_data_request(const uint8_t* data,
+    size_t data_length,
+    CipRequest* cip_request,
+    CipGlobalSessionData* global_data)
+{
+    const CipProtoConf* config;
+    /* If the request path doesn't have a Class ID, then we don't know how to
+       parse the response.*/
+    if (!cip_request->request_path.has_class_id)
+    {
+        cip_request->request_type = CipRequestTypeOther;
+        return true;
+    }
+
+    bool valid = true;
+
+    uint8_t service = cip_request->service;
+    uint32_t class_id = cip_request->request_path.class_id;
+    if (service == SERVICE_MULTIPLE_SERVICE_PACKET)
+    {
+        valid = parse_multiple_service_packet(data, data_length, cip_request, global_data);
+        cip_request->request_type = CipRequestTypeMultipleServiceRequest;
+    }
+    else if (class_id == CONNECTION_MANAGER_CLASS_ID
+        && service == CONNECTION_MANAGER_UNCONNECTED_SEND)
+    {
+        valid = parse_unconnected_send_request(data,
+            data_length,
+            cip_request,
+            global_data);
+        cip_request->request_type = CipRequestTypeUnconnectedSend;
+    }
+    else if (class_id == CONNECTION_MANAGER_CLASS_ID
+        && (service == CONNECTION_MANAGER_FORWARD_OPEN
+        || service == CONNECTION_MANAGER_LARGE_FORWARD_OPEN))
+    {
+        CipForwardOpenRequest forward_open_request;
+        memset(&forward_open_request, 0, sizeof(forward_open_request));
+
+        bool large_forward_open = (service == CONNECTION_MANAGER_LARGE_FORWARD_OPEN);
+        valid = parse_forward_open_request(data,
+            data_length,
+            large_forward_open,
+            &forward_open_request,
+            cip_request);
+        forward_open_request.timestamp = global_data->snort_packet->pkth->ts;
+
+        if (valid)
+        {
+            // Only store connection information for Class 3, Non-Null connections.
+            if (!forward_open_request.is_null_forward_open
+                && forward_open_request.transport_class == 3)
+            {
+                if (!cip_add_connection_to_pending(&global_data->connection_list,
+                    &forward_open_request))
+                {
+                    // Error if the connection couldn't be added to the list.
+                    cip_request->cip_req_invalid_nonfatal |= CIP_REQ_INVALID_CONNECTION_ADD_FAILED;
+                }
+            }
+
+            cip_request->is_forward_open_request = true;
+            cip_request->connection_path_class_id = forward_open_request.connection_path.class_id;
+            cip_request->timeout_ms = forward_open_request.timeout_ms;
+            cip_request->has_timeout = true;
+        }
+
+        cip_request->request_type = CipRequestTypeForwardOpen;
+    }
+    else if (class_id == CONNECTION_MANAGER_CLASS_ID
+        && service == CONNECTION_MANAGER_FORWARD_CLOSE)
+    {
+        CipForwardCloseRequest forward_close_request;
+        memset(&forward_close_request, 0, sizeof(forward_close_request));
+
+        valid = parse_forward_close_request(data,
+            data_length,
+            &forward_close_request);
+
+        if (valid)
+        {
+            const bool connection_established = true;
+            cip_remove_connection(&global_data->connection_list,
+                &forward_close_request.connection_signature,
+                connection_established);
+
+            cip_request->timeout_ms = forward_close_request.timeout_ms;
+            cip_request->has_timeout = true;
+        }
+
+        cip_request->request_type = CipRequestTypeForwardClose;
+    }
+    else
+    {
+        // This is a regular CIP request. No need to parse data.
+        cip_request->request_type = CipRequestTypeOther;
+    }
+
+    // Parse any embedded CIP packet that is configured.
+    config = global_data->config;
+    if (config->embedded_cip_enabled
+        && class_id == config->embedded_cip_class_id
+        && service == config->embedded_cip_service_id)
+    {
+        valid = parse_message_router_request(data, data_length, cip_request, global_data);
+    }
+
+    return valid;
+}
+
+static bool parse_cip_command_specific_data_response(const CipStatus* status,
+    const uint8_t* data,
+    size_t data_length,
+    CipRequestType request_type,
+    CipGlobalSessionData* global_data)
+{
+    bool valid = true;
+
+    if (request_type == CipRequestTypeForwardOpen)
+    {
+        CipForwardOpenResponse forward_open_response;
+        memset(&forward_open_response, 0, sizeof(forward_open_response));
+
+        valid = parse_forward_open_response(data,
+            data_length,
+            status->general_status,
+            &forward_open_response);
+        forward_open_response.timestamp = global_data->snort_packet->pkth->ts;
+
+        if (forward_open_response.success)
+        {
+            cip_add_connection_to_active(&global_data->connection_list, &forward_open_response);
+        }
+        else
+        {
+            const bool connection_established = false;
+            cip_remove_connection(&global_data->connection_list,
+                &forward_open_response.connection_signature,
+                connection_established);
+        }
+    }
+
+    return valid;
+}
+
+
+static bool parse_message_router_request(const uint8_t* data,
+    size_t data_length,
+    CipRequest* cip_request,
+    CipGlobalSessionData* global_data)
+{
+    size_t header_size = 0;
+    if (!parse_cip_request_header(data,
+        data_length,
+        &header_size,
+        cip_request))
+    {
+        return false;
+    }
+
+    cip_request->cip_data = data + header_size;
+    cip_request->cip_data_size = data_length - header_size;
+
+    bool valid = parse_cip_command_specific_data_request(data + header_size,
+        data_length - header_size,
+        cip_request,
+        global_data);
+
+    return valid;
+}
+
+static bool parse_message_router_response(const uint8_t* data,
+    size_t data_length,
+    CipRequestType request_type,
+    CipResponse* cip_response,
+    CipGlobalSessionData* global_data)
+{
+    bool valid;
+    size_t response_header_size;
+    size_t status_size;
+    #define MESSAGE_ROUTER_RESPONSE_MIN_SIZE 4
+    if (data_length < MESSAGE_ROUTER_RESPONSE_MIN_SIZE)
+    {
+        return false;
+    }
+
+    #define CIP_SERVICE_OFFSET 0
+    #define CIP_STATUS_OFFSET 2
+
+    cip_response->service = data[CIP_SERVICE_OFFSET] & ~MESSAGE_ROUTER_RESPONSE_MASK;
+
+    if (!parse_cip_status(data + CIP_STATUS_OFFSET,
+        data_length - CIP_STATUS_OFFSET,
+        &cip_response->status))
+    {
+        return false;
+    }
+
+    status_size = cip_status_size(&cip_response->status);
+
+    // This includes: Service, reserved field, total status data.
+    response_header_size = CIP_STATUS_OFFSET + status_size;
+
+    valid = true;
+    // Don't attempt to decode the command specific response if there wasn't a
+    //  match to an existing request.
+    if (request_type != CipRequestTypeNoMatchFound)
+    {
+        valid = parse_cip_command_specific_data_response(&cip_response->status,
+            data + response_header_size,
+            data_length - response_header_size,
+            request_type,
+            global_data);
+    }
+
+    return valid;
+}
+
+// Returns true if the serviceId was a request service.
+static bool is_service_request(uint8_t service_id)
+{
+    return (service_id & MESSAGE_ROUTER_RESPONSE_MASK) == 0;
+}
+
+static bool parse_message_router(const uint8_t* data,
+    size_t data_length,
+    CipCurrentData* current_data,
+    CipGlobalSessionData* global_data)
+{
+    bool valid = true;
+
+    CipMessage* cip_msg = &current_data->cip_msg;
+
+    cip_msg->is_cip_request = is_service_request(*data);
+    if (cip_msg->is_cip_request)
+    {
+        cip_msg->request.request_type = CipRequestTypeOther;
+        valid = parse_message_router_request(data,
+            data_length,
+            &cip_msg->request,
+            global_data);
+
+        cip_request_add(&global_data->unconnected_list,
+            &current_data->enip_data,
+            &cip_msg->request,
+            &global_data->snort_packet->pkth->ts);
+    }
+    else
+    {
+        CipRequestType request_type = CipRequestTypeNoMatchFound;
+        cip_request_remove(&global_data->unconnected_list,
+            &current_data->enip_data,
+            &request_type);
+
+        valid = parse_message_router_response(data,
+            data_length,
+            request_type,
+            &cip_msg->response,
+            global_data);
+    }
+
+    return valid;
+}
+
+// Returns true if this data contains valid CIP Explicit data. The data must already:
+//  1. Be ENIP_COMMAND_SEND_UNIT_DATA or ENIP_COMMAND_SEND_RR_DATA
+//  2. Have the required CPF items for that ENIP command.
+static bool parse_cip_explicit_data(CipCurrentData* current_data, CipGlobalSessionData* global_data)
+{
+    // Assume that all data values/length inside the EnipCpf are valid.
+    const EnipCpf* enip_cpf = &current_data->enip_data.enip_cpf;
+    const EnipCpfItem* cpf_item = &enip_cpf->item_list[CPF_DATA_ITEM_SLOT];
+
+    // Get the offset of the CIP Message Router data.
+    size_t cpf_data_offset = 0;
+    if (cpf_item->type == CPF_CONNECTED_DATA_ITEM_ID)
+    {
+        // For CIP Class 3 data, Connected Data contains: Sequence Count, then CIP Data.
+        const size_t CPF_CONNECTED_DATA_SEQUENCE_COUNT_SIZE = sizeof(uint16_t);
+        cpf_data_offset = CPF_CONNECTED_DATA_SEQUENCE_COUNT_SIZE;
+    }
+    else  // CPF_UNCONNECTED_DATA_ITEM_ID
+    {
+        cpf_data_offset = 0;
+    }
+
+    bool valid = false;
+    if (cpf_item->length > cpf_data_offset)
+    {
+        const uint8_t* message_router_data = cpf_item->data + cpf_data_offset;
+        size_t message_router_data_length = cpf_item->length - cpf_data_offset;
+
+        valid = parse_message_router(message_router_data,
+            message_router_data_length,
+            current_data,
+            global_data);
+    }
+
+    return valid;
+}
+
+// Used for parsing SendRRData and SendUnitData Command Specific Data.
+static bool parse_enip_command_data(const uint8_t* data,
+    size_t data_length,
+    CipCurrentData* current_data,
+    CipGlobalSessionData* global_data)
+{
+    uint32_t interface_handle;
+
+    // This should always contain: Interface Handle, Timeout.
+#define ENIP_COMMAND_HEADER_SIZE 6
+    if (data_length < ENIP_COMMAND_HEADER_SIZE)
+    {
+        return false;
+    }
+
+    // Interface Handle
+ #define ENIP_OFFSET_INTERFACE_HANDLE 0
+    interface_handle = GetLEUint32(&data[ENIP_OFFSET_INTERFACE_HANDLE]);
+
+#define CIP_INTERFACE_HANDLE 0
+    if (interface_handle != CIP_INTERFACE_HANDLE)
+    {
+        current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_INTERFACE_HANDLE;
+    }
+
+    // Parse the Encapsulated Data as Common Packet Format.
+    current_data->enip_data.cpf_decoded = parse_common_packet_format(data + ENIP_COMMAND_HEADER_SIZE,
+        data_length - ENIP_COMMAND_HEADER_SIZE,
+        &current_data->enip_data.enip_cpf,
+        current_data);
+    if (!current_data->enip_data.cpf_decoded)
+    {
+        return false;
+    }
+
+    // Exit early if CIP Explicit Data cannot be processed.
+    if (!current_data->enip_data.required_cpf_items_present)
+    {
+        return true;
+    }
+
+    current_data->cip_message_type = get_cip_message_type(current_data, global_data);
+
+    bool valid = true;
+    if (current_data->cip_message_type == CipMessageTypeExplicit)
+    {
+        valid = parse_cip_explicit_data(current_data, global_data);
+    }
+
+    return valid;
+}
+
+bool parse_enip_layer(const uint8_t* data,
+    size_t data_length,
+    bool is_TCP,
+    CipCurrentData* current_data,
+    CipGlobalSessionData* global_data)
+{
+    const EnipHeader* enip_header;
+    current_data->enip_data.enip_decoded = parse_enip_header(data,
+        data_length,
+        &current_data->enip_data);
+    if (!current_data->enip_data.enip_decoded)
+    {
+        return false;
+    }
+
+    // Command Specific Data
+    data += ENIP_HEADER_SIZE;
+    data_length -= ENIP_HEADER_SIZE;
+
+    // Verify that actual data matches data length field.
+    enip_header  = &current_data->enip_data.enip_header;
+    if (data_length < enip_header->length)
+    {
+        return false;
+    }
+
+    if (enip_command_tcp_only(enip_header->command) && !is_TCP)
+    {
+        // Flag as an error and exit early because there would be no way to tie this data
+        //  to a particular TCP session.
+        current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_ENIP_TCP_ONLY;
+
+        return true;
+    }
+
+    if (enip_header->status != ENIP_STATUS_SUCCESS)
+    {
+        if (current_data->direction == CIP_FROM_CLIENT)
+        {
+            current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_STATUS;
+        }
+        else if (current_data->direction == CIP_FROM_SERVER)
+        {
+            // Remove any outstanding request.
+            CipRequestType request_type = CipRequestTypeNoMatchFound;
+            cip_request_remove(&global_data->unconnected_list,
+                &current_data->enip_data,
+                &request_type);
+        }
+
+        // No more processing after a non-success status.
+        return true;
+    }
+
+    bool valid = true;
+
+    switch (enip_header->command)
+    {
+    case ENIP_COMMAND_REGISTER_SESSION:
+    {
+        if (data_length < REGISTER_SESSION_DATA_SIZE)
+        {
+            valid = false;
+            break;
+        }
+
+        // Check that there is no active ENIP session.
+        if (current_data->direction == CIP_FROM_CLIENT)
+        {
+            if (global_data->enip_session.active)
+            {
+                current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_DUPLICATE_SESSION;
+            }
+        }
+
+        // Add ENIP session to the current TCP session.
+        if (current_data->direction == CIP_FROM_SERVER)
+        {
+            if (!enip_session_add(&global_data->enip_session,
+                enip_header->session_handle))
+            {
+                current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_DUPLICATE_SESSION;
+            }
+        }
+    }
+    break;
+
+    case ENIP_COMMAND_SEND_RR_DATA:
+    case ENIP_COMMAND_SEND_UNIT_DATA:
+        valid = parse_enip_command_data(data, data_length, current_data, global_data);
+
+        // Check that the Session Handle matches the active ENIP session.
+        if (!enip_session_handle_valid(&global_data->enip_session, enip_header->session_handle))
+        {
+            current_data->enip_data.enip_invalid_nonfatal |= ENIP_INVALID_SESSION_HANDLE;
+        }
+        break;
+    case ENIP_COMMAND_NOP:
+        break;
+    case ENIP_COMMAND_LIST_SERVICES:
+    case ENIP_COMMAND_LIST_IDENTITY:
+    case ENIP_COMMAND_LIST_INTERFACES:
+        if (current_data->direction == CIP_FROM_SERVER)
+        {
+            current_data->enip_data.cpf_decoded = parse_common_packet_format(data,
+                data_length,
+                &current_data->enip_data.enip_cpf,
+                current_data);
+            if (!current_data->enip_data.cpf_decoded)
+            {
+                valid = false;
+            }
+        }
+        break;
+    case ENIP_COMMAND_UNREGISTER_SESSION:
+        // Remove ENIP session from the current TCP session.
+        enip_session_remove(&global_data->enip_session, enip_header->session_handle);
+        break;
+    default:
+        // Ignore legacy cases.
+        break;
+    }
+
+    return valid;
+}
+
+void pack_cip_request_event(const CipRequest* request, CipEventData* cip_event_data)
+{
+    cip_event_data->service_id = request->service;
+
+    if (request->is_forward_open_request)
+    {
+        cip_event_data->type = CIP_DATA_TYPE_CONNECTION;
+        cip_event_data->class_id = request->connection_path_class_id;
+    }
+    else if (request->request_path.primary_segment_type == CipSegment_Type_LOGICAL_CLASS)
+    {
+        // Publish Set Attribute Single services separately than other requests.
+        if (cip_event_data->service_id == SERVICE_SET_ATTRIBUTE_SINGLE
+            && request->request_path.has_instance_id
+            && request->request_path.has_attribute_id)
+        {
+            cip_event_data->instance_id = request->request_path.instance_id;
+            cip_event_data->attribute_id = request->request_path.attribute_id;
+
+            cip_event_data->type = CIP_DATA_TYPE_SET_ATTRIBUTE;
+        }
+        else
+        {
+            cip_event_data->type = CIP_DATA_TYPE_PATH_CLASS;
+        }
+
+        cip_event_data->class_id = request->request_path.class_id;
+    }
+    else if (request->request_path.primary_segment_type == CipSegment_Type_DATA_EXT_SYMBOL)
+    {
+        cip_event_data->type = CIP_DATA_TYPE_PATH_EXT_SYMBOL;
+    }
+    else
+    {
+        cip_event_data->type = CIP_DATA_TYPE_OTHER;
+    }
+}
+
diff --git a/src/service_inspectors/cip/cip_parsing.h b/src/service_inspectors/cip/cip_parsing.h
new file mode 100644 (file)
index 0000000..667fb26
--- /dev/null
@@ -0,0 +1,65 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip_parsing.h author RA/Cisco
+
+/* Description: Data parsing for EtherNet/IP and CIP formats. */
+
+#ifndef CIP_PARSING_H
+#define CIP_PARSING_H
+
+#include <cstdint>
+#include <cstring>
+#include "pub_sub/cip_events.h"       // For CipEventData
+
+#include "cip_definitions.h"  // For CIP structs
+
+namespace snort
+{
+struct Packet;
+}
+
+//// EtherNet/IP Parsing
+
+// Constants - EtherNet/IP encapsulation layer
+#define ENIP_HEADER_SIZE 24u
+
+// EtherNet/IP commands.
+enum EnipCommand
+{
+    ENIP_COMMAND_NOP = 0x0000,
+    ENIP_COMMAND_LIST_SERVICES = 0x0004,
+    ENIP_COMMAND_LIST_IDENTITY = 0x0063,
+    ENIP_COMMAND_LIST_INTERFACES = 0x0064,
+    ENIP_COMMAND_REGISTER_SESSION = 0x0065,
+    ENIP_COMMAND_UNREGISTER_SESSION = 0x0066,
+    ENIP_COMMAND_SEND_RR_DATA = 0x006F,
+    ENIP_COMMAND_SEND_UNIT_DATA = 0x0070
+};
+
+/// EtherNet/IP data parsing functions.
+bool parse_enip_layer(const uint8_t* data,
+    size_t data_length,
+    bool is_TCP,
+    CipCurrentData* current_data,
+    CipGlobalSessionData* global_data);
+
+void pack_cip_request_event(const CipRequest* request, CipEventData* cip_event_data);
+
+#endif  // CIP_PARSING_H
+
diff --git a/src/service_inspectors/cip/cip_session.cc b/src/service_inspectors/cip/cip_session.cc
new file mode 100644 (file)
index 0000000..d6860f7
--- /dev/null
@@ -0,0 +1,534 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip_session.cc author RA/Cisco
+
+/* Description: Functions for managing CIP state data across multiple packets and TCP connections.
+   Note: Performance of all lookup functions is O(n). */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "cip_session.h"
+
+#include <math.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+#include <sys/time.h>
+#include "time/timersub.h"     // For TIMERSUB
+
+#include "cip_parsing.h"  // For CIP constants
+
+static uint32_t f_unconnected_timeout_ms = DEFAULT_UNCONNECTED_REQUEST_TIMEOUT;
+
+bool enip_session_add(EnipSession* enip_session, uint32_t session_handle)
+{
+    // Only 1 ENIP session per TCP connection is allowed.
+    if (enip_session->active)
+    {
+        return false;
+    }
+
+    enip_session->session_handle = session_handle;
+    enip_session->active = true;
+
+    return true;
+}
+
+bool enip_session_remove(EnipSession* enip_session, uint32_t session_handle)
+{
+    if (!enip_session->active)
+    {
+        return false;
+    }
+
+    if (enip_session->session_handle != session_handle)
+    {
+        return false;
+    }
+
+    enip_session->active = false;
+
+    return true;
+}
+
+bool enip_session_handle_valid(const EnipSession* enip_session, uint32_t session_handle)
+{
+    return (enip_session->active && enip_session->session_handle == session_handle);
+}
+
+/// CIP Request and Connection Management.
+static void prune_cip_unconnected_list(CipUnconnectedMessageList* unconnected_list,
+    const struct timeval* timestamp)
+{
+    struct timeval timestamp_diff;
+
+    bool pruned = false;
+    size_t oldest_slot = 0;
+
+    // Prune any message that has exceeded the CIP timeout.
+    size_t i;
+    for (i = 0; i < unconnected_list->list_size; ++i)
+    {
+        if (unconnected_list->list[i].slot_active)
+        {
+            TIMERSUB(timestamp, &unconnected_list->list[i].timestamp, &timestamp_diff);
+
+            // Round up to the nearest whole second.
+            uint32_t timeout_sec
+                = ceil(unconnected_list->list[i].timeout_ms / (double)MSEC_PER_SEC);
+
+            // If the message timeout has been exceeded, remove the request from the list.
+            if (timestamp_diff.tv_sec > timeout_sec)
+            {
+                unconnected_list->list[i].slot_active = false;
+                unconnected_list->count--;
+                pruned = true;
+            }
+
+            // Check if the current item's timestamp is older than the previous oldest.
+            if (timercmp(&unconnected_list->list[i].timestamp,
+                &unconnected_list->list[oldest_slot].timestamp,
+                <) != 0)
+            {
+                oldest_slot = i;
+            }
+        }
+    }
+
+    // If no timeout was exceeded, prune the oldest one.
+    if (!pruned)
+    {
+        unconnected_list->list[oldest_slot].slot_active = false;
+        unconnected_list->count--;
+        unconnected_list->request_pruned = true;
+    }
+}
+
+static void prune_cip_connection_list(CipConnectionList* connection_list,
+    const struct timeval* timestamp)
+{
+    struct timeval ot_timestamp_diff;
+    struct timeval to_timestamp_diff;
+
+    bool pruned = false;
+
+    size_t stale_slot = 0;
+    struct timeval stale_timestamp_diff;
+    memset(&stale_timestamp_diff, 0, sizeof(stale_timestamp_diff));
+
+    // Prune any connection that has exceeded the CIP timeout.
+    size_t i;
+    for (i = 0; i < connection_list->list_size; ++i)
+    {
+        if (connection_list->list[i].slot_active)
+        {
+            TIMERSUB(timestamp, &connection_list->list[i].ot_timestamp, &ot_timestamp_diff);
+            TIMERSUB(timestamp, &connection_list->list[i].to_timestamp, &to_timestamp_diff);
+
+            // If either OT or TO connection timeouts have been exceeded, remove the connection
+            //  from the list.
+            if (ot_timestamp_diff.tv_sec > connection_list->list[i].ot_connection_timeout_sec
+                || to_timestamp_diff.tv_sec > connection_list->list[i].to_connection_timeout_sec)
+            {
+                connection_list->list[i].slot_active = false;
+                connection_list->count--;
+                pruned = true;
+            }
+
+            // Pick the most recent timestamp for this connection.
+            struct timeval connection_timestamp;
+            if (timercmp(&connection_list->list[i].ot_timestamp,
+                &connection_list->list[i].to_timestamp,
+                >) != 0)
+            {
+                connection_timestamp = connection_list->list[i].ot_timestamp;
+            }
+            else
+            {
+                connection_timestamp = connection_list->list[i].to_timestamp;
+            }
+
+            struct timeval timestamp_diff;
+            TIMERSUB(timestamp, &connection_timestamp, &timestamp_diff);
+
+            // Check if the current connection is more stale than the previously found one.
+            if (timercmp(&timestamp_diff, &stale_timestamp_diff, >) != 0)
+            {
+                stale_slot = i;
+                stale_timestamp_diff = timestamp_diff;
+            }
+        }
+    }
+
+    // If no timeout was exceeded, prune the least recently used one.
+    if (!pruned)
+    {
+        connection_list->list[stale_slot].slot_active = false;
+        connection_list->count--;
+        connection_list->connection_pruned = true;
+    }
+}
+
+static bool cip_connection_signature_match(const CipConnectionSignature* left,
+    const CipConnectionSignature* right)
+{
+    if (left->connection_serial_number == right->connection_serial_number
+        && left->originator_serial_number == right->originator_serial_number
+        && left->vendor_id == right->vendor_id)
+    {
+        return true;
+    }
+    else
+    {
+        return false;
+    }
+}
+
+static CipConnection* cip_find_connection_slot(CipConnectionList* connection_list,
+    const struct timeval* timestamp)
+{
+    CipConnection* connection = NULL;
+
+    // Prune old connections if the list is at max capacity.
+    if (connection_list->count == connection_list->list_size)
+    {
+        prune_cip_connection_list(connection_list, timestamp);
+    }
+
+    size_t i;
+    for (i = 0; i < connection_list->list_size; ++i)
+    {
+        if (!connection_list->list[i].slot_active)
+        {
+            connection = &connection_list->list[i];
+            break;
+        }
+    }
+
+    return connection;
+}
+
+CipConnection* cip_find_connection_by_id(
+    CipConnectionList* connection_list,
+    CipPacketDirection direction,
+    uint32_t connection_id,
+    bool established)
+{
+    CipConnection* connection = NULL;
+
+    size_t i;
+    for (i = 0; i < connection_list->list_size; ++i)
+    {
+        if (connection_list->list[i].slot_active
+            && (connection_list->list[i].established == established))
+        {
+            if (direction == CIP_FROM_CLIENT
+                && connection_list->list[i].connection_id_pair.ot_connection_id == connection_id)
+            {
+                connection = &connection_list->list[i];
+                break;
+            }
+
+            if (direction == CIP_FROM_SERVER
+                && connection_list->list[i].connection_id_pair.to_connection_id == connection_id)
+            {
+                connection = &connection_list->list[i];
+                break;
+            }
+        }
+    }
+
+    return connection;
+}
+
+static const CipConnection* cip_find_connection_by_id_any(
+    const CipConnectionList* connection_list,
+    uint32_t ot_connection_id,
+    uint32_t to_connection_id)
+{
+    const CipConnection* connection = NULL;
+
+    size_t i;
+    for (i = 0; i < connection_list->list_size; ++i)
+    {
+        if (connection_list->list[i].slot_active && connection_list->list[i].established)
+        {
+            if (connection_list->list[i].connection_id_pair.ot_connection_id == ot_connection_id)
+            {
+                connection = &connection_list->list[i];
+                break;
+            }
+
+            if (connection_list->list[i].connection_id_pair.to_connection_id == to_connection_id)
+            {
+                connection = &connection_list->list[i];
+                break;
+            }
+        }
+    }
+
+    return connection;
+}
+
+static const CipConnection* cip_find_connection_any(const CipConnectionList* connection_list,
+    const CipConnectionSignature* signature)
+{
+    const CipConnection* connection = NULL;
+
+    size_t i;
+    for (i = 0; i < connection_list->list_size; ++i)
+    {
+        if (connection_list->list[i].slot_active
+            && cip_connection_signature_match(&connection_list->list[i].signature, signature))
+        {
+            connection = &connection_list->list[i];
+            break;
+        }
+    }
+
+    return connection;
+}
+
+static CipConnection* cip_find_connection(CipConnectionList* connection_list,
+    const CipConnectionSignature* signature,
+    bool established)
+{
+    CipConnection* connection = NULL;
+
+    size_t i;
+    for (i = 0; i < connection_list->list_size; ++i)
+    {
+        if (connection_list->list[i].slot_active
+            && (connection_list->list[i].established == established)
+            && cip_connection_signature_match(&connection_list->list[i].signature, signature))
+        {
+            connection = &connection_list->list[i];
+            break;
+        }
+    }
+
+    return connection;
+}
+
+bool cip_add_connection_to_active(CipConnectionList* connection_list,
+    const CipForwardOpenResponse* forward_open_response)
+{
+    // Check that no existing connection has a matching connection ID for either direction.
+    const CipConnection* existing_connection = cip_find_connection_by_id_any(connection_list,
+        forward_open_response->connection_pair.ot_connection_id,
+        forward_open_response->connection_pair.to_connection_id);
+    if (existing_connection)
+    {
+        return false;
+    }
+
+    // Find the existing pending connection.
+    CipConnection* connection = cip_find_connection(connection_list,
+        &forward_open_response->connection_signature,
+        false);
+    if (!connection)
+    {
+        return false;
+    }
+
+    // Save the new Connection ID information, and mark the connection as
+    //  fully established.
+    connection->connection_id_pair = forward_open_response->connection_pair;
+    connection->established = true;
+    connection->to_timestamp = forward_open_response->timestamp;
+
+    return true;
+}
+
+bool cip_remove_connection(CipConnectionList* connection_list,
+    const CipConnectionSignature* connection_signature,
+    bool established)
+{
+    CipConnection* connection = cip_find_connection(connection_list,
+        connection_signature,
+        established);
+    if (!connection)
+    {
+        return false;
+    }
+
+    connection->slot_active = false;
+    connection_list->count--;
+
+    return true;
+}
+
+bool cip_add_connection_to_pending(CipConnectionList* connection_list,
+    const CipForwardOpenRequest* forward_open_request)
+{
+    // Check that there are no pending or existing connections with this signature.
+    const CipConnection* existing_connection = cip_find_connection_any(connection_list,
+        &forward_open_request->connection_signature);
+    if (existing_connection)
+    {
+        return false;
+    }
+
+    CipConnection* connection = cip_find_connection_slot(connection_list,
+        &forward_open_request->timestamp);
+    if (!connection)
+    {
+        return false;
+    }
+
+    connection->signature = forward_open_request->connection_signature;
+    connection->class_id = forward_open_request->connection_path.class_id;
+    connection->established = false;
+
+    // Round up to the nearest whole second.
+    connection->ot_connection_timeout_sec
+        = ceil(forward_open_request->ot_connection_timeout_us / (double)USEC_PER_SEC);
+    connection->to_connection_timeout_sec
+        = ceil(forward_open_request->to_connection_timeout_us / (double)USEC_PER_SEC);
+
+    connection->ot_timestamp = forward_open_request->timestamp;
+    connection->to_timestamp = forward_open_request->timestamp;
+
+    connection->slot_active = true;
+    connection_list->count++;
+
+    return true;
+}
+
+/// CIP Request/Response Matching.
+static CipUnconnectedMessage* find_unconnected_request_slot(
+    CipUnconnectedMessageList* unconnected_list,
+    const struct timeval* timestamp)
+{
+    CipUnconnectedMessage* unconnected_message = NULL;
+
+    // Prune old messages if the list is at max capacity.
+    if (unconnected_list->count == unconnected_list->list_size)
+    {
+        prune_cip_unconnected_list(unconnected_list, timestamp);
+    }
+
+    size_t i;
+    for (i = 0; i < unconnected_list->list_size; ++i)
+    {
+        if (!unconnected_list->list[i].slot_active)
+        {
+            unconnected_message = &unconnected_list->list[i];
+            break;
+        }
+    }
+
+    return unconnected_message;
+}
+
+static CipUnconnectedMessage* find_unconnected_request(
+    CipUnconnectedMessageList* unconnected_list,
+    uint64_t sender_context)
+{
+    CipUnconnectedMessage* unconnected_message = NULL;
+
+    size_t i;
+    for (i = 0; i < unconnected_list->list_size; ++i)
+    {
+        if (unconnected_list->list[i].slot_active
+            && unconnected_list->list[i].sender_context == sender_context)
+        {
+            unconnected_message = &unconnected_list->list[i];
+            break;
+        }
+    }
+
+    return unconnected_message;
+}
+
+bool cip_request_add(CipUnconnectedMessageList* unconnected_list,
+    const EnipSessionData* enip_data,
+    const CipRequest* cip_request,
+    const struct timeval* timestamp)
+{
+    bool valid = true;
+
+    if (enip_data->enip_header.command == ENIP_COMMAND_SEND_RR_DATA)
+    {
+        CipUnconnectedMessage* slot = find_unconnected_request_slot(unconnected_list, timestamp);
+        if (slot)
+        {
+            slot->sender_context = enip_data->enip_header.sender_context;
+            slot->request_type = cip_request->request_type;
+
+            if (cip_request->has_timeout)
+            {
+                slot->timeout_ms = cip_request->timeout_ms;
+            }
+            else
+            {
+                slot->timeout_ms = f_unconnected_timeout_ms;
+            }
+
+            slot->timestamp = *timestamp;
+            slot->slot_active = true;
+            unconnected_list->count++;
+
+            valid = true;
+        }
+        else
+        {
+            valid = false;
+        }
+    }
+
+    return valid;
+}
+
+bool cip_request_remove(CipUnconnectedMessageList* unconnected_list,
+    const EnipSessionData* enip_data,
+    CipRequestType* request_type)
+{
+    bool valid = true;
+
+    if (enip_data->enip_header.command == ENIP_COMMAND_SEND_RR_DATA)
+    {
+        CipUnconnectedMessage* request = find_unconnected_request(unconnected_list,
+            enip_data->enip_header.sender_context);
+        if (request)
+        {
+            *request_type = request->request_type;
+
+            // Remove the request from the list.
+            request->slot_active = false;
+            unconnected_list->count--;
+
+            valid = true;
+        }
+        else
+        {
+            valid = false;
+        }
+    }
+
+    return valid;
+}
+
+void set_unconnected_timeout(uint32_t unconnected_timeout)
+{
+    f_unconnected_timeout_ms = unconnected_timeout;
+}
+
diff --git a/src/service_inspectors/cip/cip_session.h b/src/service_inspectors/cip/cip_session.h
new file mode 100644 (file)
index 0000000..2d5a154
--- /dev/null
@@ -0,0 +1,75 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip_session.h author RA/Cisco
+
+/* Description: Functions for managing CIP state data across multiple packets and TCP connections.
+   */
+
+#ifndef CIP_SESSION_H
+#define CIP_SESSION_H
+
+#include <stdbool.h>
+#include <stdint.h>
+#include "main/snort_config.h"
+#include "main/snort_types.h"
+
+#include "cip_definitions.h"
+
+// Default unconnected request timeout, milliseconds.
+#define DEFAULT_UNCONNECTED_REQUEST_TIMEOUT (30000)
+
+/// ENIP Session Management.
+bool enip_session_add(EnipSession* enip_session, uint32_t session_handle);
+bool enip_session_remove(EnipSession* enip_session, uint32_t session_handle);
+
+// Returns true if session_handle matches the active session.
+bool enip_session_handle_valid(const EnipSession* enip_session, uint32_t session_handle);
+
+/// CIP Connection Management.
+CipConnection* cip_find_connection_by_id(
+    CipConnectionList* connection_list,
+    CipPacketDirection direction,
+    uint32_t connection_id,
+    bool established);
+
+bool cip_add_connection_to_active(CipConnectionList* connection_list,
+    const CipForwardOpenResponse* forward_open_response);
+bool cip_remove_connection(CipConnectionList* connection_list,
+    const CipConnectionSignature* connection_signature,
+    bool established);
+
+bool cip_add_connection_to_pending(CipConnectionList* connection_list,
+    const CipForwardOpenRequest* forward_open_request);
+
+/// CIP Request/Response Matching.
+bool cip_request_add(CipUnconnectedMessageList* unconnected_list,
+    const EnipSessionData* enip_data,
+    const CipRequest* cip_request,
+    const struct timeval* timestamp);
+
+// Find a request in the list, and remove it.
+bool cip_request_remove(CipUnconnectedMessageList* unconnected_list,
+    const EnipSessionData* enip_data,
+    CipRequestType* request_type);
+
+// Set timeout (milliseconds) to use for unconnected messages that don't have a built-in timeout.
+void set_unconnected_timeout(uint32_t unconnected_timeout);
+
+#endif  // CIP_SESSION_H
+
diff --git a/src/service_inspectors/cip/cip_util.h b/src/service_inspectors/cip/cip_util.h
new file mode 100644 (file)
index 0000000..d0f8b40
--- /dev/null
@@ -0,0 +1,51 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2014-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// cip_util.h author RA/Cisco
+
+/* Description: Common utility functions. */
+
+#ifndef CIP_UTIL_H
+#define CIP_UTIL_H
+
+#include <sys/types.h>  // For endian checks
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+
+// Get 16-bit value from little endian byte array.
+static inline uint16_t GetLEUint16(const uint8_t* pData)
+{
+    return (static_cast<uint16_t>(*(pData + 1) << 8)
+           | static_cast<uint16_t>(*(pData + 0) << 0));
+}
+
+// Get 32-bit value from little endian byte array.
+static inline uint32_t GetLEUint32(const uint8_t* pData)
+{
+    return (static_cast<uint32_t>(*(pData + 3) << 24)
+           | static_cast<uint32_t>(*(pData + 2) << 16)
+           | static_cast<uint32_t>(*(pData + 1) << 8)
+           | static_cast<uint32_t>(*(pData + 0) << 0));
+}
+
+#else  // __BYTE_ORDER
+#error "CIP Preprocessor is only supported on Little Endian."
+#endif  // __BYTE_ORDER
+
+#endif  /* CIP_UTIL_H */
+
diff --git a/src/service_inspectors/cip/dev_notes.txt b/src/service_inspectors/cip/dev_notes.txt
new file mode 100644 (file)
index 0000000..c314b67
--- /dev/null
@@ -0,0 +1,12 @@
+CIP (Common Industrial Protocol) is a protocol used in SCADA networks. The
+standard is managed by ODVA (Open DeviceNet Vendor Association). 
+
+The CIP inspector decodes the CIP protocol and provides rule options to 
+access certain protocol fields. This allows a user to write rules for CIP 
+packets without decoding the protocol with a series of ”content” and ”byte 
+test” options.
+
+The preprocessor only evaluates PAF-flushed PDUs. If the rule options don't
+check for this, they'll fire on stale session data when the original packet
+goes through before flushing.
+
diff --git a/src/service_inspectors/cip/ips_cip_attribute.cc b/src/service_inspectors/cip/ips_cip_attribute.cc
new file mode 100644 (file)
index 0000000..b2dcb56
--- /dev/null
@@ -0,0 +1,215 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_attribute.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "framework/range.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "cip_attribute"
+#define s_help \
+    "detection option to match CIP attribute"
+
+//-------------------------------------------------------------------------
+// CIP Attribute rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_attribute_perf_stats;
+
+class CipAttributeOption : public IpsOption
+{
+public:
+    CipAttributeOption(RangeCheck& v) : IpsOption(s_name)
+    { cip_attr = v; }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    RangeCheck cip_attr;
+};
+
+uint32_t CipAttributeOption::hash() const
+{
+    uint32_t a, b, c;
+
+    a = cip_attr.op;
+    b = cip_attr.min;
+    c = cip_attr.max;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipAttributeOption::operator==(const IpsOption& ips) const
+{
+    if ( strcmp(get_name(), ips.get_name()) )
+        return false;
+
+    const CipAttributeOption& rhs = static_cast<const CipAttributeOption&>(ips);
+    return ( cip_attr == rhs.cip_attr );
+}
+
+IpsOption::EvalStatus CipAttributeOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_attribute_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = static_cast<CipFlowData*>(p->flow->get_flow_data(CipFlowData::inspector_id));
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if (session_data->current_data.cip_message_type != CipMessageTypeExplicit
+        || !session_data->current_data.cip_msg.is_cip_request
+        || !session_data->current_data.cip_msg.request.request_path.has_attribute_id)
+    {
+        return NO_MATCH;
+    }
+
+    if ( cip_attr.eval(session_data->current_data.cip_msg.request.request_path.attribute_id) )
+    {
+        return MATCH;
+    }
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+#define RANGE "0:65535"
+
+static const Parameter s_params[] =
+{
+    { "~range", Parameter::PT_INTERVAL, RANGE, nullptr,
+      "match CIP attribute" },
+
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class CipAttributeModule : public Module
+{
+public:
+    CipAttributeModule() : Module(s_name, s_help, s_params) { }
+
+    bool begin(const char*, int, SnortConfig*) override;
+    bool set(const char*, Value&, SnortConfig*) override;
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    RangeCheck cip_attr;
+};
+
+bool CipAttributeModule::begin(const char*, int, SnortConfig*)
+{
+    cip_attr.init();
+    return true;
+}
+
+bool CipAttributeModule::set(const char*, Value& v, SnortConfig*)
+{
+    if ( !v.is("~range") )
+        return false;
+
+    return cip_attr.validate(v.get_string(), RANGE);
+}
+
+ProfileStats* CipAttributeModule::get_profile() const
+{
+    return &cip_attribute_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_attribute_mod_ctor()
+{
+    return new CipAttributeModule;
+}
+
+static void cip_attribute_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_attribute_ctor(Module* p, OptTreeNode*)
+{
+    CipAttributeModule* m = static_cast<CipAttributeModule*>(p);
+    return new CipAttributeOption(m->cip_attr);
+}
+
+static void cip_attribute_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_attribute_mod_ctor,
+        cip_attribute_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_attribute_ctor,
+    cip_attribute_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_attribute = &ips_api.base;
+
diff --git a/src/service_inspectors/cip/ips_cip_class.cc b/src/service_inspectors/cip/ips_cip_class.cc
new file mode 100644 (file)
index 0000000..045a888
--- /dev/null
@@ -0,0 +1,214 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_class.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "framework/range.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "cip_class"
+#define s_help \
+    "detection option to match CIP class"
+
+//-------------------------------------------------------------------------
+// CIP Class rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_class_perf_stats;
+
+class CipClassOption : public IpsOption
+{
+public:
+    CipClassOption(const RangeCheck& v) : IpsOption(s_name)
+    { cip_class = v; }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    RangeCheck cip_class;
+};
+
+uint32_t CipClassOption::hash() const
+{
+    uint32_t a,b,c;
+
+    a = cip_class.op;
+    b = cip_class.min;
+    c = cip_class.max;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipClassOption::operator==(const IpsOption& ips) const
+{
+    if ( strcmp(get_name(), ips.get_name()) )
+        return false;
+
+    const CipClassOption& rhs = static_cast<const CipClassOption&>(ips);
+    return (cip_class == rhs.cip_class);
+}
+
+IpsOption::EvalStatus CipClassOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_class_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = static_cast<CipFlowData*>(p->flow->get_flow_data(CipFlowData::inspector_id));
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if (session_data->current_data.cip_message_type != CipMessageTypeExplicit
+        || !session_data->current_data.cip_msg.is_cip_request
+        || !session_data->current_data.cip_msg.request.request_path.has_class_id)
+    {
+        return NO_MATCH;
+    }
+
+    if ( cip_class.eval(session_data->current_data.cip_msg.request.request_path.class_id) )
+        return MATCH;
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+#define RANGE "0:65535"
+
+static const Parameter s_params[] =
+{
+    { "~range", Parameter::PT_INTERVAL, RANGE, nullptr,
+      "match CIP class" },
+
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class CipClassModule : public Module
+{
+public:
+    CipClassModule() : Module(s_name, s_help, s_params) { }
+
+    bool begin(const char*, int, SnortConfig*) override;
+    bool set(const char*, Value&, SnortConfig*) override;
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    RangeCheck cip_class;
+};
+
+bool CipClassModule::begin(const char*, int, SnortConfig*)
+{
+    cip_class.init();
+    return true;
+}
+
+bool CipClassModule::set(const char*, Value& v, SnortConfig*)
+{
+    if ( !v.is("~range") )
+        return false;
+
+    return cip_class.validate(v.get_string(), RANGE);
+}
+
+ProfileStats* CipClassModule::get_profile() const
+{
+    return &cip_class_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_class_mod_ctor()
+{
+    return new CipClassModule;
+}
+
+static void cip_class_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_class_ctor(Module* p, OptTreeNode*)
+{
+    CipClassModule* m = static_cast<CipClassModule*>(p);
+    return new CipClassOption(m->cip_class);
+}
+
+static void cip_class_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_class_mod_ctor,
+        cip_class_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_class_ctor,
+    cip_class_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_class = &ips_api.base;
+
diff --git a/src/service_inspectors/cip/ips_cip_connpathclass.cc b/src/service_inspectors/cip/ips_cip_connpathclass.cc
new file mode 100644 (file)
index 0000000..18665bc
--- /dev/null
@@ -0,0 +1,215 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_connpathclass.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "framework/range.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "cip_conn_path_class"
+#define s_help \
+    "detection option to match CIP Connection Path Class"
+
+//-------------------------------------------------------------------------
+// CIP Connpathclass rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_connpathclass_perf_stats;
+
+class CipConnpathclassOption : public IpsOption
+{
+public:
+    CipConnpathclassOption(RangeCheck& v) : IpsOption(s_name)
+    { cip_cpc = v; }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    RangeCheck cip_cpc;
+};
+
+uint32_t CipConnpathclassOption::hash() const
+{
+    uint32_t a,b,c;
+
+    a = cip_cpc.op;
+    b = cip_cpc.min;
+    c = cip_cpc.max;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipConnpathclassOption::operator==(const IpsOption& ips) const
+{
+    if ( strcmp(get_name(), ips.get_name()) )
+        return false;
+
+    const CipConnpathclassOption& rhs = static_cast<const CipConnpathclassOption&>(ips);
+    return ( cip_cpc == rhs.cip_cpc );
+}
+
+IpsOption::EvalStatus CipConnpathclassOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_connpathclass_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = static_cast<CipFlowData*>(p->flow->get_flow_data(CipFlowData::inspector_id));
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if (session_data->current_data.cip_message_type != CipMessageTypeExplicit
+        || !session_data->current_data.cip_msg.is_cip_request
+        || !session_data->current_data.cip_msg.request.is_forward_open_request)
+    {
+        return NO_MATCH;
+    }
+
+    if ( cip_cpc.eval(session_data->current_data.cip_msg.request.connection_path_class_id) )
+    {
+        return MATCH;
+    }
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+#define RANGE "0:65535"
+
+static const Parameter s_params[] =
+{
+    { "~range", Parameter::PT_INTERVAL, RANGE, nullptr,
+      "match CIP Connection Path Class" },
+
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class CipConnpathclassModule : public Module
+{
+public:
+    CipConnpathclassModule() : Module(s_name, s_help, s_params) { }
+
+    bool begin(const char*, int, SnortConfig*) override;
+    bool set(const char*, Value&, SnortConfig*) override;
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    RangeCheck cip_cpc;
+};
+
+bool CipConnpathclassModule::begin(const char*, int, SnortConfig*)
+{
+    cip_cpc.init();
+    return true;
+}
+
+bool CipConnpathclassModule::set(const char*, Value& v, SnortConfig*)
+{
+    if ( !v.is("~range") )
+        return false;
+
+    return cip_cpc.validate(v.get_string(), RANGE);
+}
+
+ProfileStats* CipConnpathclassModule::get_profile() const
+{
+    return &cip_connpathclass_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_connpathclass_mod_ctor()
+{
+    return new CipConnpathclassModule;
+}
+
+static void cip_connpathclass_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_connpathclass_ctor(Module* p, OptTreeNode*)
+{
+    CipConnpathclassModule* m = static_cast<CipConnpathclassModule*>(p);
+    return new CipConnpathclassOption(m->cip_cpc);
+}
+
+static void cip_connpathclass_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_connpathclass_mod_ctor,
+        cip_connpathclass_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_connpathclass_ctor,
+    cip_connpathclass_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_connpathclass = &ips_api.base;
+
diff --git a/src/service_inspectors/cip/ips_cip_enipcommand.cc b/src/service_inspectors/cip/ips_cip_enipcommand.cc
new file mode 100644 (file)
index 0000000..043261b
--- /dev/null
@@ -0,0 +1,208 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_enipcommand.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "framework/range.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "enip_command"
+#define s_help \
+    "detection option to match CIP Enip Command"
+
+//-------------------------------------------------------------------------
+// CIP EnipCommand rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_enipcommand_perf_stats;
+
+class CipEnipCommandOption : public IpsOption
+{
+public:
+    CipEnipCommandOption(RangeCheck& v) : IpsOption(s_name)
+    { cip_enip_cmd = v; }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    RangeCheck cip_enip_cmd;
+};
+
+uint32_t CipEnipCommandOption::hash() const
+{
+    uint32_t a,b,c;
+
+    a = cip_enip_cmd.op;
+    b = cip_enip_cmd.min;
+    c = cip_enip_cmd.max;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipEnipCommandOption::operator==(const IpsOption& ips) const
+{
+    if ( strcmp(get_name(), ips.get_name()) )
+        return false;
+
+    const CipEnipCommandOption& rhs = static_cast<const CipEnipCommandOption&>(ips);
+    return ( cip_enip_cmd == rhs.cip_enip_cmd );
+}
+
+IpsOption::EvalStatus CipEnipCommandOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_enipcommand_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = static_cast<CipFlowData*>(p->flow->get_flow_data(CipFlowData::inspector_id));
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if ( cip_enip_cmd.eval(session_data->current_data.enip_data.enip_header.command) )
+    {
+        return MATCH;
+    }
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+#define RANGE "0:65535"
+
+static const Parameter s_params[] =
+{
+    { "~range", Parameter::PT_INTERVAL, RANGE, nullptr,
+      "match CIP Enip Command" },
+
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class CipEnipCommandModule : public Module
+{
+public:
+    CipEnipCommandModule() : Module(s_name, s_help, s_params) { }
+
+    bool begin(const char*, int, SnortConfig*) override;
+    bool set(const char*, Value&, SnortConfig*) override;
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    RangeCheck cip_enip_cmd;
+};
+
+bool CipEnipCommandModule::begin(const char*, int, SnortConfig*)
+{
+    cip_enip_cmd.init();
+    return true;
+}
+
+bool CipEnipCommandModule::set(const char*, Value& v, SnortConfig*)
+{
+    if ( !v.is("~range") )
+        return false;
+
+    return cip_enip_cmd.validate(v.get_string(), RANGE);
+}
+
+ProfileStats* CipEnipCommandModule::get_profile() const
+{
+    return &cip_enipcommand_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_enipcommand_mod_ctor()
+{
+    return new CipEnipCommandModule;
+}
+
+static void cip_enipcommand_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_enipcommand_ctor(Module* p, OptTreeNode*)
+{
+    CipEnipCommandModule* m = static_cast<CipEnipCommandModule*>(p);
+    return new CipEnipCommandOption(m->cip_enip_cmd);
+}
+
+static void cip_enipcommand_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_enipcommand_mod_ctor,
+        cip_enipcommand_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_enipcommand_ctor,
+    cip_enipcommand_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_enipcommand = &ips_api.base;
+
diff --git a/src/service_inspectors/cip/ips_cip_enipreq.cc b/src/service_inspectors/cip/ips_cip_enipreq.cc
new file mode 100644 (file)
index 0000000..68e5e59
--- /dev/null
@@ -0,0 +1,165 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_enipreq.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "enip_req"
+#define s_help \
+    "detection option to match ENIP Request"
+
+//-------------------------------------------------------------------------
+// CIP Enipreq rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_enipreq_perf_stats;
+
+class CipEnipreqOption : public IpsOption
+{
+public:
+    CipEnipreqOption() : IpsOption(s_name) { }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+
+    EvalStatus eval(Cursor&, Packet*) override;
+};
+
+uint32_t CipEnipreqOption::hash() const
+{
+    uint32_t a = 0, b = 0, c = 0;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipEnipreqOption::operator==(const IpsOption& ips) const
+{
+    return !strcmp(get_name(), ips.get_name());
+}
+
+IpsOption::EvalStatus CipEnipreqOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_enipreq_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = static_cast<CipFlowData*>(p->flow->get_flow_data(CipFlowData::inspector_id));
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if (session_data->current_data.direction == CIP_FROM_CLIENT)
+    {
+        return MATCH;
+    }
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+class CipEnipreqModule : public Module
+{
+public:
+    CipEnipreqModule() : Module(s_name, s_help) { }
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+};
+
+ProfileStats* CipEnipreqModule::get_profile() const
+{
+    return &cip_enipreq_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_enipreq_mod_ctor()
+{
+    return new CipEnipreqModule;
+}
+
+static void cip_enipreq_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_enipreq_ctor(Module*, OptTreeNode*)
+{
+    return new CipEnipreqOption;
+}
+
+static void cip_enipreq_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_enipreq_mod_ctor,
+        cip_enipreq_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_enipreq_ctor,
+    cip_enipreq_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_enipreq = &ips_api.base;
+
diff --git a/src/service_inspectors/cip/ips_cip_eniprsp.cc b/src/service_inspectors/cip/ips_cip_eniprsp.cc
new file mode 100644 (file)
index 0000000..bc014e7
--- /dev/null
@@ -0,0 +1,165 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_eniprsp.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "enip_rsp"
+#define s_help \
+    "detection option to match ENIP response"
+
+//-------------------------------------------------------------------------
+// CIP EnipRsp rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_eniprsp_perf_stats;
+
+class CipEnipRspOption : public IpsOption
+{
+public:
+    CipEnipRspOption() : IpsOption(s_name) { }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+
+    EvalStatus eval(Cursor&, Packet*) override;
+};
+
+uint32_t CipEnipRspOption::hash() const
+{
+    uint32_t a = 0, b = 0, c = 0;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipEnipRspOption::operator==(const IpsOption& ips) const
+{
+    return !strcmp(get_name(), ips.get_name());
+}
+
+IpsOption::EvalStatus CipEnipRspOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_eniprsp_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = static_cast<CipFlowData*>(p->flow->get_flow_data(CipFlowData::inspector_id));
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if (session_data->current_data.direction == CIP_FROM_SERVER)
+    {
+        return MATCH;
+    }
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+class CipEnipRspModule : public Module
+{
+public:
+    CipEnipRspModule() : Module(s_name, s_help) { }
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+};
+
+ProfileStats* CipEnipRspModule::get_profile() const
+{
+    return &cip_eniprsp_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_eniprsp_mod_ctor()
+{
+    return new CipEnipRspModule;
+}
+
+static void cip_eniprsp_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_eniprsp_ctor(Module*, OptTreeNode*)
+{
+    return new CipEnipRspOption;
+}
+
+static void cip_eniprsp_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_eniprsp_mod_ctor,
+        cip_eniprsp_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_eniprsp_ctor,
+    cip_eniprsp_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_eniprsp = &ips_api.base;
+
diff --git a/src/service_inspectors/cip/ips_cip_instance.cc b/src/service_inspectors/cip/ips_cip_instance.cc
new file mode 100644 (file)
index 0000000..32e3656
--- /dev/null
@@ -0,0 +1,215 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_instance.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "framework/range.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "cip_instance"
+#define s_help \
+    "detection option to match CIP instance"
+
+//-------------------------------------------------------------------------
+// CIP Instance rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_instance_perf_stats;
+
+class CipInstanceOption : public IpsOption
+{
+public:
+    CipInstanceOption(RangeCheck& v) : IpsOption(s_name)
+    { cip_inst = v; }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    RangeCheck cip_inst;
+};
+
+uint32_t CipInstanceOption::hash() const
+{
+    uint32_t a, b, c;
+
+    a = cip_inst.op;
+    b = cip_inst.min;
+    c = cip_inst.max;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipInstanceOption::operator==(const IpsOption& ips) const
+{
+    if ( strcmp(get_name(), ips.get_name()) )
+        return false;
+
+    const CipInstanceOption& rhs = static_cast<const CipInstanceOption&>(ips);
+    return ( cip_inst == rhs.cip_inst );
+}
+
+IpsOption::EvalStatus CipInstanceOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_instance_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = static_cast<CipFlowData*>(p->flow->get_flow_data(CipFlowData::inspector_id));
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if (session_data->current_data.cip_message_type != CipMessageTypeExplicit
+        || !session_data->current_data.cip_msg.is_cip_request
+        || !session_data->current_data.cip_msg.request.request_path.has_instance_id)
+    {
+        return NO_MATCH;
+    }
+
+    if ( cip_inst.eval(session_data->current_data.cip_msg.request.request_path.instance_id) )
+    {
+        return MATCH;
+    }
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+#define RANGE "0:4294967295"
+
+static const Parameter s_params[] =
+{
+    { "~range", Parameter::PT_INTERVAL, RANGE, nullptr,
+      "match CIP instance" },
+
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class CipInstanceModule : public Module
+{
+public:
+    CipInstanceModule() : Module(s_name, s_help, s_params) { }
+
+    bool begin(const char*, int, SnortConfig*) override;
+    bool set(const char*, Value&, SnortConfig*) override;
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    RangeCheck cip_inst;
+};
+
+bool CipInstanceModule::begin(const char*, int, SnortConfig*)
+{
+    cip_inst.init();
+    return true;
+}
+
+bool CipInstanceModule::set(const char*, Value& v, SnortConfig*)
+{
+    if ( !v.is("~range") )
+        return false;
+
+    return cip_inst.validate(v.get_string(), RANGE);
+}
+
+ProfileStats* CipInstanceModule::get_profile() const
+{
+    return &cip_instance_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_instance_mod_ctor()
+{
+    return new CipInstanceModule;
+}
+
+static void cip_instance_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_instance_ctor(Module* p, OptTreeNode*)
+{
+    CipInstanceModule* m = static_cast<CipInstanceModule*>(p);
+    return new CipInstanceOption(m->cip_inst);
+}
+
+static void cip_instance_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_instance_mod_ctor,
+        cip_instance_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_instance_ctor,
+    cip_instance_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_instance = &ips_api.base;
+
diff --git a/src/service_inspectors/cip/ips_cip_req.cc b/src/service_inspectors/cip/ips_cip_req.cc
new file mode 100644 (file)
index 0000000..2511b33
--- /dev/null
@@ -0,0 +1,166 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_req.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "cip_req"
+#define s_help \
+    "detection option to match CIP request"
+
+//-------------------------------------------------------------------------
+// CIP Req rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_req_perf_stats;
+
+class CipReqOption : public IpsOption
+{
+public:
+    CipReqOption() : IpsOption(s_name) { }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+
+    EvalStatus eval(Cursor&, Packet*) override;
+};
+
+uint32_t CipReqOption::hash() const
+{
+    uint32_t a = 0, b = 0, c = 0;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipReqOption::operator==(const IpsOption& ips) const
+{
+    return !strcmp(get_name(), ips.get_name());
+}
+
+IpsOption::EvalStatus CipReqOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_req_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = static_cast<CipFlowData*>(p->flow->get_flow_data(CipFlowData::inspector_id));
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if (session_data->current_data.cip_message_type == CipMessageTypeExplicit
+        && session_data->current_data.cip_msg.is_cip_request)
+    {
+        return MATCH;
+    }
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+class CipReqModule : public Module
+{
+public:
+    CipReqModule() : Module(s_name, s_help) { }
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+};
+
+ProfileStats* CipReqModule::get_profile() const
+{
+    return &cip_req_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_req_mod_ctor()
+{
+    return new CipReqModule;
+}
+
+static void cip_req_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_req_ctor(Module*, OptTreeNode*)
+{
+    return new CipReqOption;
+}
+
+static void cip_req_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_req_mod_ctor,
+        cip_req_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_req_ctor,
+    cip_req_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_req = &ips_api.base;
+
diff --git a/src/service_inspectors/cip/ips_cip_rsp.cc b/src/service_inspectors/cip/ips_cip_rsp.cc
new file mode 100644 (file)
index 0000000..88097b4
--- /dev/null
@@ -0,0 +1,166 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_rsp.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "cip_rsp"
+#define s_help \
+    "detection option to match CIP response"
+
+//-------------------------------------------------------------------------
+// CIP Rsp rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_rsp_perf_stats;
+
+class CipRspOption : public IpsOption
+{
+public:
+    CipRspOption() : IpsOption(s_name) { }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+
+    EvalStatus eval(Cursor&, Packet*) override;
+};
+
+uint32_t CipRspOption::hash() const
+{
+    uint32_t a = 0, b = 0, c = 0;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipRspOption::operator==(const IpsOption& ips) const
+{
+    return !strcmp(get_name(), ips.get_name());
+}
+
+IpsOption::EvalStatus CipRspOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_rsp_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = static_cast<CipFlowData*>(p->flow->get_flow_data(CipFlowData::inspector_id));
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if (session_data->current_data.cip_message_type == CipMessageTypeExplicit
+        && !session_data->current_data.cip_msg.is_cip_request)
+    {
+        return MATCH;
+    }
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+class CipRspModule : public Module
+{
+public:
+    CipRspModule() : Module(s_name, s_help) { }
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+};
+
+ProfileStats* CipRspModule::get_profile() const
+{
+    return &cip_rsp_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_rsp_mod_ctor()
+{
+    return new CipRspModule;
+}
+
+static void cip_rsp_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_rsp_ctor(Module*, OptTreeNode*)
+{
+    return new CipRspOption;
+}
+
+static void cip_rsp_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_rsp_mod_ctor,
+        cip_rsp_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_rsp_ctor,
+    cip_rsp_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_rsp = &ips_api.base;
+
diff --git a/src/service_inspectors/cip/ips_cip_service.cc b/src/service_inspectors/cip/ips_cip_service.cc
new file mode 100644 (file)
index 0000000..5ca1a43
--- /dev/null
@@ -0,0 +1,219 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_service.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "framework/range.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "cip_service"
+#define s_help \
+    "detection option to match CIP service"
+
+//-------------------------------------------------------------------------
+// CIP Service rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_service_perf_stats;
+
+class CipServiceOption : public IpsOption
+{
+public:
+    CipServiceOption(RangeCheck& v) : IpsOption(s_name)
+    { cip_serv = v; }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    RangeCheck cip_serv;
+};
+
+uint32_t CipServiceOption::hash() const
+{
+    uint32_t a, b, c;
+
+    a = cip_serv.op;
+    b = cip_serv.min;
+    c = cip_serv.max;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipServiceOption::operator==(const IpsOption& ips) const
+{
+    if ( strcmp(get_name(), ips.get_name()) )
+        return false;
+
+    const CipServiceOption& rhs = static_cast<const CipServiceOption&>(ips);
+    return ( cip_serv == rhs.cip_serv );
+}
+
+IpsOption::EvalStatus CipServiceOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_service_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = static_cast<CipFlowData*>(p->flow->get_flow_data(CipFlowData::inspector_id));
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if (session_data->current_data.cip_message_type != CipMessageTypeExplicit)
+    {
+        return NO_MATCH;
+    }
+
+    if (session_data->current_data.cip_msg.is_cip_request)
+    {
+        if ( cip_serv.eval(session_data->current_data.cip_msg.request.service) )
+            return MATCH;
+    }
+    else
+    {
+        if ( cip_serv.eval(session_data->current_data.cip_msg.response.service) )
+            return MATCH;
+    }
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+#define RANGE "0:127"
+
+static const Parameter s_params[] =
+{
+    { "~range", Parameter::PT_INTERVAL, RANGE, nullptr,
+      "match CIP service" },
+
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class CipServiceModule : public Module
+{
+public:
+    CipServiceModule() : Module(s_name, s_help, s_params) { }
+
+    bool begin(const char*, int, SnortConfig*) override;
+    bool set(const char*, Value&, SnortConfig*) override;
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    RangeCheck cip_serv;
+};
+
+bool CipServiceModule::begin(const char*, int, SnortConfig*)
+{
+    cip_serv.init();
+    return true;
+}
+
+bool CipServiceModule::set(const char*, Value& v, SnortConfig*)
+{
+    if ( !v.is("~range") )
+        return false;
+
+    return cip_serv.validate(v.get_string(), RANGE);
+}
+
+ProfileStats* CipServiceModule::get_profile() const
+{
+    return &cip_service_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_service_mod_ctor()
+{
+    return new CipServiceModule;
+}
+
+static void cip_service_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_service_ctor(Module* p, OptTreeNode*)
+{
+    CipServiceModule* m = static_cast<CipServiceModule*>(p);
+    return new CipServiceOption(m->cip_serv);
+}
+
+static void cip_service_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_service_mod_ctor,
+        cip_service_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_service_ctor,
+    cip_service_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_service = &ips_api.base;
+
diff --git a/src/service_inspectors/cip/ips_cip_status.cc b/src/service_inspectors/cip/ips_cip_status.cc
new file mode 100644 (file)
index 0000000..d67a9b9
--- /dev/null
@@ -0,0 +1,214 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2019-2019 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ips_cip_status.cc author Jian Wu <jiawu2@cisco.com>
+
+/* Description: Rule options for CIP preprocessor */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "framework/range.h"
+#include "hash/hashfcn.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+
+#include "cip.h"
+
+using namespace snort;
+
+#define s_name "cip_status"
+#define s_help \
+    "detection option to match CIP response status"
+
+//-------------------------------------------------------------------------
+// CIP Status rule option
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats cip_status_perf_stats;
+
+class CipStatusOption : public IpsOption
+{
+public:
+    CipStatusOption(RangeCheck& v) : IpsOption(s_name)
+    { cip_status = v; }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    RangeCheck cip_status;
+};
+
+uint32_t CipStatusOption::hash() const
+{
+    uint32_t a, b, c;
+
+    a = cip_status.op;
+    b = cip_status.min;
+    c = cip_status.max;
+
+    mix_str(a, b, c, get_name());
+    finalize(a,b,c);
+
+    return c;
+}
+
+bool CipStatusOption::operator==(const IpsOption& ips) const
+{
+    if ( strcmp(get_name(), ips.get_name()) )
+        return false;
+
+    const CipStatusOption& rhs = static_cast<const CipStatusOption&>(ips);
+    return ( cip_status == rhs.cip_status );
+}
+
+IpsOption::EvalStatus CipStatusOption::eval(Cursor&, Packet* p)
+{
+    Profile profile(cip_status_perf_stats);
+
+    if ( !p->flow || !p->is_full_pdu() )
+        return NO_MATCH;
+
+    CipFlowData* fd = (CipFlowData*)p->flow->get_flow_data(CipFlowData::inspector_id);
+
+    if (!fd)
+        return NO_MATCH;
+
+    CipSessionData* session_data = &fd->session;
+
+    if (session_data->current_data.cip_message_type != CipMessageTypeExplicit
+        || session_data->current_data.cip_msg.is_cip_request)
+    {
+        return NO_MATCH;
+    }
+
+    if ( cip_status.eval(session_data->current_data.cip_msg.response.status.general_status) )
+    {
+        return MATCH;
+    }
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// module
+//-------------------------------------------------------------------------
+
+#define RANGE "0:255"
+
+static const Parameter s_params[] =
+{
+    { "~range", Parameter::PT_INTERVAL, RANGE, nullptr,
+      "match CIP response status" },
+
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class CipStatusModule : public Module
+{
+public:
+    CipStatusModule() : Module(s_name, s_help, s_params) { }
+
+    bool begin(const char*, int, SnortConfig*) override;
+    bool set(const char*, Value&, SnortConfig*) override;
+    ProfileStats* get_profile() const override;
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    RangeCheck cip_status;
+};
+
+bool CipStatusModule::begin(const char*, int, SnortConfig*)
+{
+    cip_status.init();
+    return true;
+}
+
+bool CipStatusModule::set(const char*, Value& v, SnortConfig*)
+{
+    if ( !v.is("~range") )
+        return false;
+
+    return cip_status.validate(v.get_string(), RANGE);
+}
+
+ProfileStats* CipStatusModule::get_profile() const
+{
+    return &cip_status_perf_stats;
+}
+
+//-------------------------------------------------------------------------
+// api
+//-------------------------------------------------------------------------
+
+static Module* cip_status_mod_ctor()
+{
+    return new CipStatusModule;
+}
+
+static void cip_status_mod_dtor(Module* m)
+{
+    delete m;
+}
+
+static IpsOption* cip_status_ctor(Module* p, OptTreeNode*)
+{
+    CipStatusModule* m = static_cast<CipStatusModule*>(p);
+    return new CipStatusOption(m->cip_status);
+}
+
+static void cip_status_dtor(IpsOption* p)
+{
+    delete p;
+}
+
+static const IpsApi ips_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        cip_status_mod_ctor,
+        cip_status_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    cip_status_ctor,
+    cip_status_dtor,
+    nullptr
+};
+
+const BaseApi* ips_cip_status = &ips_api.base;
+
index eba4435235c9e03607f1a8925268cf823ae5ba0f..aee10b8a8209590fe2733ca0685cf0c27feb1691 100644 (file)
@@ -48,6 +48,7 @@ extern const BaseApi* sin_telnet;
 extern const BaseApi* sin_wizard;
 
 // these define multiple plugins
+extern const BaseApi* sin_cip[];
 extern const BaseApi* sin_dce[];
 extern const BaseApi* sin_dnp3[];
 extern const BaseApi* sin_gtp[];
@@ -86,6 +87,7 @@ void load_service_inspectors()
     PluginManager::load_plugins(sin_ssl);
 
 #ifdef STATIC_INSPECTORS
+    PluginManager::load_plugins(sin_cip);
     PluginManager::load_plugins(sin_dce);
     PluginManager::load_plugins(sin_dnp3);
     PluginManager::load_plugins(sin_gtp);