208 lines
7.3 KiB
Python
208 lines
7.3 KiB
Python
# Copyright (C) 2025 James Brotosky, Brandon Wickline
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import json
|
|
import logging
|
|
import logging.config
|
|
import logging.handlers
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import sys
|
|
|
|
from dotenv import load_dotenv, set_key
|
|
|
|
from utils.configmanager import PROTECTED_KEYS, load_protected_config
|
|
|
|
|
|
def get_base_directory() -> Path:
|
|
system = platform.system()
|
|
home = Path.home()
|
|
if system == "Windows":
|
|
return Path(os.getenv("APPDATA", home / "AppData" / "Roaming")) / "Loxide"
|
|
elif system == "Darwin":
|
|
return home / "Library" / "Application Support" / "Loxide"
|
|
else:
|
|
return home / ".local" / "share" / "Loxide"
|
|
|
|
|
|
def configure_logging(log_dir: Path, log_level: str = "DEBUG"):
|
|
log_file = log_dir / "Loxide.log"
|
|
|
|
config = {
|
|
"version": 1, # Required key for dictConfig format version
|
|
"disable_existing_loggers": False, # Keeps existing loggers active
|
|
"formatters": {
|
|
"detailed": {
|
|
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
# Includes timestamp, logger name, level, and message
|
|
},
|
|
"simple": {
|
|
"format": "%(levelname)s - %(message)s"
|
|
# Minimal format for console output
|
|
},
|
|
},
|
|
"handlers": {
|
|
"file": {
|
|
"class": "logging.handlers.TimedRotatingFileHandler",
|
|
"filename": str(log_file),
|
|
"when": "midnight", # Rotate logs at midnight
|
|
"interval": 1, # Every 1 day
|
|
"backupCount": 7, # Keep 7 days of logs
|
|
"encoding": "utf-8", # Ensure UTF-8 encoding
|
|
"level": "DEBUG", # Always log DEBUG and above
|
|
"formatter": "detailed", # Use detailed format
|
|
},
|
|
"console": {
|
|
"class": "logging.StreamHandler",
|
|
"level": log_level.upper(), # Configurable log level
|
|
"formatter": "simple", # Use simple format
|
|
},
|
|
},
|
|
"root": {
|
|
"level": "DEBUG", # Root logger level
|
|
"handlers": ["file", "console"], # Attach both handlers
|
|
},
|
|
}
|
|
|
|
# Add Windows Event Log handler if on Windows
|
|
if platform.system() == "Windows":
|
|
try:
|
|
config["handlers"]["eventlog"] = {
|
|
"class": "logging.handlers.NTEventLogHandler",
|
|
"appname": "Loxide", # Event log source name
|
|
"level": "CRITICAL", # Only log critical errors
|
|
"formatter": "simple", # Use simple format
|
|
}
|
|
config["root"]["handlers"].append("eventlog")
|
|
except Exception as e:
|
|
logging.warning(f"Could not attach Windows Event Log handler: {e}")
|
|
|
|
# Apply the logging configuration
|
|
logging.config.dictConfig(config)
|
|
logging.getLogger().debug("✅ Logging configured.")
|
|
|
|
|
|
def get_system_config_path() -> Path:
|
|
base_path = Path(
|
|
getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
|
|
)
|
|
return base_path.parent / "system_config.json"
|
|
|
|
|
|
def load_system_config() -> dict:
|
|
try:
|
|
config_path = get_system_config_path()
|
|
with open(config_path, "r") as f:
|
|
return json.load(f)
|
|
except FileNotFoundError:
|
|
logging.warning("⚠️ system_config.json not found. Using built-in defaults.")
|
|
return {
|
|
"APPNAME": "Loxide",
|
|
"LOG_LEVEL": "DEBUG",
|
|
"PATH_EXCLUSION_CONST": 4,
|
|
"MIN_FILES_FOR_PATH": 4,
|
|
"VT_THREAT_TOLERANCE": 4,
|
|
"POLICY_MAP_ENF_AUD": {"enforced_id": "audit_id"},
|
|
}
|
|
|
|
|
|
def load_user_config(config_dir: Path) -> dict:
|
|
user_config_path = config_dir / "user_config.json"
|
|
if not user_config_path.exists():
|
|
default_user_config = {"URL": "", "LOG_LEVEL": "INFO"}
|
|
with open(user_config_path, "w") as f:
|
|
json.dump(default_user_config, f, indent=4)
|
|
logging.debug(f"Created user config at {user_config_path}")
|
|
with open(user_config_path, "r") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def write_config_to_env(config: dict, env_path: Path):
|
|
for key, value in config.items():
|
|
if key in PROTECTED_KEYS:
|
|
continue # Skip protected keys
|
|
try:
|
|
serialized = (
|
|
json.dumps(value) if isinstance(value, (list, dict)) else str(value)
|
|
)
|
|
set_key(env_path, key, serialized)
|
|
except Exception as e:
|
|
logging.warning(f"Failed to write {key} to .env: {e}")
|
|
|
|
|
|
def setup():
|
|
base_dir = get_base_directory()
|
|
dirs = {
|
|
"config": base_dir / "config",
|
|
"cache": base_dir / "cache",
|
|
"logs": base_dir / "logs",
|
|
}
|
|
|
|
for name, path in dirs.items():
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
logging.debug(f"{name.capitalize()} directory ensured at: {path}")
|
|
|
|
system_config = load_system_config()
|
|
configure_logging(dirs["logs"], system_config.get("LOG_LEVEL", "DEBUG"))
|
|
|
|
env_path = base_dir / ".env"
|
|
if not env_path.exists():
|
|
env_path.touch()
|
|
load_dotenv(dotenv_path=env_path, override=True)
|
|
|
|
working_dir = Path(os.getenv("WORKING_DIR") or (base_dir / "data"))
|
|
working_dir.mkdir(parents=True, exist_ok=True)
|
|
set_key(env_path, "WORKING_DIR", str(working_dir))
|
|
os.environ["WORKING_DIR"] = str(working_dir)
|
|
logging.debug(f"Working directory set to: {working_dir}")
|
|
|
|
folders_structure = {
|
|
"Approved": [],
|
|
"Needs_Review": ["Review_First", "Review_Second", "HTML"],
|
|
"Preflight": ["HTML"],
|
|
"Archived": [],
|
|
}
|
|
|
|
for folder_name, subfolders in folders_structure.items():
|
|
folder_path = working_dir / folder_name
|
|
folder_path.mkdir(parents=True, exist_ok=True)
|
|
logging.debug(f"'{folder_name}' folder ensured at: {folder_path}")
|
|
for subfolder in subfolders:
|
|
subfolder_path = folder_path / subfolder
|
|
subfolder_path.mkdir(parents=True, exist_ok=True)
|
|
logging.debug(f" └─ '{subfolder}' subfolder created at: {subfolder_path}")
|
|
|
|
user_config = load_user_config(dirs["config"])
|
|
merged_config = {**system_config, **user_config}
|
|
|
|
protected_config = load_protected_config()
|
|
merged_config.update(protected_config)
|
|
|
|
# ✅ URL resolution order: system_config → .env → user prompt
|
|
url = system_config.get("URL")
|
|
if not url:
|
|
url = os.getenv("URL")
|
|
if not url:
|
|
url = input(
|
|
"🌐 Enter the service URL (e.g., https://example.com/api): "
|
|
).strip()
|
|
merged_config["URL"] = url
|
|
set_key(env_path, "URL", url)
|
|
os.environ["URL"] = url
|
|
logging.debug(f"Service URL set to: {url}")
|
|
|
|
write_config_to_env(merged_config, env_path)
|