Skip to content

Crash - Cannot import name 'FixtureDef' from 'pytest' #130

@coffeegist

Description

@coffeegist

Bug description

When parsing the following a.py:

"""Data source abstractions for BOFHound parsing pipeline."""

import sys
from abc import ABC, abstractmethod
import logging
from typing import Iterator, List, AsyncIterator
from typing_extensions import override
import glob
import os
from pathlib import Path
from mythic import mythic
from syncer import sync
import base64

from bofhound.logger import logger



class DataSource(ABC):
    """Abstract base class for data sources that provide lines to parse."""

    @abstractmethod
    def get_data_streams(self) -> Iterator['DataStream']:
        """Return an iterator of data streams to process."""
        pass


class DataStream(ABC):
    """Abstract base class representing a single stream of data to parse."""

    @property
    @abstractmethod
    def identifier(self) -> str:
        """Unique identifier for this data stream (e.g., filename, callback ID)."""
        pass

    @abstractmethod
    def lines(self) -> Iterator[str]:
        """Return an iterator of lines from this data stream."""
        pass

    def __str__(self) -> str:
        return self.identifier

class FileDataSource(DataSource):
    """Data source that reads from local files."""

    def __init__(self, input_path: str, filename_pattern: str = "*.log"):
        self.input_path = input_path
        self.filename_pattern = filename_pattern

    def get_data_streams(self) -> Iterator['FileDataStream']:
        """Get file-based data streams."""
        if os.path.isfile(self.input_path):
            yield FileDataStream(self.input_path)
        elif os.path.isdir(self.input_path):
            pattern = f"{self.input_path}/**/{self.filename_pattern}"
            files = glob.glob(pattern, recursive=True)
            files.sort(key=os.path.getmtime)

            for file_path in files:
                yield FileDataStream(file_path)
        else:
            raise ValueError(f"Input path does not exist: {self.input_path}")


class FileDataStream(DataStream):
    """Data stream that reads from a local file."""

    def __init__(self, file_path: str):
        self.file_path = file_path

    @property
    def identifier(self) -> str:
        return self.file_path

    def lines(self) -> Iterator[str]:
        """Read lines from the file."""
        with open(self.file_path, 'r', encoding='utf-8') as f:
            for line in f:
                yield line.rstrip('\n\r')


class MythicCallback:
    """
    Quick and dirty class to hold Mythic callback information
    and allow print statments from the main logic to still work
    """
    def __init__(self, callback, mythic_instance=None):
        self.callback_id = callback["id"]
        self.display_id = callback["display_id"]
        self.domain = callback["domain"]
        self.user = callback["user"]
        self.host = callback["host"]
        self.uuid = callback["agent_callback_id"]
        self._mythic_instance = mythic_instance

    def __repr__(self):
        return f"Mythic callback {self.callback_id} [{self.uuid}]"


class MythicDataSource(DataSource):
    """Data source that fetches data from Mythic server."""

    def __init__(self, mythic_server: str, mythic_token: str):
        self.mythic_server = mythic_server
        self.mythic_token = mythic_token
        self._mythic_instance = None

    def _connect(self):
        logger.debug("Logging into Mythic...")
        try:
            self._mythic_instance = sync(mythic.login(
                apitoken=self.mythic_token,
                server_ip=self.mythic_server,
                server_port=7443,
                timeout=-1,
                logging_level=logging.CRITICAL,
            ))
        except Exception as e:
            logger.error("Error logging into Mythic")
            logger.error(e)
            sys.exit(-1)

        logger.debug("Logged into Mythic successfully")


    def _get_callbacks(self) -> Iterator[MythicCallback]:
        logger.debug("Retrieving callbacks from Mythic...")
        return_attributes = [
            "id",
            "display_id",
            "domain",
            "user",
            "host",
            "agent_callback_id"
        ]

        try:
            if not self._mythic_instance:
                self._connect()

            raw_callbacks = sync(mythic.get_all_callbacks(
                self._mythic_instance,
                custom_return_attributes=",".join(return_attributes)
            ))

            for callback in raw_callbacks:
                yield MythicCallback(callback, self._mythic_instance)

        except Exception as e:
            logger.error("Error retrieving callbacks from Mythic")
            logger.error(e)
            sys.exit(-1)

    @override
    def get_data_streams(self) -> Iterator['MythicDataStream']:
        """
        Get Mythic callback data streams.
        For mythic, instead of processing individual log "files"
        we will processes the taskings given to individual callbacks
        """
        for callback in self._get_callbacks():
            yield MythicDataStream(callback)

