Source code for src.config
"""Module that loads the tool's runtime configuration from a file and
makes it accessible to other modules."""
from __future__ import annotations
import logging
from configparser import ConfigParser
from datetime import datetime
from os import getenv
from pathlib import Path
from typing import Any, Optional, TypeVar
logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] {%(filename)s:%(funcName)s:%(lineno)d} %(levelname)s - "
"%(message)s",
datefmt="%H:%M:%S",
)
logger: logging.Logger = logging.getLogger(__name__)
[docs]
class Singleton:
[docs]
def __new__(cls, *args: Any, **kw: dict[str, Any]) -> Singleton:
if not hasattr(cls, "_instance"):
orig = super()
cls._instance = orig.__new__(cls, *args, **kw)
return cls._instance
T = TypeVar("T") # Declare type variable
[docs]
class Context(Singleton):
"""
Attributes:
settings: mapping of detected runtime configuration options to their
values. key naming scheme is <section>_<name>
"""
config_file_name: str = "occmdcfg.ini"
[docs]
def __init__(self):
self.CONFIG_FILE_LOCATIONS: list[Path] = [
Path.cwd(),
Path.cwd().parent,
Path.home(),
Path.home() / ".config/occmd/",
Path.cwd() / "examples/", # fall back to example config
]
# there is a single timestamp per run
self.timestamp: datetime = datetime.utcnow()
# read config from file
self.config: ConfigParser = self._read_config_file()
# store options as attributes
self.settings: dict[str, Any] = {}
self._set_config_attributes()
# handle config values that are no strings
self._transform_settings()
[docs]
def _read_config_file(self) -> ConfigParser:
for location in self.CONFIG_FILE_LOCATIONS:
config_file = location / self.config_file_name
if config_file.exists() and config_file.is_file():
logger.info(f"Found config file in {location=})")
config = ConfigParser()
config.read(config_file.absolute().as_posix())
return config
logger.info(f"Unable to find config file in {location=})")
raise RuntimeError("Config file not found")
[docs]
def _set_config_attributes(self) -> None:
for section in self.config.sections():
for key, conf_val in self.config[section].items():
conf_name: str = f"{section}_{key}"
env_val: Optional[str] = getenv(conf_name.upper())
if env_val:
logger.info(
f"Config {conf_name} was overridden by environment variable"
)
value: str = env_val if env_val else conf_val
logger.info(f"Setting config option {conf_name} = {value}")
self.settings |= {f"{conf_name}": value}
[docs]
def _transform_settings(self) -> None:
try:
self._transform_setting("local_repo_db_resources_dir", Path)
Path(self.settings["local_repo_db_resources_dir"]).mkdir(
parents=True, exist_ok=True
)
self._transform_setting("local_repo_db_path_db_raw", Path)
Path(self.settings["local_repo_db_path_db_raw"]).mkdir(
parents=True, exist_ok=True
)
self._transform_setting("Dashboard_file", Path)
self._transform_setting("Dashboard_pl_whitelist", Path)
self._transform_setting("Dashboard_pl_whitelist_wiki", Path)
self._transform_setting("Secrets_baselines_dir", Path)
Path(self.settings["Secrets_baselines_dir"]).mkdir(
parents=True, exist_ok=True
)
self._transform_setting("CheckedInBinaries_blacklist_dir", Path)
Path(self.settings["CheckedInBinaries_blacklist_dir"]).mkdir(
parents=True, exist_ok=True
)
self._transform_setting("SastUsageBasic_tools_dir", Path)
Path(self.settings["SastUsageBasic_tools_dir"]).mkdir(
parents=True, exist_ok=True
)
self._transform_setting("SastUsageBasic_tool_schema", Path)
self._transform_setting("SastUsageBasic_tools_csv", Path)
except AttributeError as e:
raise RuntimeError("Required config option was not set") from e
except Exception as e:
raise RuntimeError("Required config option was invalid") from e
[docs]
def _transform_setting(
self, setting_name: str, new_type: type[Any]
) -> None:
old_value: str = str(self.settings[setting_name])
self.settings |= {setting_name: new_type(old_value)}
context: Context = Context()