#! /usr/bin/env python
#
# $Header: /home/ram/u0/vern/bro/RCS/snort2bro,v 1.5 2003/08/31 00:38:11 vern Exp $

import sys
import re
import getopt
import os.path
import struct

# FIXME: Not all of the implemented Snort options are really tested...

snortcmd       = re.compile( "(preprocessor|include|var|config|alert|log|pass|activate|dynamic|output)\s*:?\s*(.*)" )
snortrule      = re.compile( "(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+\((.*)\)" )
snortopt       = re.compile( r"([a-zA-Z_]+)\s*(:\s*(([^;]|(?<=\\);)+)|);" )

snortipre      = "(\d+\.\d+\.\d+\.\d+(/\d+)?)"
snortip        = re.compile( "(!)?%s" % snortipre )
snortiplist    = re.compile( "(!)?\[(%s(,%s)*)\]" % ( snortipre,snortipre ) )
snortport      = re.compile( "(!)?(\d+)" )
snortportrange = re.compile( "(!)?(\d+)?:(\d+)?" )
snortval       = re.compile( "([<>=!]?) *(\d+)" )
snortvalrange  = re.compile( "(\d+)? *- *(\d+)?" )
snortbytecode  = re.compile( r"\\\|\s*(([a-fA-F0-9]{2}\s*)+)\s*\\\|" ) # "|" are quoted when we do the match
snorthex       = re.compile( "(..) *" )
snortquote     = re.compile( r"\\(.)" )
snortalpha     = re.compile( r"(\\x[a-fA-F0-9]{2}|[A-Za-z])" )
snortrpc       = re.compile( r"((\d)+|\*) *, *((\d)+|\*) *, *((\d)+|\*)" )

snortsid       = re.compile( "sid: *(\d+)" )

# Mapping of Snort's variables to Bro's
#     <snort> -> ( <bro>, <invert> )
MapVars = {
    "external_net": ( "local_nets", 1 ),
    "home_net": ( "local_nets", 0 ),
    "http_servers": ( "http_servers", 0 ),
    "http_ports": ( "http_ports", 0 ),
    "oracle_ports": ( "oracle_ports", 0 ),
    "smtp_servers": ( "smtp_servers", 0 ),
    "sql_servers": ( "sql_servers", 0 ),
    "telnet_servers": ( "telnet_servers", 0 ),
}

# Mapping of variables to content
SnortVars = {}

# List of tuples (file,linenr) for all not fully processed input files:
Inputs = []

# Last input line read
RawInputLine = ""

# Counts Snort rules without SID
UnknownCount = 0

# There may be some rules for which it don't make sense to translate them. We ignore them.
IgnoreSIDs = (
    # sid-526 BAD TRAFFIC data in TCP SYN packet 
    "sid-526",
      )

def error( str ):
    if Inputs:
        if RawInputLine:
            print >>sys.stderr, ">>", RawInputLine,
            if RawInputLine[-1] != "\n":
                print >>sys.stderr
        print >>sys.stderr, "Error in %s, line %d: %s" % ( Inputs[0][0].name, Inputs[0][1], str )

    else:
        print >>sys.stderr, "Error:", str
    sys.exit( 1 )

def warning( str ):
    if Inputs:
        if RawInputLine:
            print >>sys.stderr, ">>", RawInputLine,
            if RawInputLine[-1] != "\n":
                print >>sys.stderr
        print >>sys.stderr, "Warning in %s, line %d: %s" % ( Inputs[0][0].name, Inputs[0][1], str )
    else:
        print >>sys.stderr, "Warning:", str

# Converts Snort-like globs into regexp chars
def replaceGlobs( str ):
    # Note that the globs are quoted when we see them here
    # FIXME: If the original pattern contains a "\?" or "\*" it
    # will be converted as well.
    str = str.replace( "\\*", ".*" )
    str = str.replace( "\\?", "." )
    return str

