Source code for badsnakes.libs.logger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
:Purpose:   This module provides the *file-based* reporting
            functionality for the project. Terminal-based reporting is
            handled by the :mod:`badsnakes.libs.reporter` module.

            Each ``_LogTemplate*`` class is responsible for the
            formatting of each AST node class. Whereas the primary caller
            :class:`Logger` class controls the file creation and writing
            functionalities.

:Platform:  Linux/Windows | Python 3.10+
:Developer: J Berendt
:Email:     development@s3dev.uk

:Comments:  n/a

:Example:
            Create a log file for a given module, or modules::

                >>> from badsnakes import Module
                >>> from badsnakes.libs.logger import Logger

                # Analyse a Python module.
                >>> m = Module(path='/path/to/millworker.py')
                >>> m.analyse()

                # Create the log file.
                >>> l = Logger(path='/path/to/millworker.log', modules=[m])
                >>> l.write()

"""

# Enable type hinting
from __future__ import annotations

import os


[docs] class Logger: """Write module findings to a log file. Args: path (str): Full path to the log file to be created. modules (list[module.Module] | tuple[module.Module]): A list or tuple containing :class:`badsnakes.libs.module.Module` objects containing findings to be written to a log file. """ _HEADER = 'severity,module,line_start,line_end,title,text' def __init__(self, path: str, modules: list[module.Module] | tuple[module.Module]): # noqa # pylint: disable=undefined-variable """Logger class initialiser.""" self._fp = path self._modules = modules if isinstance(modules, (list, tuple)) else [modules] self._tmps = None # Logging templates. self._setup()
[docs] def write(self): """Write log entries for all modules. :Logic: For each module passed on instantiation, create a logging template parent-class, based on the module's name. Suspect findings are written first, followed by dangerous findings. """ for module in self._modules: self._tmps = _LogTemplates(module_name=module.name_and_parent) self._write_suspect(module=module) self._write_dangerous(module=module)
[docs] def _setup(self): """Create the new log file, if it does not already exist.""" if not os.path.exists(self._fp): with open(self._fp, 'w', encoding='utf-8') as f: f.write(f'{self._HEADER}\n')
[docs] def _write(self, text: str): """Generalised log entry writer. Args: text (str): Delimited text string to be written to the log. """ if text: with open(self._fp, 'a', encoding='utf-8') as f: f.write(f'{text}\n')
[docs] def _write_dangerous(self, module: module.Module): # noqa # pylint: disable=undefined-variable """Write a module's *dangerous* findings to the log file. Args: module (module.Module): Module for which the log entries are to be written. :Logic: For each AST node class, obtain the associated log template sub-class from :class:`_LogTemplates`, using the node class' ``.name`` property. Using the AST-specific log template, write the dangerous findings to the log file created on logger instantiation. """ for nc in module.nodeclasses: logger = getattr(self._tmps, nc.name, None) if logger: for node in nc.analyser.dangerous: self._write(logger.entry(node)) for node in nc.analyser.dangerous_longstring: self._write(logger.entry_longstring(node))
[docs] def _write_suspect(self, module: module.Module): # noqa # pylint: disable=undefined-variable """Write a module's *suspect* findings to the log file. Args: module (module.Module): Module for which the log entries are to be written. :Logic: For each AST node class, obtain the associated log template sub-class from :class:`_LogTemplates`, using the node class' ``.name`` property. Using the AST-specific log template, write the suspect findings to the log file created on logger instantiation. """ for nc in module.nodeclasses: logger = getattr(self._tmps, nc.name, None) if logger: for node in nc.analyser.suspect: self._write(logger.entry(node)) for node in nc.analyser.suspect_longstring: self._write(logger.entry_longstring(node))
# %% Related private class implementations.
[docs] class _LogTemplates: """Private wrapper class for all logging templates.""" __slots__ = ('arguments', 'assignments', 'attributes', 'calls', 'codetext', 'constants', 'functiondefs', 'imports') def __init__(self, module_name: str): self.arguments = _LogTemplateArguments(module_name=module_name) self.assignments = _LogTemplateAssignment(module_name=module_name) self.attributes = _LogTemplateAttribute(module_name=module_name) self.calls = _LogTemplateCall(module_name=module_name) self.codetext = _LogTemplateCodeText(module_name=module_name) self.constants = _LogTemplateConstant(module_name=module_name) self.functiondefs = _LogTemplateFunctionDef(module_name=module_name) self.imports = _LogTemplateImport(module_name=module_name)
[docs] class _LogTemplateBase: """Private template logging base class. This class provides the :meth:`_populate` method to the sub-classes, which is used to build the log entry. Args: module_name (str): Name of the module being logged. This name is written to the 'module' field of the log file. """ _BASE = '{severity},{module},{line_start},{line_end},{title},"{text}"' _TITLE = '' def __init__(self, module_name: str): """Log templates base class initialiser.""" self._name = module_name
[docs] def entry_longstring(self, node: object): """Generate an entry for a long string. Args: node (object): AST node container from which the severity and line numbers are obtained. Note: Long strings are truncated to the leading and trailing 25 characters to preserve brevity. Returns: str: The complete, formatted entry to be written to the log file. """ trunc = f'{repr(node.value[:25])}...{repr(node.value[-25:])}' text = f"A {len(node.value)} char string detected: {trunc}" return self._populate(node=node, text=text, title='Long string')
[docs] def _populate(self, node: object, text: str, title: str=None) -> str: """Generate a node-specific log entry. Args: node (object): AST node container from which the severity and line numbers are obtained. text (str): Text to be written to the 'text' field of the log file. title (str, optional): Title to be written to the 'title' field of the log file. If provided, this argument overrides the ``_TITLE`` attribute of the node-specific logging class. Defaults to None. Returns: str: The complete, formatted entry to be written to the log file. """ title = title if title else self._TITLE return self._BASE.format(severity=node.severity.name, module=self._name, line_start=node.line, line_end=node.line_end, title=title, text=text)
[docs] class _LogTemplateArguments(_LogTemplateBase): """``Constant`` node class specific logging template class.""" _TITLE = 'Argument'
[docs] def entry(self, node: containers.Constant) -> str: # noqa # pylint: disable=undefined-variable """Generate a node-specific log entry. Args: node (containers.Constant): A :class:`badsnakes.libs.containers.Constant` object containing the values from which the log entry is derived. Returns: str: The complete, formatted entry to be written to the log file. """ text = f"'{node.value}' passed into the '{node.name}' function" return self._populate(node=node, text=text)
[docs] class _LogTemplateAssignment(_LogTemplateBase): """``Assignment`` node class specific logging template class.""" _TITLE = 'Assignment'
[docs] def entry(self, node: containers.Assign) -> str: # noqa # pylint: disable=undefined-variable """Generate a node-specific log entry. Args: node (containers.Assign): A :class:`badsnakes.libs.containers.Assign` object containing the values from which the log entry is derived. Returns: str: The complete, formatted entry to be written to the log file. """ text = f"{node.name} = {node.value}" return self._populate(node=node, text=text)
[docs] class _LogTemplateAttribute(_LogTemplateBase): """``Attribute`` node class specific logging template class.""" _TITLE = 'Attribute'
[docs] def entry(self, node: containers.Attribute) -> str: # noqa # pylint: disable=undefined-variable """Generate a node-specific log entry. Args: node (containers.Attribute): A :class:`badsnakes.libs.containers.Attribute` object containing the values from which the log entry is derived. Returns: str: The complete, formatted entry to be written to the log file. """ text = f"Use of {node.name}.{node.value} detected" return self._populate(node=node, text=text)
[docs] class _LogTemplateCall(_LogTemplateBase): """``Call`` node class specific logging template class.""" _TITLE = 'Function call'
[docs] def entry(self, node: containers.Call) -> str: # noqa # pylint: disable=undefined-variable """Generate a node-specific log entry. Args: node (containers.Call): A :class:`badsnakes.libs.containers.Call` object containing the values from which the log entry is derived. Returns: str: The complete, formatted entry to be written to the log file. """ text = f'{node.module}.{node.name}' if node.module else node.name return self._populate(node=node, text=text)
[docs] class _LogTemplateCodeText(_LogTemplateBase): """``CodeText`` node class specific logging template class.""" _TITLE = 'CodeText'
[docs] def entry(self, node: containers.CodeText) -> str: # noqa # pylint: disable=undefined-variable """Generate a node-specific log entry. Args: node (containers.CodeText): A :class:`badsnakes.libs.containers.CodeText` object containing the values from which the log entry is derived. Returns: str: The complete, formatted entry to be written to the log file. """ return self._populate(node=node, text=node.reason)
[docs] class _LogTemplateConstant(_LogTemplateBase): """``Constant`` node class specific logging template class.""" _TITLE = 'String search'
[docs] def entry(self, node: containers.Constant) -> str: # noqa # pylint: disable=undefined-variable """Generate a node-specific log entry. Args: node (containers.Constant): A :class:`badsnakes.libs.containers.Constant` object containing the values from which the log entry is derived. Returns: str: The complete, formatted entry to be written to the log file. """ text=f"'{node.searchstr}' found in {repr(node.value)}" return self._populate(node=node, text=text)
[docs] class _LogTemplateFunctionDef(_LogTemplateBase): """``FunctionDef`` node class specific logging template class.""" _TITLE = 'Function definition'
[docs] def entry(self, node: containers.FunctionDef) -> str: # noqa # pylint: disable=undefined-variable """Generate a node-specific log entry. Args: node (containers.FunctionDef): A :class:`badsnakes.libs.containers.FunctionDef` object containing the values from which the log entry is derived. Returns: str: The complete, formatted entry to be written to the log file. """ text = f"Function named '{node.name}' detected" return self._populate(node=node, text=text)
[docs] class _LogTemplateImport(_LogTemplateBase): """``FunctionDef`` node class specific logging template class.""" _TITLE = 'Import'
[docs] def entry(self, node: containers.Import) -> str: # noqa # pylint: disable=undefined-variable """Generate a node-specific log entry. Args: node (containers.Import): A :class:`badsnakes.libs.containers.Import` object containing the values from which the log entry is derived. Returns: str: The complete, formatted entry to be written to the log file. """ if node.asname: module = f'{node.module}.{node.name}' if node.name else node.module text = f"import {module} as {node.asname}" elif node.name: text = f"from {node.module} import {node.name}" else: text = f"import {node.module}" return self._populate(node=node, text=text)