ok

Mini Shell

Direktori : /opt/imunify360/venv/lib/python3.11/site-packages/im360/plugins/resident/
Upload File :
Current File : //opt/imunify360/venv/lib/python3.11/site-packages/im360/plugins/resident/graylist.py

"""
Manage Gray List
"""
import logging
import time

from peewee import DoesNotExist

from defence360agent.contracts.messages import MessageType, Reject
from defence360agent.contracts.plugins import (
    MessageSink,
    MessageSource,
    expect,
)
from defence360agent.model import instance
from defence360agent.model.simplification import run_in_executor
from defence360agent.utils import timeit
from im360.internals import geo
from im360.model.firewall import IgnoreList, IPList
from im360.utils.net import IPNetwork, pack_ip_network, unpack_ip_network
from im360.utils.validate import IP

logger = logging.getLogger(__name__)


class ManageGrayList(MessageSink, MessageSource):
    PROCESSING_ORDER = MessageSink.ProcessingOrder.GRAYLIST_DB_FIXUP

    async def create_sink(self, loop):
        self._loop = loop

    async def create_source(self, loop, sink):
        self._loop = loop
        self._sink = sink

    def _cleanup_ignore_list(self, net):
        """
        Removes *net* and its subnets from ignore list
        :return: list of nets removed
        """
        to_unblock = list(IgnoreList.subnets(net))
        IgnoreList.remove(to_unblock)
        return to_unblock

    def _process_alert(self, ip: IPNetwork, expiration: int, deep: int):
        listname = IPList.GRAY  # alert puts in GRAY list by definition
        assert expiration  # alert should always have finite expiration
        if expiration <= time.time():
            raise Reject("Already expired")

        unblocklist = []

        # 1. Cleanup exact match from GRAY_SPLASHSCREEN
        try:
            existing = IPList.get(ip=ip, listname=IPList.GRAY_SPLASHSCREEN)
        except DoesNotExist:
            pass
        else:
            if existing.lives_less(expiration) or not existing.lives_longer(
                expiration
            ):
                unblocklist.append(
                    (existing.ip_network, IPList.GRAY_SPLASHSCREEN)
                )
                existing.delete_instance()

        # 2. Remove from ignore list
        unblocklist.extend(
            (net, IPList.IGNORE) for net in self._cleanup_ignore_list(ip)
        )

        # 3. Store IP in DB
        existing, created = IPList.get_or_create(
            ip=ip,
            listname=listname,
            defaults=dict(
                expiration=expiration,
                deep=deep,
                manual=False,
                comment="Automatically blocked due to distributed attack",
                imported_from="Imunify360",
            ),
        )
        if not created:
            existing.update_properties(expiration, deep, listname)
        else:
            with geo.reader() as geo_reader:
                country = geo_reader.get_id(ip)
            existing.country = country
            existing.save()

        return dict(
            blocklist={(ip, listname): dict(expiration=expiration)},
            unblocklist=unblocklist,
        )

    @expect(MessageType.SensorAlert)
    async def add_to_db(self, message):
        """On alert, add attackers' ip/net to db's GRAY iplist or update
        expiry date for an existing GRAY iplist entry.

        Whitelist is checked in separate plugin

        No check existing supernets.

        GRAY subnets of the given net are not removed from the db!

        BLACK subnets are _not_ removed (whether they are added
        manually or not).

        GRAY_SPLASHSCREEN subnets /that expire earlier than ip's expiry
        date/ are removed from the db /only if the ip is added above/.

        Ipsets/webshield are updated elsewhere.

        """

        def process():
            with instance.db.atomic():
                return self._process_alert(
                    ip=message["attackers_ip"],
                    expiration=message["properties"]["expiration"],
                    deep=message["properties"]["deep"],
                )

        await self._sink.process_message(
            MessageType.BlockUnblockList(
                await run_in_executor(
                    self._loop,
                    process,
                )
            )
        )

    @expect(MessageType.ClientUnblock)
    async def delete_from_db(self, message):
        """
        Spec [1]:
        for l in ["GRAY", "GRAY_SPLASHSCREEN"]:
            existing = search_exactly(net, l)
            if existing:
                remove(net, l)

            supernets = search(net, l)
            if supernets:
                for n in search_subnets(net, "IGNORE"):
                    remove(net, "IGNORE")
                add(net, "IGNORE")
                return
        [1]: https://gerrit.cloudlinux.com/#/c/61260/18/src/handbook/message_processing/local_unblock.py
        """  # noqa
        ip = message["attackers_ip"]

        blocklist = {}
        unblocklist = []
        affected_listnames = [IPList.GRAY, IPList.GRAY_SPLASHSCREEN]

        blocked_in_any_supernet = bool(
            [
                supernet
                for supernet in IPList.find_closest_ip_nets(
                    ip, listname=affected_listnames, limit=1
                )
                if supernet.ip_network != ip
            ]
        )
        existing_ignored_subnets = list(IgnoreList.subnets(ip))

        if blocked_in_any_supernet:
            await run_in_executor(
                self._loop, lambda: IgnoreList.get_or_create(ip=ip)
            )
            blocklist[(message["attackers_ip"], IPList.IGNORE)] = {
                "expiration": IPList.NEVER
            }
        else:
            await run_in_executor(
                self._loop,
                lambda: self._delete_from_db(
                    ip,
                    ignore_list=existing_ignored_subnets,
                    listname=affected_listnames,
                ),
            )
            unblocklist = [
                (message["attackers_ip"], listname)
                for listname in affected_listnames
            ]
            for ip in existing_ignored_subnets:
                unblocklist.append((ip, IPList.IGNORE))

        await self._sink.process_message(
            MessageType.BlockUnblockList(
                {
                    "blocklist": blocklist,
                    "unblocklist": unblocklist,
                }
            )
        )

    @staticmethod
    def _delete_from_db(ip, ignore_list=None, *, listname=None):
        if listname is None:
            listname = [IPList.GRAY, IPList.GRAY_SPLASHSCREEN, IPList.BLACK]
        elif isinstance(listname, str):
            listname = [listname]
        if ignore_list is None:
            ignore_list = []
        deleted = IPList.delete_from_list(
            ip=ip, listname=listname, manual=False
        )

        if deleted:
            logger.debug("IP %s is unchecked from DB", ip)
        else:
            logger.debug("IP %s is not in DB", ip)

        if ignore_list:
            # unblock all subnet, we should flush all ignored ips
            IgnoreList.remove(ignore_list)

    @expect(MessageType.SynclistResponse)
    async def process_synclist(self, message):
        def process():
            with instance.db.atomic():
                return self._process_synclist(message)

        await self._sink.process_message(
            await run_in_executor(
                self._loop,
                process,
            )
        )

    def _process_blocklist(self, blocklist, manual_blacklist):
        with timeit(
            "remove expired and manually blacklisted ips from blocklist",
            logger,
        ):
            _blocklist = {
                ip: prop
                for ip, prop in blocklist.items()
                if (
                    ip not in manual_blacklist
                    and (IPList.lives_longer_prop(prop, time.time()))
                )
            }
        if not _blocklist:
            return {}, []
        to_block, to_unblock = [], []

        with timeit("filter exact matched nets with greater ttl", logger):
            for ip in IPList.find_ips_with_later_expiration(_blocklist):
                _blocklist.pop(ip)

        with geo.reader() as geo_reader:
            for ip, properties in _blocklist.items():
                listname = IPList.get_listname_from(properties)
                expiration = properties.get("expiration", IPList.NEVER)
                # if already blocked with higher priority/ttl - skip
                to_unblock.extend(
                    (net, IPList.IGNORE)
                    for net in self._cleanup_ignore_list(ip)
                )
                net, mask, version = pack_ip_network(ip)
                to_block.append(
                    dict(
                        ip=IP.ip_net_to_string(ip),
                        network_address=net,
                        netmask=mask,
                        version=version,
                        listname=listname,
                        expiration=expiration,
                        deep=properties.get("deep", 0),
                        manual=False,
                        country=geo_reader.get_id(ip),
                    )
                )
            with timeit("add records to iplist", logger):
                IPList.block_many(to_block)
        return (
            {
                (
                    unpack_ip_network(
                        item["network_address"],
                        item["netmask"],
                        item["version"],
                    ),
                    item["listname"],
                ): {"expiration": item["expiration"]}
                for item in to_block
            },
            to_unblock,
        )

    def _process_unblocklist(self, unblocklist, manual_blacklist):
        to_block, to_unblock = {}, []
        unblocklist = {
            ip: properties
            for ip, properties in unblocklist.items()
            if not any(
                ip.subnet_of(net)
                for net in manual_blacklist
                if ip.version == net.version
            )
        }

        def ips_to_remove_from_db():
            full_action_types_list_to_unblock = [
                IPList.listname2action_type(list_)
                for list_ in [
                    IPList.GRAY_SPLASHSCREEN,
                    IPList.GRAY,
                    IPList.BLACK,
                ]
            ]
            for ip, properties in unblocklist.items():
                action_type = (
                    properties.get("action_type") if properties else None
                )
                action_types_to_unblock = (
                    [action_type]
                    if action_type is not None
                    else full_action_types_list_to_unblock
                )
                for action_type in action_types_to_unblock:
                    # required only *action_type* field to unblock
                    yield (ip, {"action_type": action_type})

        to_unblock += IPList.remove_many(ips_to_remove_from_db())

        return to_block, to_unblock

    def _process_synclist(self, message):
        manual_blacklist = set(
            IPList.fetch_ipnetwork_list(IPList.BLACK, manual=True)
        )
        blocklist, unblocklist = {}, []

        for method, list_ in [
            (self._process_blocklist, message["blocklist"]),
            (self._process_unblocklist, message["unblocklist"]),
        ]:
            with timeit("synclist %s" % method, logger):
                to_block, to_unblock = method(list_, manual_blacklist)
                blocklist.update(to_block)
                unblocklist.extend(to_unblock)

        return MessageType.BlockUnblockList(
            {
                "blocklist": blocklist,
                "unblocklist": unblocklist,
            }
        )

Zerion Mini Shell 1.0