# Replaces Snort-like bytecodes by \x.. sequences
def replaceByteCodes( str ):
    return snortbytecode.sub( lambda bytecode: snorthex.sub( "\\x\\1", bytecode.group( 1 ) ), str )

# Removes Snort's \ quotations
def removeQuotes( str ):
    return snortquote.sub( "\\1", str )

# Quotes all '"'
def quoteStr( str ):
    return str.replace( "\"", "\\\"" )

# Translates all alphabetical characters to case-insensitive classes
def makeCaseInsensitive( str ):

    def replaceby( match ):
        s = match.group( 1 )
        # Do not change hex codes
        if s.startswith( "\\x" ):
            return s
        return "[%s%s]" % ( s.lower(), s.upper() )

    str = snortalpha.sub( replaceby , str )
    return str

# Insert some special variants/character classes for URIs
#     - "/ -> [/\]"
def makeURIPattern( str ):
    str = str.replace( "\\/", "[\\/\\\\]" )
    return str

# Escapes all regex control characters  so that it can be safely used as a regex
def escapeCtrl( str ):
    re = ""
    for i in str:
        if "^$|.*+?[](){}/\"\\".find( i ) >= 0:
            i = "\\" + i
        re += i
    return re

# Converts a Snort pattern into a RE
# If icase==1, constructs an case-insensitive regex.
# If uri==1, creates some special things for URIs (see above)
# If glob==1, the pattern is a Snort-like "regex" glob
# If negate==1, the pattern is negated
# If neglen>0, it's the number of character which are *not* allowed to match the negated pattern
#
# FIXME: We should use a more sophisticated parser here
#
def patternToRE( str, icase, uri, globs, negate, neglen ):

    if negate:
        if patternLength( str ) > 1:
            warning( "Can\'t negate patterns with more than one character" )
            return "<willnevermatch>"
        str = "[^%s]" % replaceByteCodes( escapeCtrl( str ) )

        if neglen > 0:
            return str + "{%d}" % neglen
        else:
            return str + "*"

    str = removeQuotes( str )
    str = escapeCtrl( str )
    if globs:
        str = replaceGlobs( str )
    str = replaceByteCodes( str )

    if uri:
        str = makeURIPattern( str )

    if icase:
        str = makeCaseInsensitive( str )

    return str

# Counts the number chars in the Snort pattern
def patternLength( str ):
    str = removeQuotes( str )
    str = escapeCtrl( str )
    str = replaceByteCodes( str )

    count = 0
    esc = 0

    for c in str:
        if c == "\\" and not esc:
            esc = 1
            continue

        if esc and c == "x":
            count -= 2

        esc = 0
        count += 1

    return count

# Parse a Snort variable declaration
def parseVar( decl ):
    ( key, value ) = decl.split()
    SnortVars[ "$"+key ] = value

# Perform Snort's variable expansion
def expandVars( str ):
    for ( key, value ) in SnortVars.items():
        str = str.replace( key, value )
    return str

# Parse Snort's rule options
def parseRuleOptions( str ):

    options = {}

    m = snortopt.findall( str )

    lastcontent = None
    depth = distance = negate = nocase = offset = regex = within = -1

    negpattern = -1

#    print str

    for ( b1, b2, b3, b4 ) in m:
        if b2:
            val = b3
            if val[0] == "!":
                negpattern = 1
                val = val[1:]
            if len( val ) >= 2:
                if val[0] == '"' and val[-1] == '"':
                    val = val[1:-1]
        else:
            val = ""

        # Note that there may be more than one depth/offset per rule.
        # Each depth/offset/regex affects exactly that content which precedes it.
        # It's illegal to have a depth/offset/regex before a content.
        # That is all undocumented.
        # ... !@#$%^&* ...

        option = b1.lower()

        if option == "content":

            if not lastcontent:
                lastcontent = val
                continue

            oldval = val
            val = ( lastcontent, depth, distance, negate, nocase, offset, regex, within )
            depth = distance = negate = nocase = offset = regex = within = -1
            negate = negpattern
            lastcontent = oldval

        elif option == "depth":
            depth = int( val )
            continue

        elif option == "distance":
            distance = int( val )
            continue

        elif option == "within":
            within = int( val )
            continue

        elif option == "offset":
            offset = int( val )
            continue

        elif option == "regex":
            regex = 1
            continue

        elif option == "nocase":
            nocase = 1
            continue

        try:
            options[ option ] += [ val ]
        except LookupError:
            options[ option ] = [ val ]

    if lastcontent:
        try:
            options[ "content" ] += [ ( lastcontent, depth, distance, negate, nocase, offset, regex, within ) ]
        except LookupError:
            options[ "content" ] = [ ( lastcontent, depth, distance, negate, nocase, offset, regex, within ) ]

    return options

# Parses a list of IPs in Snort's format
def parseIP( ip ):

    # See if it's an unexpanded var.
    if ip.startswith( "$" ):
        try:
            ( brovar, neg ) = MapVars[ ip[1:] ]
            return ( neg, [brovar] )
        except LookupError:
            error( "Unknown variable " + ip )

    m = snortip.match( ip )
    if m:
        ips = ( m.group( 2 ), )
    else:
        m = snortiplist.match( ip )
        if m:
            ips = m.group( 2 ).split( "," )
        else:
            error( "Can\'t parse IP " + ip )

    if m.group( 1 ) != "!":
        return( 0, ips )
    else:
        return( 1, ips )

# Parses a list of ports in Snort's format
def parsePort( port ):

    # See if it's an unexpanded var.
    if port.startswith( "$" ):
        try:
            ( brovar, neg ) = MapVars[ port[1:] ]
            if neg:
                internal_error( "Can\'t negate mapping for port vars" )
            return ( 0, brovar, brovar )
        except LookupError:
            error( "Unknown variable " + port )

    m = snortportrange.match( port )
    if m:
        try:
            min = int( m.group( 2 ) )
        except TypeError:
            min = 0
        try:
            max = int( m.group( 3 ) )
        except TypeError:
            max = 65535
    else:
        m = snortport.match( port )
        if m:
            min = max = int( m.group( 2 ) )
        else:
            error( "Can\'t parse port " + port )

    if m.group( 1 ) != "!":
        return( 0, min, max )
    else:
        return( 1, min, max )

######

# Convert the common head of a Snort rule
def convertHead( prot, srcip, srcport, dstip, dstport ):

    rule = ""

    # Convert IP protocol
    if prot.lower() != "ip":
	rule += "  ip-proto == %s\n" % prot.lower()

    # Convert IPs
    for ( tag, ip ) in ( ( "src-ip", srcip ), ( "dst-ip", dstip ) ):
        if ip != "any":
            ( negate, iplist ) = parseIP( ip )
            if not negate:
                cmp = "=="
            else:
                cmp = "!="

            rule += "  %s %s %s\n" % ( tag, cmp, ",".join( iplist ) )

    # Convert Ports
    for ( tag, port ) in ( ( "src-port", srcport ), ( "dst-port", dstport ) ):
        if port != "any":
            ( negate, min, max ) = parsePort( port )
            if min == max:
                if not negate:
                    cmp = "=="
                else:
                    cmp = "!="
                rule += "  %s %s %s\n" % ( tag, cmp, min )
            else:
                if not negate:
                    cmp1 = ">="
                    cmp2 = "<="
                else:
                    cmp1 = "<"
                    cmp2 = ">"

                rule += "  %s %s %s\n" % ( tag, cmp1, min )
                rule += "  %s %s %s\n" % ( tag, cmp2, max )

    return rule

