Files
truf-server/app/paths.py
T
2026-09-30 20:30:56 +03:00

238 lines
9.0 KiB
Python

import ntpath
import os
import re
from query_policy import validate_rejected_query_policy
APP_DIR = os.path.dirname(os.path.abspath(__file__))
CANONICAL_ROOT = os.path.dirname(APP_DIR)
DEFAULT_TRUFFLEHOG = r"C:\Tools\trufflehog.exe"
PLACEHOLDER_RE = re.compile(r"\{([A-Za-z_][A-Za-z0-9_]*)\}")
class PathResolutionError(ValueError):
pass
def _norm(path):
return os.path.normpath(str(path))
def _config_dir(config_path=None):
if config_path:
return os.path.dirname(os.path.abspath(config_path))
return APP_DIR
def _expand(value, context):
text = str(value)
missing = sorted({name for name in PLACEHOLDER_RE.findall(text) if name not in context})
if missing:
raise PathResolutionError(f"Unknown path placeholder(s): {', '.join(missing)} in {text!r}")
for name in PLACEHOLDER_RE.findall(text):
text = text.replace("{" + name + "}", str(context[name]))
return os.path.expandvars(os.path.expanduser(text))
def is_command_name(value):
text = str(value or "")
return bool(text) and not os.path.isabs(text) and "\\" not in text and "/" not in text
def is_database_url(value):
return str(value or "").strip().lower().startswith(("postgresql://", "postgres://"))
def resolve_path(value, context=None, base_dir=None, allow_command=False, required=False):
if value is None or str(value).strip() == "":
if required:
raise PathResolutionError("Required path is empty")
return value
context = context or {}
text = _expand(value, context)
if os.name != 'nt' and (ntpath.splitdrive(text)[0] or '\\' in text):
raise PathResolutionError(f"Windows path is not supported on this platform: {text!r}")
if allow_command and is_command_name(text):
return text
if os.path.isabs(text):
return _norm(text)
base = base_dir or context.get("project_dir") or context.get("config_dir") or os.getcwd()
if os.name != 'nt' and (ntpath.splitdrive(str(base))[0] or '\\' in str(base)):
raise PathResolutionError(f"Windows base path is not supported on this platform: {base!r}")
return _norm(os.path.join(base, text))
def default_trufflehog_path():
return DEFAULT_TRUFFLEHOG if os.name == 'nt' and os.path.exists(DEFAULT_TRUFFLEHOG) else "trufflehog"
def resolve_project_paths(global_config=None, config_path=None):
global_config = global_config or {}
context = {"config_dir": _config_dir(config_path)}
root_raw = (
global_config.get("root_dir")
or os.getenv("SCANNER_ROOT_DIR")
or os.getenv("SCANNER_PROJECT_ROOT")
or CANONICAL_ROOT
)
context["root_dir"] = resolve_path(root_raw, context, base_dir=context["config_dir"], required=True)
project_raw = global_config.get("project_dir") or os.getenv("SCANNER_PROJECT_DIR") or context["config_dir"]
context["project_dir"] = resolve_path(project_raw, context, base_dir=context["config_dir"], required=True)
ordered_defaults = [
("runtime_dir", os.getenv("SCANNER_RUNTIME_DIR") or os.path.join(context["root_dir"], "runtime")),
("result_bundle_dir", os.getenv("SCANNER_RESULT_BUNDLE_DIR") or "{runtime_dir}/result_bundles"),
("result_spool_dir", "{runtime_dir}/result_spool"),
("results_dir", os.getenv("SCAN_RESULTS_DIR") or "{runtime_dir}/results"),
("queue_dir", "{runtime_dir}/queues"),
("state_dir", "{runtime_dir}/state"),
("log_dir", "{runtime_dir}/logs"),
("control_dir", "{runtime_dir}/control"),
("keycheck_dir", "{runtime_dir}/keychecks"),
("postman_cache_dir", "{runtime_dir}/postman_cache"),
("gharchive_cache_dir", "{state_dir}/gharchive_cache"),
("work_dir", os.getenv("TRUFFLEHOG_WORK_DIR") or os.path.join(context["root_dir"], "tmp")),
("proxy_file", "{runtime_dir}/proxy.txt"),
("database_path", os.getenv("SCANNER_DB_PATH") or os.getenv("SCAN_DB_PATH") or "{results_dir}/scanner.db"),
("state_file", "{state_dir}/runner_state.json"),
("secrets_file", "{project_dir}/secrets.yaml"),
]
for key, default in ordered_defaults:
raw = global_config.get(key) or default
context[key] = resolve_path(raw, context, base_dir=context["project_dir"], required=True)
managed_database_url = os.getenv("TRUF_MANAGED_POSTGRES_DSN") or ""
database_url = managed_database_url or global_config.get("database_url") or os.getenv("SCANNER_DB_URL") or os.getenv("DATABASE_URL") or ""
context["database_url"] = _expand(database_url, context) if database_url else ""
dashboard_db_url = managed_database_url or global_config.get("dashboard_db_url") or os.getenv("SCANNER_DASHBOARD_DB_URL") or context["database_url"]
context["dashboard_db_url"] = _expand(dashboard_db_url, context) if dashboard_db_url else ""
trufflehog_raw = global_config.get("trufflehog_path") or os.getenv("TRUFFLEHOG_PATH") or default_trufflehog_path()
context["trufflehog_path"] = resolve_path(
trufflehog_raw,
context,
base_dir=context["project_dir"],
allow_command=True,
required=True,
)
return context
def default_project_paths():
return resolve_project_paths({}, None)
def resolve_optional_path(value, path_context, base_dir=None, allow_command=False):
if not value:
return value
return resolve_path(value, path_context, base_dir=base_dir or path_context.get("project_dir"), allow_command=allow_command)
def resolve_postgres_data_dir(global_config=None, runtime_dir=None, base_dir=None):
global_config = global_config or {}
runtime_dir = runtime_dir or global_config.get('runtime_dir')
if not runtime_dir:
root_dir = global_config.get('root_dir') or CANONICAL_ROOT
runtime_dir = os.path.join(root_dir, 'runtime')
context = dict(global_config)
context['runtime_dir'] = runtime_dir
raw = global_config.get('postgres_data_dir') or os.path.join(runtime_dir, 'postgres', 'data')
return resolve_path(
raw,
context,
base_dir=base_dir or global_config.get('project_dir') or global_config.get('root_dir'),
required=True,
)
def resolve_postgres_bin_dir(global_config=None, runtime_dir=None, base_dir=None):
global_config = global_config or {}
runtime_dir = runtime_dir or global_config.get('runtime_dir')
if not runtime_dir:
runtime_dir = os.path.join(global_config.get('root_dir') or CANONICAL_ROOT, 'runtime')
context = dict(global_config, runtime_dir=runtime_dir)
return resolve_path(
global_config.get('postgres_bin_dir') or os.path.join(runtime_dir, 'postgres', 'pgsql', 'bin'),
context,
base_dir=base_dir or global_config.get('project_dir') or global_config.get('root_dir'),
required=True,
)
def apply_path_config(config, config_path=None):
config = config or {}
validate_rejected_query_policy(config)
global_config = config.setdefault("global", {})
path_context = resolve_project_paths(global_config, config_path)
for key, value in path_context.items():
global_config[key] = value
if global_config.get('legacy_result_spool_dir'):
global_config['legacy_result_spool_dir'] = resolve_path(
global_config['legacy_result_spool_dir'],
path_context,
base_dir=path_context['project_dir'],
required=True,
)
if global_config.get('postgres_data_dir'):
global_config['postgres_data_dir'] = resolve_postgres_data_dir(
global_config,
path_context['runtime_dir'],
base_dir=path_context['project_dir'],
)
if global_config.get('postgres_bin_dir'):
global_config['postgres_bin_dir'] = resolve_postgres_bin_dir(
global_config,
path_context['runtime_dir'],
base_dir=path_context['project_dir'],
)
for key in (
'api_proxy_file', 'download_proxy_file', 'trufflehog_config',
'dashboard_db_path', 'scan_limiter_db', 'dockerhub_tag_cache_path',
):
if global_config.get(key):
global_config[key] = resolve_path(
global_config[key],
path_context,
base_dir=path_context['project_dir'],
required=True,
)
supervisor = config.setdefault("supervisor", {})
supervisor_defaults = {
"log_dir": "{log_dir}",
"control_dir": "{control_dir}",
"instance_file": "{control_dir}/supervisor.instance.json",
"lock_file": "{control_dir}/supervisor.lock",
"supervisor_log": "{log_dir}/supervisor.log",
"status_file": "{log_dir}/supervisor.status.txt",
"dashboard_log": "{log_dir}/dashboard.log",
"state_dir": "{state_dir}",
}
for key, default in supervisor_defaults.items():
supervisor[key] = resolve_path(
supervisor.get(key) or default,
path_context,
base_dir=path_context["project_dir"],
required=True,
)
return config
def ensure_directories(paths, keys):
for key in keys:
path = paths.get(key)
if path:
os.makedirs(path, exist_ok=True)