"""Index analyzer plugin for Yeti indicators."""

import datetime
import json
import logging
import re
from typing import Dict, List, Optional, Set, Tuple, Union, Any

import yaml
from flask import current_app

try:
    from yeti.api import YetiApi
    from yeti import errors as yeti_errors

    YETI_AVAILABLE = True
except ImportError:
    YETI_AVAILABLE = False


from timesketch.lib import emojis, sigma_util
from timesketch.lib.analyzers import interface, manager

logger = logging.getLogger("timesketch.analyzers.yetiindicators")

TYPE_TO_EMOJI = {
    "malware": "SPIDER",
    "threat-actor": "SKULL",
    "intrusion-set": "SKULL",
    "tool": "HAMMER",
    "investigation": "FIRE",
    "campaign": "BOMB",
    "vulnerability": "SHIELD",
    "attack-pattern": "HIGH_VOLTAGE",
}

# Maps Yeti indicator locations to a Timesketch intelligence type
INDICATOR_LOCATION_MAPPING = {"filesystem": "fs_path"}

# Items tagged with this tag in Yeti will be ignored by the analyzers.
TIMESKETCH_MUTE_TAG = "timesketch:mute"

# Maps Yeti observable types to Timesketch observable types
OBSERVABLE_INTEL_MAPPING = {
    "path": "fs_path",
    "filename": "fs_path",
    "hostname": "hostname",
    "ipv4": "ipv4",
    "sha256": "hash_sha256",
    "sha1": "hash_sha1",
    "md5": "hash_md5",
    "generic": "message",
}

HIGH_SEVERITY_TYPES = {
    "malware",
    "threat-actor",
    "intrusion-set",
    "campaign",
    "vulnerability",
    "investigation",
}


def slugify(text: str) -> str:
    """Converts a string to a slug."""
    text = text.lower()
    text = re.sub(r"[^a-z0-9]", "-", text)
    text = re.sub(r"-+", "-", text)
    return text