# Converts one of Snort's bit strings (like "A+" for "flags")
def convertBitSet( str, bitspecs, hdrfield, fieldmask ):

    # This is not strictly Snort conforming but works as long as the
    # flag string is not ambigious.

    type = ""
    mask = 0
    for c in str:
        if "+!*".find( c ) >= 0:
            type = c
            continue
        try:
            mask |= bitspecs[c]
        except LookupError:
            error( "Unknown bit in \"%s\"" % str )

    if type == "":
        # Snort's default is check for equality (undocumented)
        rule = "  %s & %d == %d\n" % ( hdrfield, fieldmask, mask )
    if type == "+":
        rule = "  %s & %d == %d\n" % ( hdrfield, ( mask | fieldmask ), mask )
    if type == "*":
        rule = "  %s & %d != 0\n" % ( hdrfield, ( mask | fieldmask ) )
    if type == "!":
        rule = "  %s & %d == 0\n" % ( hdrfield, ( mask | fieldmask ) )

    return rule

# Converts a test for a value which may include some range (e.g. "<5", "21-42")
def convertVal( str, hdrfield ):

    m = snortvalrange.match( str )
    if m:
        try:
            min = int( m.group( 1 ) )
        except TypeError:
            min = 0
        try:
            max = int( m.group( 2 ) )
        except TypeError:
            max = -1

        rule = ""
        if min > 0:
            rule += "  %s >= %d\n" % ( hdrfield, min )
        if min >= 0:
            rule += "  %s <= %d\n" % ( hdrfield, max )
        return rule

    m = snortval.match( str )
    if m:
        val = int( m.group( 2 ) )
        cmp = m.group( 1 )

        if cmp == "<" or cmp == ">":
            return "  %s %s %d\n" % ( hdrfield, cmp, val )
        if cmp == "!":
            return "  %s != %d\n" % ( hdrfield, val )
        if cmp == "" or cmp == "=":
            return "  %s == %d\n" % ( hdrfield, val )

    error( "Can\'t parse value \"%s\"" % str )

# Convert one value of the RPC triple into a RE
def convertRPCVal( str ):
    if str == "*":
        return "."
    else:
        # Convert value to hex pattern in network bye order
        val = struct.pack( "!I", int( str ) )
        return ( "\\x%02x" * 4 ) % struct.unpack( "BBBB", val )

# Sanity check to see if the two protocols match
def checkProt( testprot, option, ruleprot ):
    if testprot != ruleprot:
        error( "Option \"%s\" only valid for %s rules" % ( option, testprot.upper() ) )

def convertOptions( options, prot ):

    rule = ""
    payloads = []

    for ( key, vallist ) in options.items():

        if key == "ack":
            checkProt( "tcp", key, prot )
            rule += "  header tcp[8:4] == %d\n" % int( vallist[0] )

        elif key == "classtype":
            pass

        elif key == "content":

            for i in range( len( vallist ) ):

                ( content, depth, distance, negate, nocase, offset, regex, within ) = vallist[i]

                # Special case: If have we have a payload size which equals the size of the pattern,
                # the payload has to match exactly

                prefix = ".*"

                if "dsize" in options.keys():
                    try:
                        if patternLength( content ) == int( options["dsize"][0] ):
                            prefix = ""
                    except ValueError:
                        # dsize is something which contains a range; do nothing as
                        # it's just an optimization after all
                        pass

                realdepth = depth;
                realoffset = offset;

                maxdist = 1

                if depth >= 0:
                    realdepth -= patternLength( content )

                if offset >= 0:
                    realoffset -= 1
                    maxdist = 0

                if offset >= 0 and depth >= 0:
                    realdepth -= offset
                    maxdist = 1

                if within >= 0:
                    realdepth += within - patternLength( content ) + 1

                if distance >= 0:
                    realoffset += distance + 1
                    maxdist = 0

                if within >= 0 and distance >= 0:
                    maxdist = 1

                if realdepth < 0 and realoffset <= 0:
                    re = prefix
                else:
                    re = ""

                if realdepth < 0:
                    realdepth = -1

                if realoffset >= 0:
                    if maxdist:
                        re += ".{%d}" % realoffset
                    else:
                        re += ".{%d}.*" % realoffset

                if realdepth > 0 and negate < 0:
                    re += ".{0,%d}" % realdepth
                    maxdist = 1

                re += patternToRE( content, nocase >= 0, 0, regex >= 0, negate >= 0, realdepth + 1 )

                if distance >= 0 or within >= 0:
                    try:
                        payloads[-1] += re
                    except LookupError:
                        payloads = [ re ]
                else:
                    payloads += [ re ]

                if REFile:
                    print >>REFile, re
                if PatternFile:
                    print >>PatternFile, val

        elif key == "depth":
            # Ignore it here; we test for it when handling content
            pass

        elif key == "distance":
            # Ignore it here; we test for it when handling content
            pass

        elif key == "dsize":
            rule += convertVal( vallist[0], "payload-size" )
            pass

        elif key == "flags":
            checkProt( "tcp", key, prot )

            TCPFlags =  { "F": 0x01, "S": 0x02, "R": 0x04, "P": 0x08,
                          "A": 0x10, "U": 0x20, "2": 0x40, "1": 0x80, "0": 0x00 }
            rule += convertBitSet( "".join( vallist ), TCPFlags, "header tcp[13:1]", 0xff )

        elif key == "flow":
            checkProt( "tcp", key, prot )

            Flows = { "established" : "established",
                      "to_server" : "originator",
                      "to_client" : "responder",
                      "from_server" : "responder",
                      "from_client" : "originator",
                      "state_less" : "state_less"
                      }

            tcpstate = []
            for val in vallist:
                for ( snort, bro ) in Flows.items():
                    if val.find( snort.lower() ) >= 0:
                        tcpstate += ( bro, )
            if tcpstate:
                rule += "  tcp-state %s\n" % ",".join( tcpstate )

        elif key == "fragbits":
            FragFlags = { "M": 0x20, "D": 0x40, "R": 0x80 }
            rule += convertBitSet( "".join( vallist ), FragFlags, "header ip[6:1]", 0xe0 )

        elif key == "icmp_seq":
            checkProt( "icmp", key, prot )
            # Check if it's an ECHO or ECHO_REPLY packet
            # Note Snort's undocumented behaviour: It does check REPLYs
            # as well, and it does not check the ICMP code
            rule += "  header icmp[0:1] == 0,8\n"
            rule += "  header icmp[6:2] == %d\n" % int( vallist[0] )

        elif key == "icmp_id":
            checkProt( "icmp", key, prot )
            # Check if it's an ECHO or ECHO_REPLY packet
            # Note Snort's undocumented behaviour: It does check REPLYs
            # as well, and it does not check the ICMP code
            rule += "  header icmp[0:1] == 0,8\n"
            rule += "  header icmp[4:2] == %d\n" % int( vallist[0] )

        elif key == "icode":
            checkProt( "icmp", key, prot )
            rule += "  header icmp[1:1] == %d\n" % int( vallist[0] )

        elif key == "id":
            rule += "  header ip[4:2] == %d\n" % int( vallist[0] )

        elif key == "ip_proto":
            rule += convertVal( vallist[0], "header ip[9:1]" )

        elif key == "ipopts":
            rule += "  ip-options %s\n" % ",".join( vallist ).lower()

        elif key == "itype":
            checkProt( "icmp", key, prot )
            rule += "  header icmp[0:1] == %d\n" % int( vallist[0] )

        elif key == "msg":
            rule += "  event \"%s\"\n" % quoteStr( removeQuotes( " ".join( vallist ) ) )

        elif key == "nocase":
            # Ignore it here; we test for it when handling content
            pass

        elif key == "offset":
            # Ignore it here; we test for it when handling content
            pass

        elif key == "ref":
            pass

        elif key == "reference":
            pass

        elif key == "regex":
            pass

        elif key == "rev":
            pass

        elif key == "rpc":
            m = snortrpc.match( vallist[0] )
            if not m:
                error( "Can\'t parse RPC values" )
            ( app, proc, version ) = ( m.group( 1 ), m.group( 3 ), m.group( 5 ) )

            # The Snort rule set has only explicit UDP/TCP rules containg
            # "rpc" but no general IP rules. So we use the rule type
            # to decide whether we check for UDP- or TCP-style RPC
            #
            # Snort only looks at calls, but not on replies (undocumented)
            # Snort validates the RPC_MSG_VERSION (undocumented)
            if prot == "udp":
                off = 7
            else:
                checkProt( "tcp", key, prot )
                off = 11

            # RPC version == 2, Call == 1 \
            rule += "  payload /" + ( "." * off ) \
                    + "\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01"  \
                    + convertRPCVal( app ) + convertRPCVal( version ) + convertRPCVal( proc ) \
                    + "/\n"

        elif key == "sameip":
            rule += "  same-ip\n"

        elif key == "seq":
            checkProt( "tcp", key, prot )
            rule += "  header tcp[4:4] == %d\n" % int( vallist[0] )

        elif key == "sid":
            pass

        elif key == "ttl":
            rule += convertVal( vallist[0], "header ip[8:1]" )
            pass

        elif key == "uricontent":
            for val in vallist:
