#!/usr/bin/ruby
#
# Details: Fgen subsystem, Snort rule generator
# Authors: Dmitry Maksyoma <dmaks@esphion.com>
# Started: 10/2005
#
# (c) 2005 Esphion Limited
# Copyright in this document, whether in written, electronic or other
# format including any source code or other computer code set forth
# therein or attached thereto and copyright in all parts thereof is owned
# by Esphion Limited, who reserves all rights therein.
# In particular, no part of this document/computer code may be reproduced,
# copied, stored in a retrieval system, used or transmitted by any means
# whatsoever without the prior written consent of Esphion Limited.
#
# All enquiries in relation thereto should be directed to:
#
# Esphion Ltd
# P.O. Box 300496,
# Albany,
# Auckland,
# New Zealand.
#
# Phone: +64 9 415 0227
# Fax:	 +64 9 415 0228
# Email: info@esphion.com

$:.insert(-2, File.join(File.dirname(File.expand_path($0)), "../lib"))
require 'pattern/entities'
require 'fgen/formatters'
require 'fgen/common'
include Entities

def fmt_ip_snort(bytes)
  add_cidr(bytes, 4)
end

SNORT_IP_FLAGS_DESC = {IPHeader::RESERVED_FLAG=>"R",
  IPHeader::DF_FLAG=>"D", IPHeader::MORE_FLAG=>"M"}

SNORT_IP_OPTIONS_DESC = {0=>"eol", 1=>"nop", 130=>"sec", 131=>"lsrr",
  137=>"ssrr", 7=>"rr", 136=>"satid", 68=>"ts"}

SNORT_TCP_FLAGS_DESC = {TCPHeader::FIN_FLAG=>"F",
  TCPHeader::SYN_FLAG=>"S", TCPHeader::RST_FLAG=>"R",
  TCPHeader::PSH_FLAG=>"P", TCPHeader::ACK_FLAG=>"A",
  TCPHeader::URG_FLAG=>"U", TCPHeader::ECE_FLAG=>"1",
  TCPHeader::CWR_FLAG=>"2"}

SNORT_PROTOCOL_DESC = {IPHeader::PROTO_ICMP=>"icmp",
  IPHeader::PROTO_TCP=>"tcp", IPHeader::PROTO_UDP=>"udp"}

def format_pattern(pattern, name, src_ips, dst_ips)
  #name = purify_name(name)
  ipl = pattern.ip
  ippl = pattern.ipp
  pl = pattern.packetPayload

  return nil unless ipl

  protocol = nil
  # Treat fragmeted packets as IP packets.
  # TODO: payload support for such packets.
  if ipl.flags_more
    ippl = pl = nil 
    protocol = 'ip'
  else
    protocol = SNORT_PROTOCOL_DESC[ipl.proto] || 'ip'
  end

  # Use the first IP option that has a description in SNORT_IP_OPTIONS_DESC
  # (only options supported by Snort have descriptions).
  ipopts = nil
  ipl.ipopts.each { |opt|
    if desc = SNORT_IP_OPTIONS_DESC[opt]
      ipopts = "ipopts: #{desc};"
      break
    end
  }

  if src_ips
    srcip = bracket_ips(src_ips)
  else
    srcip = fmt_ip_snort(ipl[:srcip])
  end
  srcip = "any" unless srcip
  if dst_ips
    dstip = bracket_ips(dst_ips)
  else
    dstip = fmt_ip_snort(ipl[:dstip])
  end
  dstip = "any" unless dstip

  srcport = dstport = nil
  if ippl
    if ippl.has_fld?(:srcport)
      srcport = ippl.srcport
    end
    if ippl.has_fld?(:dstport)
      dstport = ippl.dstport
    end
  end
  srcport = "any" unless srcport
  dstport = "any" unless dstport

  tos = "tos: #{$_};" if $_ = ipl.tos
  ip_id = "id: #{$_};" if $_ = ipl.ip_id
  frag_offset = "fragoffset: #{$_};" if $_ = ipl.frag_offset
  if ip_flags = ipl.flags
    set_flags = ""
    IPHeader::ALL_FLAGS.each{|f|
      set_flags << SNORT_IP_FLAGS_DESC[f] if ip_flags & f > 0 
    }	
    ip_flags = "fragbits: " << (set_flags.empty? ? "!RDM" : "#{set_flags}") << ";"
  end
  ttl = "ttl: #{$_};" if $_ = ipl.ttl
  ip_proto = "ip_proto: #{$_};" \
    if !ipl.flags_more && protocol == 'ip' && \
      ($_ = SNORT_PROTOCOL_DESC[ipl.proto] || ipl.proto)

  seq = ack = tcp_flags = window = stateless = nil
  icmp = []; other = ''
  if ippl
    if (ipl.proto_tcp)
      seq = "seq: #{$_};" if $_ = ippl.seq
      ack = "ack: #{$_};" if $_ = ippl.ack
      if tcp_flags = ippl.flags
	if tcp_flags == 0
	  tcp_flags = "flags: 0;"
	else
	  t = "flags: "
	  TCPHeader::ALL_FLAGS.each{|f|
	    t << SNORT_TCP_FLAGS_DESC[f] if tcp_flags & f > 0
	  }
	  tcp_flags = t << ";"
	end
      end
      window = "window: #{$_};" if $_ = ippl.window
      stateless = "stateless; "
    elsif (ipl.proto_udp)
    elsif (ipl.proto_icmp)
      icmp << " itype: #{$_}; " if $_ = ippl.itype
      icmp << " icode: #{$_}; " if $_ = ippl.icode
      icmp << " icmp_id: #{$_}; " if $_ = ippl.iid
      icmp << " icmp_seq: #{$_}; " if $_ = ippl.iseq
      icmp = icmp.empty? ? nil : icmp.join(" ")
    else
      ippl.layer.keys.sort.each{|key|
	other << "byte_test 1, =, #{ippl.layer[key]}, #{key}; "
      }
      other << "\n"
    end
  end

  #PAYLOAD LAYER
  payload = nil
  if pl
    #puts "totalLen: #{totalLen}, headerLen: #{headerLen}, dataOffset: #{dataOffset}"
    baseOffset = (ipl.proto_icmp && ippl && \
      (ippl.itype != 8 && ippl.itype != 0)) ? 4 : 0
    payload = \
      fmt_payload(pl, "content: ", "rawbytes", baseOffset)
  end

  name = "Esphion signature (default name)" unless name
  msg = "msg: \"#{name}\";"

  ("alert #{protocol} #{srcip} #{srcport} -> #{dstip} #{dstport} " <<
    "( #{tos} #{ip_id} #{frag_offset} #{ip_flags} #{ttl} #{ip_proto} " <<
    "#{ipopts} #{seq} #{ack} #{tcp_flags} #{window} #{stateless}" <<
    "#{icmp} #{other} #{payload} #{msg} )").strip.gsub(/[ \t\r\f]+/, ' ')
end

do_format