class YetiBaseAnalyzer(interface.BaseAnalyzer):
    """Base class for Yeti indicator analyzers."""

    DEPENDENCIES = frozenset(["domain"])

    def run(self):
        """Entry point for the analyzer."""
        raise NotImplementedError

    def __init__(self, index_name, sketch_id, timeline_id=None):
        """Initialize the Analyzer.

        Args:
            index_name: OpenSearch index name
            sketch_id: The ID of the sketch.
            timeline_id: The ID of the timeline.
        """
        super().__init__(index_name, sketch_id, timeline_id=timeline_id)
        self.intel = {}
        self.api = None
        self.yeti_web_root = ""

        if not YETI_AVAILABLE:
            raise RuntimeError("The 'yeti-python' library is not installed.")

        yeti_api_root = current_app.config.get("YETI_API_ROOT")
        yeti_web_root = current_app.config.get("YETI_WEB_ROOT")

        if not yeti_api_root:
            raise RuntimeError("YETI_API_ROOT is not configured.")

        if yeti_api_root.endswith("/"):
            yeti_api_root = yeti_api_root[:-1]

        if yeti_web_root:
            self.yeti_web_root = yeti_web_root
        else:
            self.yeti_web_root = yeti_api_root.replace("/api/v2", "")

        if self.yeti_web_root.endswith("/"):
            self.yeti_web_root = self.yeti_web_root[:-1]

        yeti_api_key = current_app.config.get("YETI_API_KEY")
        tls_cert = current_app.config.get("YETI_TLS_CERTIFICATE")

        if not yeti_api_key:
            raise RuntimeError("YETI_API_KEY is not configured.")

        if tls_cert and yeti_api_root.startswith("https://"):
            self.api = YetiApi(yeti_api_root, tls_cert=tls_cert)
        else:
            self.api = YetiApi(yeti_api_root)

        try:
            self.api.auth_api_key(yeti_api_key)
        except yeti_errors.YetiAuthError as e:
            raise RuntimeError(f"Yeti authentication failed: {e}") from e

        self._intelligence_refs = set()
        self._intelligence_attribute = {"data": []}

    def mark_event(
        self, indicator: Dict, event: interface.Event, neighbors: List[Dict]
    ):
        """Annotate an event with data from indicators and neighbors.

        Tags with skull emoji, adds a comment to the event.
        Args:
            indicator: a dictionary representing a Yeti indicator object.
            event: a Timesketch sketch Event object.
            neighbors: a list of Yeti entities related to the indicator.
        """
        tags = set()
        msg = ""
        if indicator["root_type"] == "indicator":
            tags = {slugify(tag) for tag in indicator["relevant_tags"]}
            msg = f'Indicator match: "{indicator["name"]}" (ID: {indicator["id"]})\n'
        if indicator["root_type"] == "observable":
            tags = {slugify(tag["name"]) for tag in indicator["tags"]}
            msg = f'Observable match: "{indicator["value"]}" (ID: {indicator["id"]})\n'
        for neighbor in neighbors:
            tags.add(slugify(neighbor["name"]))
            emoji_name = TYPE_TO_EMOJI.get(neighbor["type"])
            event.add_emojis([emojis.get_emoji(emoji_name)])

        event.add_tags(list(tags))
        event.commit()

        if neighbors:
            msg += f"Related entities: {[neighbor['name'] for neighbor in neighbors]}"

        comments = {comment.comment for comment in event.get_comments()}
        if msg not in comments:
            event.add_comment(msg)
            event.commit()

    def add_intelligence_entry(
        self, indicator: Dict, event: interface.Event, entity: Dict
    ) -> None:
        """Adds an intelligence entry to the local intelligence attribute.

        For regex indicators, the intelligence will be the match that is
        extracted from the event. For observables, the full message event
        message attribute is used.

        Args:
            indicator: The Yeti indicator that matched the event.
            event: The Timesketch event that intelligence needs to be extracted
                from.
            entity: The Yeti Entity that is linked to the indicator.
        """
        intel_type = "other"
        match_in_sketch = None
        uri = ""

        if indicator["type"] == "regex":
            if "compiled_regexp" not in indicator:
                indicator["compiled_regexp"] = re.compile(indicator["pattern"])
            regexp = indicator["compiled_regexp"]
            match = regexp.search(event.source.get("message"))
            if match:
                match_in_sketch = match.group(0)
            if not match:
                return

            uri = f"{self.yeti_web_root}/indicators/{indicator['id']}"
            intel_type = INDICATOR_LOCATION_MAPPING.get(indicator["location"], "other")
            tags = indicator["relevant_tags"]

        if indicator["root_type"] == "observable":
            match_in_sketch = indicator["value"]
            intel_type = OBSERVABLE_INTEL_MAPPING.get(indicator["type"], "other")
            uri = f"{self.yeti_web_root}/observables/{indicator['id']}"
            tags = [tag["name"] for tag in indicator["tags"]]

        if not match_in_sketch:
            return

        if (match_in_sketch, uri) in self._intelligence_refs:
            return

        intel = {
            "externalURI": uri,
            "ioc": match_in_sketch,
            "tags": [slugify(tag) for tag in tags + [entity["name"]]],
            "type": intel_type,
        }

        self._intelligence_attribute["data"].append(intel)
        self._intelligence_refs.add((match_in_sketch, uri))

    def _merge_intelligence_attributes(self, attribute_values):
        """Merges multiple intelligence values that might have been stored."""
        data = []
        existing_refs = set()
        for value in attribute_values:
            for ioc in value["data"]:
                if ioc["externalURI"] in existing_refs:
                    continue
                data.append(ioc)
                existing_refs.add(ioc["externalURI"])
        return {"data": data}

    def get_intelligence_attribute(self) -> Tuple[Dict, Set[Tuple[str, str]]]:
        """Fetches the intelligence attribute from the database."""
        try:
            intelligence_attribute = self.sketch.get_sketch_attributes("intelligence")

            # In some cases, the intelligence attribute may be split into
            # multiple "values" due tu race conditions. Merge them if that's
            # the case. The API will return only the first value if the list
            # has 1 element, so this check is necessary.
            if isinstance(intelligence_attribute, list):
                intelligence_attribute = self._merge_intelligence_attributes(
                    intelligence_attribute
                )

            refs = {
                (ioc["ioc"], ioc["externalURI"])
                for ioc in intelligence_attribute["data"]
            }
            return intelligence_attribute, refs

        except ValueError:
            logging.debug(
                "Intelligence not set on sketch, will be created from scratch."
            )
        return {"data": []}, set()

    def save_intelligence(self) -> None:
        """Saves the intelligence attribute to the database."""
        # This is necessary to take into account changes that may
        # have been made by other runs of this analyzer or other analyzers
        # that add intelligence to the sketch.
        db_attribute, _ = self.get_intelligence_attribute()
        for ioc in db_attribute["data"]:
            if (ioc["ioc"], ioc["externalURI"]) not in self._intelligence_refs:
                self._intelligence_attribute["data"].append(ioc)

        attribute_string = json.dumps(self._intelligence_attribute)
        self.sketch.add_sketch_attribute(
            "intelligence",
            [attribute_string],
            ontology="intelligence",
            overwrite=True,
        )