class MythicDataStream(DataStream):
    """Data stream that reads from a Mythic callback's task outputs."""

    def __init__(self, callback: MythicCallback):
        self.callback = callback

    @property
    def identifier(self) -> str:
        return f"mythic_callback_{self.callback.callback_id}"

    def lines(self) -> Iterator[str]:
        """Get lines from Mythic callback task outputs."""
        # Get all tasks for this callback
        tasks = self._get_tasks()

        for task in tasks:
            # Get task output
            outputs = self._get_task_output(task["display_id"])

            for output in outputs:
                # Decode and yield each line
                try:
                    decoded_data = base64.b64decode(output["response_text"]).decode("utf-8")
                    for line in decoded_data.splitlines():
                        if line.strip():  # Skip empty lines
                            yield line
                except Exception:
                    continue  # Skip malformed responses

    def _get_tasks(self):
        """Get tasks for the callback."""
        return sync(mythic.get_all_tasks(
            self.callback._mythic_instance,
            callback_display_id=self.callback.display_id
        ))

    def _get_task_output(self, task_id):
        """Get output for a specific task."""
        return sync(mythic.get_all_task_output_by_id(
            self.callback._mythic_instance,
            task_id
        ))

Command used

pylint a.py

Pylint output

pylint crashed with a ``AstroidError`` and with the following stacktrace:
Traceback (most recent call last):
  File "/Users/coffeegist/.vscode/extensions/ms-python.pylint-2025.3.12271016/bundled/libs/pylint/lint/pylinter.py", line 788, in _lint_file
    check_astroid_module(module)
  File "/Users/coffeegist/.vscode/extensions/ms-python.pylint-2025.3.12271016/bundled/libs/pylint/lint/pylinter.py", line 1020, in check_astroid_module
    retval = self._check_astroid_module(
  File "/Users/coffeegist/.vscode/extensions/ms-python.pylint-2025.3.12271016/bundled/libs/pylint/lint/pylinter.py", line 1072, in _check_astroid_module
    walker.walk(node)
  File "/Users/coffeegist/.vscode/extensions/ms-python.pylint-2025.3.12271016/bundled/libs/pylint/utils/ast_walker.py", line 87, in walk
    callback(astroid)
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pylint_pytest/checkers/fixture.py", line 129, in visit_module
    ret = pytest.main(
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/_pytest/config/__init__.py", line 150, in main
    arguments directly from the process command line (:data:`sys.argv`).
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/_pytest/config/__init__.py", line 331, in _prepareconfig
    elif not isinstance(args, list):
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pluggy/_callers.py", line 43, in run_old_style_hookwrapper
    teardown.send(result)
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/_pytest/helpconfig.py", line 104, in pytest_cmdline_parse
    @pytest.hookimpl(wrapper=True)
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pluggy/_result.py", line 103, in get_result
    raise exc.with_traceback(tb)
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pluggy/_callers.py", line 38, in run_old_style_hookwrapper
    res = yield
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pluggy/_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/_pytest/config/__init__.py", line 1075, in pytest_cmdline_parse
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/_pytest/config/__init__.py", line 1425, in parse
    finally:
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/_pytest/config/__init__.py", line 1305, in _preparse
    for name in _iter_rewritable_modules(package_files):
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pluggy/_manager.py", line 416, in load_setuptools_entrypoints
    plugin = ep.load()
  File "/Users/coffeegist/.pyenv/versions/3.10.18/lib/python3.10/importlib/metadata/__init__.py", line 171, in load
    module = import_module(match.group('module'))
  File "/Users/coffeegist/.pyenv/versions/3.10.18/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 992, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/_pytest/assertion/rewrite.py", line 186, in exec_module
    it.
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pytest_asyncio/__init__.py", line 7, in <module>
    from .plugin import fixture, is_async_test
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/_pytest/assertion/rewrite.py", line 186, in exec_module
    it.
  File "/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pytest_asyncio/plugin.py", line 38, in <module>
    from pytest import (
ImportError: cannot import name 'FixtureDef' from 'pytest' (/Users/coffeegist/Library/Caches/pypoetry/virtualenvs/bofhound-h-QmirpJ-py3.10/lib/python3.10/site-packages/pytest/__init__.py)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/coffeegist/.vscode/extensions/ms-python.pylint-2025.3.12271016/bundled/libs/pylint/lint/pylinter.py", line 752, in _lint_files
    self._lint_file(fileitem, module, check_astroid_module)
  File "/Users/coffeegist/.vscode/extensions/ms-python.pylint-2025.3.12271016/bundled/libs/pylint/lint/pylinter.py", line 790, in _lint_file
    raise astroid.AstroidError from e
astroid.exceptions.AstroidError

Expected behavior

No crash.

Pylint version

pylint 3.3.4
astroid 3.3.8
Python 3.10.18 (main, Sep 29 2025, 14:13:44) [Clang 17.0.0 (clang-1700.0.13.5)]

OS / Environment

darwin (Darwin)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions