# Copyright (C) 2017 Open Information Security Foundation
#
# You can copy, redistribute or modify this Program under the terms of
# the GNU General Public License version 2 as published by the Free
# Software Foundation.
#
# 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
# version 2 along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

# This module contains functions for matching rules for disabling,
# enabling, converting to drop or modification.

import re
import os.path
import logging
import shlex
import fnmatch
import suricata.update.rule


logger = logging.getLogger()


class AllRuleMatcher(object):
    """Matcher object to match all rules. """

    def match(self, rule):
        return True

    @classmethod
    def parse(cls, buf):
        if buf.strip() == "*":
            return cls()
        return None


class ProtoRuleMatcher:
    """A rule matcher that matches on the protocol of a rule."""

    def __init__(self, proto):
        self.proto = proto

    def match(self, rule):
        return rule.proto == self.proto


class IdSetRuleMatcher(object):
    """Matcher object that matches on a set of rule SIDs.

    This matcher is not directly parsed, but instead constructed after
    loading individual IdRuleMatchers and consolidated into one
    matcher that is much faster.

    """
    def __init__(self):
        self.sids = {}

    def add(self, generatorId, signatureId):
        self.sids[(generatorId, signatureId)] = True

    def match(self, rule):
        key = (rule.gid, rule.sid)
        return key in self.sids


class IdRuleMatcher(object):
    """Matcher object to match an idstools rule object by its signature
    ID."""

    def __init__(self, generatorId=None, signatureId=None, rev=None):
        self.signatureIds = []
        if generatorId and signatureId and rev is not None:
            self.signatureIds.append((generatorId, signatureId, rev))
        elif generatorId and signatureId:
            self.signatureIds.append((generatorId, signatureId))

    def match(self, rule):
        for sig in self.signatureIds:
            if len(sig) == 3:
                if sig[0] == rule.gid and sig[1] == rule.sid and sig[2] == rule.rev:
                    return True
            elif len(sig) == 2:
                if sig[0] == rule.gid and sig[1] == rule.sid:
                    return True
        return False

    @classmethod
    def parse(cls, buf):
        matcher = cls()

        for entry in buf.split(","):
            entry = entry.strip()

            parts = entry.split(":")
            if not parts:
                return None

            # The first part musth parse as a number, if not, its
            # not a signature ID expression.
            try:
                int(parts[0])
            except:
                return None

            if len(parts) == 1:
                try:
                    signatureId = int(parts[0])
                    matcher.signatureIds.append((1, signatureId))
                except:
                    return None
            elif len(parts) == 2:
                try:
                    generatorId = int(parts[0])
                    signatureId = int(parts[1])
                    matcher.signatureIds.append((generatorId, signatureId))
                except:
                    return None
            elif len(parts) == 3:
                try:
                    generatorId = int(parts[0])
                    signatureId = int(parts[1])
                    rev = int(parts[2])
                    matcher.signatureIds.append((generatorId, signatureId, rev))
                except:
                    return None

        # If no valid signature IDs were parsed, return None
        if not matcher.signatureIds:
            return None

        return matcher


class FilenameMatcher(object):
    """Matcher object to match a rule by its filename. This is similar to
    a group but has no specifier prefix.
    """

    def __init__(self, pattern):
        self.pattern = pattern

    def match(self, rule):
        if hasattr(rule, "group") and rule.group is not None:
            return fnmatch.fnmatch(rule.group, self.pattern)
        return False

    @classmethod
    def parse(cls, buf):
        if buf.startswith("filename:"):
            try:
                group = buf.split(":", 1)[1]
                return cls(group.strip())
            except:
                pass
        return None


class GroupMatcher(object):
    """Matcher object to match an idstools rule object by its group (ie:
    filename).

    The group is just the basename of the rule file with or without
    extension.

    Examples:
    - emerging-shellcode
    - emerging-trojan.rules

    """

    def __init__(self, pattern):
        self.pattern = pattern

    def match(self, rule):
        if hasattr(rule, "group") and rule.group is not None:
            if fnmatch.fnmatch(os.path.basename(rule.group), self.pattern):
                return True
            # Try matching against the rule group without the file
            # extension.
            if fnmatch.fnmatch(
                    os.path.splitext(
                        os.path.basename(rule.group))[0], self.pattern):
                return True
        return False

    @classmethod
    def parse(cls, buf):
        if buf.startswith("group:"):
            try:
                logger.debug("Parsing group matcher: %s" % (buf))
                group = buf.split(":", 1)[1]
                return cls(group.strip())
            except:
                pass
        if buf.endswith(".rules"):
            return cls(buf.strip())
        return None