class YetiGraphAnalyzer(YetiBaseAnalyzer):
    """Analyzer that leverages the Yeti graph.

    Selects entities, traverses graphs to find indicators or observables
    to look for.
    """

    # Entities of this type will be fetched from Yeti
    # Can optionally contain tags after a `:`
    # e.g. `malware:ransomware,tag2`
    _TYPE_SELECTOR: List[str]
    # Graph will be traversed from the entities looking for these types
    # of neighbors
    _TARGET_NEIGHBOR_TYPE: List[str] = []
    # If True, will save intelligence to the sketch
    _SAVE_INTELLIGENCE: bool = False

    # Number of hops to traverse from the entity
    _MAX_HOPS = 5
    # Direction to traverse the graph. One of {inbound, outbound, any}
    _DIRECTION = "inbound"

    def get_neighbors(
        self, yeti_object: Dict, max_hops: int, neighbor_types: List[str]
    ) -> List[Dict]:
        """Retrieves a list of neighbors associated to a given entity.

        Args:
          yeti_object: The Yeti object to get neighbors from.
          max_hops: The number of hops to traverse from the entity.
          neighbor_types: A list of Yeti object types to filter by.

        Returns:
          A list of dictionaries describing a Yeti object.
        """
        extended_id = f"{yeti_object['root_type']}/{yeti_object['id']}"
        try:
            results = self.api.search_graph(
                source=extended_id,
                graph="links",
                min_hops=1,
                max_hops=max_hops,
                direction=self._DIRECTION,
                include_original=False,
                target_types=neighbor_types,
                count=0,
            )
        except yeti_errors.YetiApiError as e:
            raise RuntimeError(
                f"Error {e.status_code} retrieving neighbors for "
                f"{extended_id} from Yeti: {e}"
            ) from e

        neighbors = {}
        for neighbor in results.get("vertices", {}).values():
            if (
                neighbor["type"] in neighbor_types
                or neighbor["root_type"] in neighbor_types
            ):
                neighbors[neighbor["id"]] = neighbor
        return neighbors

    def get_entities(self, type_selector: List[str]) -> Dict[str, dict]:
        """Fetches Entities with a certain tag on Yeti.

        Args:
            type_selector: Use these type selectors to search entities. Type
              Selectors have a TYPE:TAG1,TAG2 format.

        Returns:
            A dictionary of entities obtained from Yeti, keyed by entity ID.
        """
        all_entities = {}
        for selector in type_selector:
            tags = []
            entity_type = selector
            if ":" in selector:
                parts = selector.split(":", 1)
                entity_type = parts[0]
                tags = parts[1].split(",")

            if not entity_type:
                entity_type = None

            try:
                results = self.api.search_entities(
                    entity_type=entity_type, tags=tags, count=0
                )
                entities = {item["id"]: item for item in results}
                all_entities.update(entities)
            except yeti_errors.YetiApiError as e:
                raise RuntimeError(
                    f"Error {e.status_code} retrieving entities from Yeti: {e}"
                ) from e

        return all_entities

    def build_query_from_observable(self, observable: Dict) -> Dict:
        """Builds a query DSL from a Yeti observable.

        Uses a wildcard query on the keyword field of message, given that
        we want to surface all events that contain the verbatim observable
        value.

        Args:
            observable: a dictionary representing a Yeti observable object.

        Returns:
            A dictionary representing a query DSL.
        """
        escaped = observable["value"].replace("\\", "\\\\")
        field = "message.keyword"
        search = f"*{escaped}*"

        if observable["type"] == "sha256":
            field = "sha256_hash"
            search = escaped

        return {
            "query": {
                "wildcard": {
                    field: {
                        "value": search,
                        "case_insensitive": True,
                    }
                },
            },
        }

    def build_query_from_regexp(self, indicator: Dict) -> Dict:
        """Builds a query DSL from a Yeti Regex indicator.

        Args:
            indicator: a dictionary representing a Yeti Regex indicator object.

        Returns:
            A dictionary representing a query DSL.
        """
        field = ""
        if indicator["location"] == "registry":
            field = "key_path.keyword"
        elif indicator["location"] == "filesystem":
            field = "filename.keyword"
        else:
            field = "message.keyword"

        return {
            "query": {
                "bool": {
                    "should": [
                        {
                            "regexp": {
                                field: {
                                    "value": f".*{indicator['pattern']}.*",
                                    "case_insensitive": True,
                                }
                            }
                        },
                        {
                            "regexp": {
                                "display_name.keyword": {
                                    "value": f".*{indicator['pattern']}.*",
                                    "case_insensitive": True,
                                }
                            }
                        },
                    ]
                }
            }
        }

    def build_query_from_sigma(self, indicator: Dict) -> Optional[Dict]:
        """Builds a query DSL from a Yeti Sigma indicator.

        Args:
            indicator: a dictionary representing a Yeti Sigma indicator object.

        Returns:
            A dictionary representing a query DSL.
        """
        try:
            parsed_sigma = sigma_util.parse_sigma_rule_by_text(
                indicator["pattern"], sanitize=False
            )
        except yaml.scanner.ScannerError as exception:
            logging.error(
                "Error parsing Sigma rule %s: %s",
                indicator["id"],
                str(exception),
            )
            return None
        return {"query": {"query_string": {"query": parsed_sigma["search_query"]}}}

    def run(self):
        """Entry point for the analyzer.

        Returns:
            String with summary of the analyzer result.
        """
        total_matches = 0
        total_processed = 0
        total_failed = 0
        entities_found = set()
        matching_indicators = set()
        priority = "NOTE"

        self.save_intelligence()

        if self._SAVE_INTELLIGENCE:
            self._intelligence_attribute, self._intelligence_refs = (
                self.get_intelligence_attribute()
            )

        entities = self.get_entities(type_selector=self._TYPE_SELECTOR)
        for entity in entities.values():
            if TIMESKETCH_MUTE_TAG in entity.get("tags", []):
                logging.debug(
                    "Skipping entity %s because it is tagged with %s",
                    entity["name"],
                    TIMESKETCH_MUTE_TAG,
                )
                continue
            indicators = self.get_neighbors(
                entity,
                max_hops=self._MAX_HOPS,
                neighbor_types=self._TARGET_NEIGHBOR_TYPE,
            )
            logging.debug(
                "Found %d neighbor indicators for %s",
                len(indicators),
                entity["name"],
            )

            for indicator in indicators.values():
                if TIMESKETCH_MUTE_TAG in indicator.get("tags", []):
                    logging.debug(
                        "Skipping indicator %s because it is tagged with %s",
                        indicator["name"],
                        TIMESKETCH_MUTE_TAG,
                    )
                    continue
                query_dsl = None
                if indicator["root_type"] == "observable":
                    query_dsl = self.build_query_from_observable(indicator)
                elif indicator["type"] == "regex":
                    query_dsl = self.build_query_from_regexp(indicator)
                elif indicator["type"] == "sigma":
                    query_dsl = self.build_query_from_sigma(indicator)
                elif indicator["type"] == "query":
                    if indicator["query_type"] == "opensearch":
                        query_dsl = {
                            "query": {"query_string": {"query": indicator["pattern"]}}
                        }
                if not query_dsl:
                    logging.warning(
                        "Unsupported indicator type, skipping: %s (%s)",
                        indicator["type"],
                        indicator["root_type"],
                    )
                    continue
                events = self.event_stream(
                    query_dsl=query_dsl, return_fields=["message"], scroll=False
                )
                logging.debug("Searching for %s", str(query_dsl))
                try:
                    indicator_match = 0
                    start = datetime.datetime.now()
                    for event in events:
                        total_matches += 1
                        self.mark_event(indicator, event, [entity])
                        matching_indicators.add(indicator["id"])
                        if entity["type"] in HIGH_SEVERITY_TYPES:
                            priority = "HIGH"
                            if self._SAVE_INTELLIGENCE:
                                self.add_intelligence_entry(indicator, event, entity)

                        entities_found.add(f"{entity['name']}:{entity['type']}")
                        indicator_match += 1
                    logging.debug(
                        "Found %s matches for indicator %s in %s",
                        indicator_match,
                        indicator["id"],
                        str(datetime.datetime.now() - start),
                    )
                except Exception as exception:  # pylint: disable=broad-except
                    # No matter the exception, we don't want to stop the
                    # analyzer. Errors are logged and reported in the UI.
                    logging.error(
                        "Error processing events in sketch %s for indicator %s: %s",
                        self.sketch.id,
                        indicator["id"],
                        str(exception),
                    )
                    total_failed += 1

                total_processed += 1

        self.output.result_status = "SUCCESS"
        self.output.result_priority = priority

        if not total_matches:
            note = (
                f"0/{total_processed} indicators were found in the "
                f"timeline ({total_failed} failed)"
            )
            self.output.result_summary = note
            return str(self.output)

        for entity in entities_found:
            name, _type = entity.split(":")
            self.sketch.add_view(
                f"Indicator matches for {name} ({_type})",
                self.NAME,
                query_string=f'tag:"{slugify(name)}"',
            )

        if self._SAVE_INTELLIGENCE:
            self.save_intelligence()

        success_note = (
            f"{total_matches} events matched "
            f"{len(matching_indicators)}/{total_processed} "
            f"indicators ({total_failed} failed).\n\n"
            f"Entities found: {', '.join(entities_found)}"
        )
        self.output.result_summary = success_note

        return str(self.output)


class YetiTriageIndicators(YetiGraphAnalyzer):
    """Analyzer for Yeti triage indicators."""

    NAME = "yetitriageindicators"
    DISPLAY_NAME = "Yeti forensics triage indicators"
    DESCRIPTION = (
        "Mark triage events using forensics indicators from Yeti. Will fetch"
        ' all attack-patterns tagged with the "triage" tag, and traverse the'
        " graph searching for regex indicators."
        " {attack-pattern:triage} → {regex, query}"
    )

    DEPENDENCIES = frozenset(["domain"])

    _TYPE_SELECTOR = ["attack-pattern:triage"]
    _TARGET_NEIGHBOR_TYPE = ["regex", "query"]
    _DIRECTION = "inbound"
    _SAVE_INTELLIGENCE = False


class YetiBadnessIndicators(YetiGraphAnalyzer):
    """Analyzer for Yeti indicators related to attacks."""

    NAME = "yetibadnessindicators"
    DISPLAY_NAME = "Yeti badness indicators"
    DESCRIPTION = (
        "Mark badness-related events using indicators from "
        "Yeti. Will fetch all malware and exploit-tagged attack-pattern "
        "entities and traverse the graph searching for regex indicators,"
        " and save matches to the sketch's intelligence attribute. "
        "{malware, attack-pattern:exploit} ← {regex, query}"
    )

    DEPENDENCIES = frozenset(["domain"])

    _TYPE_SELECTOR = ["malware", "attack-pattern:exploit"]
    _TARGET_NEIGHBOR_TYPE = ["regex", "query"]
    _DIRECTION = "inbound"
    _SAVE_INTELLIGENCE = True


class YetiLOLBASIndicators(YetiGraphAnalyzer):
    """Analyzer for Yeti LOLBAS indicators."""

    # YetiTriageIndicators gives extra context around execution, etc.
    DEPENDENCIES = frozenset(["yetitriageindicators"])

    NAME = "yetilolbasindicators"
    DISPLAY_NAME = "Yeti LOLBAS indicators"
    DESCRIPTION = (
        "Mark events that match Yeti indicators linked to tools"
        " that are tagged `lolbas`. {tool:lobas} ← {sigma, query, regex}"
    )

    _TYPE_SELECTOR = ["tool:lolbas"]
    _TARGET_NEIGHBOR_TYPE = ["sigma", "query", "regex"]
    _SAVE_INTELLIGENCE = True
    _DIRECTION = "inbound"
    _MAX_HOPS = 1


class YetiInvestigations(YetiGraphAnalyzer):
    """Analyzer for Yeti investigation-related indicators."""

    NAME = "yetiinvestigations"
    DISPLAY_NAME = "Yeti Investigations intelligence"
    DESCRIPTION = (
        "Mark events that match Yeti investigation indicators and observables."
        " {investigation} ← {indicators, observables}"
    )

    _TYPE_SELECTOR = ["investigation"]
    _TARGET_NEIGHBOR_TYPE = ["sigma", "query", "regex", "observable"]
    _SAVE_INTELLIGENCE = True
    _DIRECTION = "any"
    _MAX_HOPS = 1