#                if ALTERNATIVE_PATTERNS:
#                    re = "(|.*\\r\\n\\r\\n)(GET|HEAD|POST) *[^\\n]*%s" % patternToRE( val, "nocase" in options.keys(), 1, 0 )
#                else:
#                    re = "(GET|HEAD|POST) *[^\\n]*%s" % patternToRE( val, "nocase" in options.keys(), 1, 0 )
#
#                rule += "  payload /%s/\n" % re

                if ALTERNATIVE_PATTERNS:
                    re = "(|.*\\r\\n\\r\\n)(GET|HEAD|POST) *[^\\n]*%s" % patternToRE( val, "nocase" in options.keys(), 1, 0, 0, 0 )
                    rule += "  payload /%s/\n" % re
                else:
                    re = ".*%s" % patternToRE( val, "nocase" in options.keys(), 1, 0, 0, 0 )
                    rule += "  http /%s/\n" % re



                if REFile:
                    print >>REFile, re
                if PatternFile:
                    print >>PatternFile, val

        elif key == "within":
            # Ignore it here; we test for it when handling content
            pass

        else:
            warning( "Option %s not supported currently; ignored" % key )
            rule += "  # Not supported: %s: %s\n" % ( key, ",".join( vallist ) )

    for p in payloads:
        rule += "  payload /%s/\n" % p

    return rule

def parseAlert( rule ):

    global UnknownCount

    m = snortrule.match( rule )
    if not m:
        error( "Can\'t parse alert rule" )

    fields = m.groups()

    ( prot, srcip, srcport, dir, dstip, dstport ) = map( lambda s: s.lower(), fields[0:6] )

    options = parseRuleOptions( fields[6] )

    try:
        sid = options["sid"][0]
        try:
        if int( sid ) in  IgnoreSIDs:
            return
        id = "sid-" + sid
        except ValueError:
            id = sid

    except LookupError:
        id = "sid-unknown-%d" % UnknownCount
        UnknownCount += 1

    try:
        # Create rules depending on the direction; for "<>" we simply create
        # two rules

        if dir == "<>":
            id1 = id + "-a"
            id2 = id + "-b"
        else:
            id1 = id2 = id

        if dir != "->":
            rule = "signature %s {\n" % id1
            rule += convertHead( prot, dstip, dstport, srcip, srcport )
            rule += convertOptions( options, prot )
            print rule + "  }\n"

        if dir != "<-":
            rule = "signature %s {\n" % id2
            rule += convertHead( prot, srcip, srcport, dstip, dstport )
            rule += convertOptions( options, prot )
            print rule + "  }\n"

    except LookupError, e:
        error( "Can\'t convert rule: " + str( e ) )