class ReRuleMatcher(object):
    """Matcher object to match an idstools rule object by regular
    expression."""

    def __init__(self, pattern):
        self.pattern = pattern

    def match(self, rule):
        if self.pattern.search(rule.raw):
            return True
        return False

    @classmethod
    def parse(cls, buf):
        if buf.startswith("re:"):
            try:
                logger.debug("Parsing regex matcher: %s" % (buf))
                patternstr = buf.split(":", 1)[1].strip()
                pattern = re.compile(patternstr, re.I)
                return cls(pattern)
            except:
                pass
        return None


class MetadataRuleMatch(object):
    """ Matcher that matches on key/value style metadata fields. Case insensitive. """

    def __init__(self, key, value):
        self.key = key
        self.value = value

    def match(self, rule):
        for entry in rule.metadata:
            parts = entry.strip().split(" ", 1)
            if parts[0].strip().lower() == self.key and parts[1].strip().lower() == self.value:
                return True
        return False

    @classmethod
    def parse(cls, buf):
        if buf.startswith("metadata:"):
            buf = buf.split(":", 1)[1].strip()
            parts = buf.split(" ", 1)
            if len(parts) == 2:
                key = parts[0].strip().lower()
                val = parts[1].strip().lower()
                return cls(key, val)
        return None


class ModifyRuleFilter(object):
    """Filter to modify an idstools rule object.

    Important note: This filter does not modify the rule inplace, but
    instead returns a new rule object with the modification.
    """

    def __init__(self, matcher, pattern, repl):
        self.matcher = matcher
        self.pattern = pattern
        self.repl = repl

    def match(self, rule):
        return self.matcher.match(rule)

    def run(self, rule):
        modified_rule = self.pattern.sub(self.repl, rule.format())
        parsed = suricata.update.rule.parse(modified_rule, rule.group)
        if parsed is None:
            logger.error("Modification of rule %s results in invalid rule: %s",
                         rule.idstr, modified_rule)
            return rule
        return parsed

    @classmethod
    def parse(cls, buf):
        tokens = shlex.split(buf)
        if len(tokens) == 3:
            matchstring, a, b = tokens
        elif len(tokens) > 3 and tokens[0] == "modifysid":
            matchstring, a, b = tokens[1], tokens[2], tokens[4]
        else:
            raise Exception("Bad number of arguments.")
        matcher = parse_rule_match(matchstring)
        if not matcher:
            raise Exception("Bad match string: %s" % (matchstring))
        pattern = re.compile(a)

        # Convert Oinkmaster backticks to Python.
        b = re.sub(r"\$\{(\d+)\}", "\\\\\\1", b)

        return cls(matcher, pattern, b)


class DropRuleFilter(object):
    """ Filter to modify an idstools rule object to a drop rule. """

    def __init__(self, matcher):
        self.matcher = matcher

    def match(self, rule):
        if rule["noalert"]:
            return False
        return self.matcher.match(rule)

    def run(self, rule):
        drop_rule = suricata.update.rule.parse(re.sub(
            r"^\w+", "drop", rule.raw), rule.group)
        drop_rule.enabled = rule.enabled
        return drop_rule

class AddMetadataFilter(object):

    def __init__(self, matcher, key, val):
        self.matcher = matcher
        self.key = key
        self.val = val

    def match(self, rule):
        return self.matcher.match(rule)

    def run(self, rule):
        new_rule_string = re.sub(r";\s*\)$", "; metadata: {} {};)".format(self.key, self.val), rule.format())
        new_rule = suricata.update.rule.parse(new_rule_string, rule.group)
        if not new_rule:
            logger.error("Rule is not valid after adding metadata: [{}]: {}".format(rule.idstr, new_rule_string))
            return rule
        return new_rule

    @classmethod
    def parse(cls, buf):
        try:
            command, match_string, key, val = shlex.split(buf)
        except:
            raise Exception("metadata-add: invalid number of arguments")
        matcher = parse_rule_match(match_string)
        if not matcher:
            raise Exception("Bad match string: %s" % (match_string))
        return cls(matcher, key, val)


def parse_rule_match(match):
    matcher = AllRuleMatcher.parse(match)
    if matcher:
        return matcher

    matcher = IdRuleMatcher.parse(match)
    if matcher:
        return matcher

    matcher = ReRuleMatcher.parse(match)
    if matcher:
        return matcher

    matcher = FilenameMatcher.parse(match)
    if matcher:
        return matcher

    matcher = GroupMatcher.parse(match)
    if matcher:
        return matcher

    matcher = MetadataRuleMatch.parse(match)
    if matcher:
        return matcher

    return None