class YetiKeywords(YetiGraphAnalyzer):
    """Analyzer for Yeti investigation-related indicators."""

    NAME = "yetikeywords"
    DISPLAY_NAME = "Yeti Keyword matching"
    DESCRIPTION = (
        "Mark events that match Yeti keywords linked to attack patterns."
        " {attack-pattern:keywords} ← {observable}"
    )

    _TYPE_SELECTOR = ["attack-pattern:keywords"]
    _TARGET_NEIGHBOR_TYPE = ["observable"]
    _SAVE_INTELLIGENCE = True
    _DIRECTION = "any"
    _MAX_HOPS = 1


class YetiBloomChecker(YetiBaseAnalyzer):
    """Analyzer for Yeti bloom filter checker."""

    NAME = "yetibloomchecker"
    DISPLAY_NAME = "Yeti Bloom filter checker"
    DESCRIPTION = "Check if hashes in the timeline are present in Yeti's bloom filter."

    def run_composite_aggregation(
        self, hashmap: set[str], after_key: dict[str, Any] = None
    ) -> tuple[set[str], Union[Dict, None]]:
        """Runs a composite aggregation of hashes on a sketch.

        Accumulates hashes in the hashmap parameter:

        Args:
            hashmap: A set of hashes to be checked.
            after_key: The key to continue the aggregation from, if needed.

        Returns:
            A tuple containing the updated hashmap and the after_key for
            pagination.
        """
        agg_name = f"my_composite_agg_{self.index_name}"
        agg_dsl = {
            "size": 0,
            "query": {
                "bool": {"filter": [{"term": {"__ts_timeline_id": self.timeline_id}}]}
            },
            "aggs": {
                agg_name: {
                    "composite": {
                        "size": 10000,
                        "sources": [
                            {"sha256_hash": {"terms": {"field": "sha256_hash.keyword"}}}
                        ],
                    }
                }
            },
        }

        if after_key:
            agg_dsl["aggs"][agg_name]["composite"]["after"] = after_key

        result = self.datastore.search(
            sketch_id=self.sketch.id,
            indices=[self.index_name],
            query_dsl=agg_dsl,
        )
        after_key = result["aggregations"][agg_name].get("after_key")
        buckets = result["aggregations"][agg_name]["buckets"]
        for r in buckets:
            hashmap.add(r["key"]["sha256_hash"])

        return hashmap, after_key

    def run(self):
        hashmap = set()
        after = None
        while True:
            _, after = self.run_composite_aggregation(hashmap, after_key=after)
            if not after:
                break

        try:
            bloom_hits = self.api.search_bloom(list(hashmap))
        except yeti_errors.YetiApiError as e:
            return f"Error getting bloom hits from Yeti: {e}"
        except RuntimeError as exception:
            return str(exception)
        hit_dict = {hit["value"]: hit["hits"] for hit in bloom_hits}

        tagged = 0
        for event in self.event_stream(
            query_string="_exists_:sha256_hash", return_fields=["sha256_hash"]
        ):
            sha256_hash = event.source.get("sha256_hash")
            if sha256_hash in hit_dict:
                tagged += 1
                tags = hit_dict[sha256_hash]
                event.add_tags([f"bloom:{tag}" for tag in tags])
                event.commit()

        msg = (
            f"Bloom filter check completed. {len(hashmap)} hashes checked,"
            f" {len(hit_dict)} hits found, {tagged} events tagged."
        )
        self.output.result_summary = msg
        self.output.result_status = "SUCCESS"
        return str(self.output)


manager.AnalysisManager.register_analyzer(YetiBadnessIndicators)
manager.AnalysisManager.register_analyzer(YetiBloomChecker)
manager.AnalysisManager.register_analyzer(YetiInvestigations)
manager.AnalysisManager.register_analyzer(YetiKeywords)
manager.AnalysisManager.register_analyzer(YetiLOLBASIndicators)
manager.AnalysisManager.register_analyzer(YetiTriageIndicators)