#### Main

# Use some alternative patterns
ALTERNATIVE_PATTERNS = 0
# If PatternFile is a file, all content/uricontent patterns are written to it
PatternFile = 0
# If REFile is a file, all REs generated from content/uricontent patterns are written to it
REFile = 0
# Directories where to look for include's
INCPATH = [ "./" ]

# Number of Snort rules to output (no conversion to Bro format)
SnortRules=-1

# Total number of rules
TotalRules=0

def openInputFile( filename ):

    for i in INCPATH:
        try:
            fullpath =  os.path.join( i, filename )
            file = open( fullpath )
            print >>sys.stderr, "Reading", fullpath
            return file
        except IOError:
            pass

    error( "Can\'t find file %s" % filename )

def usage():
    print >>sys.stderr, "Usage: snort2bro [-I <includedir>] [-X] [-P]"
    print >>sys.stderr
    print >>sys.stderr, "    -I <dir> : Add dir to search path for Snort\'s include statement"
    print >>sys.stderr, "    -X       : Produce some alternate REs"
    print >>sys.stderr, "    -P       : Write all patterns to patterns.txt and "
    print >>sys.stderr, "               all generared REs to res.txt"
    print >>sys.stderr, "    -S <n>   : No conversion; just print out one big Snort config file"
    print >>sys.stderr, "               containing the first <n> rules"

    print >>sys.stderr
    sys.exit( 1 )

try:
    options, rest = getopt.getopt( sys.argv[1:], "XPI:S:" )
except:
    usage()

for( opt, arg ) in options:
    if opt == "-X":
        ALTERNATIVE_PATTERNS = 1

    if opt == "-P":
        PatternFile = open( "patterns.txt", "w" );
        REFile = open( "res.txt", "w" );

    if opt == "-I":
        INCPATH += [ arg, ]

    if opt == "-S":
        SnortRules = int( arg )

if len( rest ):
    Inputs = [ ( openInputFile( file ), 0 ) for file in rest ]
else:
    Inputs = [ ( sys.stdin, 0 ) ]

while Inputs:

    RawInputLine = Inputs[0][0].readline()
    if not RawInputLine:
        Inputs = Inputs[1:]
        continue

    # Increase line count
    Inputs[0] = ( Inputs[0][0], Inputs[0][1] + 1 )

    line = RawInputLine.strip()
    if not line or line.startswith( '#' ):
        continue

    line = expandVars( line )

    m = snortcmd.match( line )
    if not m:
        error( "Can\'t parse line " + line )

    cmd = m.group( 1 )
    args = m.group( 2 )

    if cmd == "include":
        Inputs = [ ( openInputFile( args ), 0 ) ] + Inputs
        continue

    if cmd == "var":
        parseVar( args )

    if cmd == "alert":
        TotalRules += 1

    if SnortRules >= 0:

        noprint = 0

        if cmd == "alert":

            for i in IgnoreSIDs:
                m = snortsid.search( args )
                if m and int( m.group( 1 ) ) == i:
                    noprint = 1

            if SnortRules == 0:
                noprint = 1

            if not noprint:
                SnortRules -= 1

        if not noprint:
            print RawInputLine,

        continue

    if cmd == "var":
        continue

    if cmd == "alert":
        parseAlert( args )
        continue

    if cmd == "output":
        continue

    if cmd == "preprocessor":
        continue

    if cmd == "config":
        # FIXME: Should we convert this to Bro?
        continue

    warning( "'%s' is not supported yet; line ignored." % cmd )


# Options -S returns the number of rules as exit code
if SnortRules >= 0:
    print >>sys.stderr, TotalRules
