325 lines
14 KiB
Python
325 lines
14 KiB
Python
import sys
|
|
|
|
sys.dont_write_bytecode = True
|
|
|
|
import argparse
|
|
import fnmatch
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
|
|
from db_backend import database_url_from_env, is_postgres_url
|
|
from paths import apply_path_config, default_project_paths
|
|
from migrate_runtime_safety import require_runtime_hardening_stopped
|
|
from postgres_runtime import load_postgres_environment
|
|
from runtime_security import ClusterAuthorityLock, reject_reparse_components
|
|
|
|
|
|
DESKTOP_HF = r"C:\Users\pro100noob\Desktop\HugginFace"
|
|
EXCLUDED_DIRS = {"__pycache__", ".git", ".opencode", "node_modules", "tmp", "runtime"}
|
|
APP_FILES = [
|
|
"app.py",
|
|
"console_runner.py",
|
|
"dashboard.py",
|
|
"keycheck_runner.py",
|
|
"migrate_layout.py",
|
|
"paths.py",
|
|
"scan_manager.py",
|
|
"scanner.py",
|
|
"scanner_db.py",
|
|
"supervisor.py",
|
|
"ui_components.py",
|
|
"config.yaml",
|
|
"secrets.yaml",
|
|
"requirements.txt",
|
|
"CHEATSHEET.md",
|
|
"DETECTOR_NOTES.md",
|
|
]
|
|
APP_DIRS = [".streamlit", "keycheckers"]
|
|
RESULT_FILES = ["found_secrets.jsonl", "scan_results.jsonl", "scan_errors.log", "scanner.db", "scanner.db-wal", "scanner.db-shm"]
|
|
KEYCHECK_SERVICE_SCRIPTS = {
|
|
"anthropic": [os.path.join("anthropic", "anthropicKeycheck.py")],
|
|
"aws": [os.path.join("aws", "awsKeycheck.py")],
|
|
"azure": [os.path.join("azure", "azureKeycheck.py")],
|
|
"deepseek": [os.path.join("deepseek", "deepseekKeycheck.py")],
|
|
"dockerhub": [os.path.join("dockerhub", "dockerhubKeycheck.py"), os.path.join("dockerhub", "dockerhub.txt")],
|
|
"gcp": [os.path.join("gcp", "gcpKeycheck.py"), os.path.join("gcp", "gcp.txt")],
|
|
"gemini": [os.path.join("gemini", "geminiKeycheck.py"), os.path.join("gemini", "gem.txt")],
|
|
"groq": [os.path.join("groq", "groqKeycheck.py")],
|
|
"github": [os.path.join("github", "githubKeycheck.py"), os.path.join("github", "github.txt")],
|
|
"gitlab": [os.path.join("gitlab", "gitlabKeycheck.py"), os.path.join("gitlab", "gitlab.txt")],
|
|
"kimi": [os.path.join("kimi", "kimiKeycheck.py")],
|
|
"openai": ["Keycheck.py"],
|
|
"openrouter": ["OpenrouterKeycheck.py"],
|
|
"provider_resolver": [os.path.join("provider_resolver", "providerResolverKeycheck.py")],
|
|
"qwen": [os.path.join("qwen", "qwenKeycheck.py")],
|
|
"replicate": [os.path.join("replicate", "replicateKeycheck.py")],
|
|
"xai": [os.path.join("xai", "xaiKeycheck.py")],
|
|
"huggingface": [os.path.join("huggingface", "huggingfaceKeycheck.py")],
|
|
"zai": [os.path.join("zai", "zaiKeycheck.py")],
|
|
}
|
|
KEYCHECK_TOP_LEVEL_PREFIXES = {
|
|
"openai": "openai",
|
|
"openrouter": "openrouter",
|
|
}
|
|
|
|
|
|
def load_config(config_path):
|
|
if not config_path:
|
|
return {'global': default_project_paths()}
|
|
try:
|
|
import yaml
|
|
except ImportError as e:
|
|
raise SystemExit("PyYAML is required for --config") from e
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|
config = apply_path_config(yaml.safe_load(f) or {}, config_path)
|
|
return config
|
|
|
|
|
|
def load_layout(config_path):
|
|
return load_config(config_path).get('global') or {}
|
|
|
|
|
|
def mkdir(path, dry_run=False):
|
|
if dry_run:
|
|
print(f"mkdir {path}")
|
|
return
|
|
os.makedirs(path, exist_ok=True)
|
|
|
|
|
|
def copy_file(src, dst, overwrite=False, dry_run=False):
|
|
if not os.path.exists(src):
|
|
return False
|
|
if os.path.lexists(dst) and not overwrite:
|
|
print(f"skip existing {dst}")
|
|
return False
|
|
parent = os.path.dirname(dst)
|
|
if parent:
|
|
mkdir(parent, dry_run)
|
|
if dry_run:
|
|
print(f"copy {src} -> {dst}")
|
|
return True
|
|
try:
|
|
reject_reparse_components(parent or os.path.dirname(os.path.abspath(dst)))
|
|
if os.path.lexists(dst):
|
|
reject_reparse_components(dst)
|
|
shutil.copy2(src, dst)
|
|
except OSError as e:
|
|
print(f"skip locked/unavailable {src}: {e}")
|
|
return False
|
|
print(f"copied {src} -> {dst}")
|
|
return True
|
|
|
|
|
|
def ignore_app_dir(_dir, names):
|
|
ignored = set()
|
|
for name in names:
|
|
if name in EXCLUDED_DIRS:
|
|
ignored.add(name)
|
|
if fnmatch.fnmatch(name, "*.pyc"):
|
|
ignored.add(name)
|
|
return ignored
|
|
|
|
|
|
def copy_dir(src, dst, overwrite=False, dry_run=False, verified_apply=False):
|
|
if not os.path.isdir(src):
|
|
return False
|
|
if os.path.lexists(dst) and not overwrite:
|
|
print(f"skip existing {dst}")
|
|
return False
|
|
if dry_run:
|
|
print(f"copytree {src} -> {dst}")
|
|
return True
|
|
if os.path.lexists(dst) and overwrite:
|
|
raise RuntimeError('legacy directory replacement is retired; existing directories are never replaced')
|
|
shutil.copytree(src, dst, ignore=ignore_app_dir, dirs_exist_ok=False)
|
|
print(f"copied {src} -> {dst}")
|
|
return True
|
|
|
|
|
|
def create_layout(layout, dry_run=False):
|
|
for key in ("project_dir", "runtime_dir", "results_dir", "queue_dir", "log_dir", "state_dir", "keycheck_dir", "work_dir"):
|
|
mkdir(layout[key], dry_run)
|
|
mkdir(os.path.join(layout["runtime_dir"], "imports"), dry_run)
|
|
|
|
|
|
def copy_app_files(source_dir, layout, overwrite=False, dry_run=False, verified_apply=False):
|
|
project_dir = layout["project_dir"]
|
|
if os.path.abspath(source_dir) == os.path.abspath(project_dir):
|
|
print("app source is already project_dir; app copy skipped")
|
|
return
|
|
for name in APP_FILES:
|
|
copy_file(os.path.join(source_dir, name), os.path.join(project_dir, name), overwrite, dry_run)
|
|
for name in APP_DIRS:
|
|
copy_dir(os.path.join(source_dir, name), os.path.join(project_dir, name), overwrite, dry_run, verified_apply)
|
|
|
|
|
|
def copy_scanner_runtime(old_root, layout, overwrite=False, dry_run=False, verified_apply=False):
|
|
for name in RESULT_FILES:
|
|
copy_file(os.path.join(old_root, name), os.path.join(layout["results_dir"], name), overwrite, dry_run)
|
|
for pattern in ("todo_*.txt", "checked_*.txt"):
|
|
if not os.path.isdir(old_root):
|
|
continue
|
|
for name in os.listdir(old_root):
|
|
if fnmatch.fnmatch(name, pattern):
|
|
copy_file(os.path.join(old_root, name), os.path.join(layout["queue_dir"], name), overwrite, dry_run)
|
|
copy_dir(os.path.join(old_root, "logs"), layout["log_dir"], overwrite, dry_run, verified_apply)
|
|
copy_dir(os.path.join(old_root, "state"), layout["state_dir"], overwrite, dry_run, verified_apply)
|
|
copy_file(os.path.join(old_root, "runner_state.json"), os.path.join(layout["state_dir"], "runner_state.json"), overwrite, dry_run)
|
|
|
|
|
|
def line_hash(line):
|
|
return hashlib.sha256(line.strip().encode("utf-8", errors="replace")).hexdigest()
|
|
|
|
|
|
def existing_line_hashes(path):
|
|
hashes = set()
|
|
if not os.path.exists(path):
|
|
return hashes
|
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
for line in f:
|
|
if line.strip():
|
|
hashes.add(line_hash(line))
|
|
return hashes
|
|
|
|
|
|
def import_jsonl_dedupe(inputs, output, dry_run=False):
|
|
hashes = existing_line_hashes(output)
|
|
added = 0
|
|
if dry_run:
|
|
print(f"dedupe import {len(inputs)} file(s) -> {output}")
|
|
return 0
|
|
mkdir(os.path.dirname(output), dry_run=False)
|
|
with open(output, "a", encoding="utf-8") as dst:
|
|
for path in inputs:
|
|
if not os.path.exists(path):
|
|
continue
|
|
with open(path, "r", encoding="utf-8", errors="replace") as src:
|
|
for line in src:
|
|
if not line.strip():
|
|
continue
|
|
digest = line_hash(line)
|
|
if digest in hashes:
|
|
continue
|
|
dst.write(line if line.endswith("\n") else line + "\n")
|
|
hashes.add(digest)
|
|
added += 1
|
|
print(f"imported {added} unique finding line(s) into {output}")
|
|
return added
|
|
|
|
|
|
def copy_legacy_keychecker_outputs(layout, desktop_dir=DESKTOP_HF, overwrite=False, dry_run=False):
|
|
if not os.path.isdir(desktop_dir):
|
|
return
|
|
for service in KEYCHECK_SERVICE_SCRIPTS:
|
|
source_dir = os.path.join(desktop_dir, service)
|
|
target_dir = os.path.join(layout["keycheck_dir"], service)
|
|
if os.path.isdir(source_dir):
|
|
for name in os.listdir(source_dir):
|
|
if name.lower().endswith((".txt", ".jsonl")):
|
|
copy_file(os.path.join(source_dir, name), os.path.join(target_dir, name), overwrite, dry_run)
|
|
for service, prefix in KEYCHECK_TOP_LEVEL_PREFIXES.items():
|
|
target_dir = os.path.join(layout["keycheck_dir"], service)
|
|
for name in os.listdir(desktop_dir):
|
|
lower = name.lower()
|
|
if lower.startswith(prefix) and lower.endswith((".txt", ".jsonl")):
|
|
copy_file(os.path.join(desktop_dir, name), os.path.join(target_dir, name), overwrite, dry_run)
|
|
|
|
|
|
def merge_unique_lines(inputs, output, dry_run=False):
|
|
values = []
|
|
seen = existing_values = set()
|
|
if os.path.exists(output):
|
|
with open(output, "r", encoding="utf-8", errors="replace") as f:
|
|
existing_values = {line.strip().lstrip("\ufeff") for line in f if line.strip()}
|
|
seen = set(existing_values)
|
|
for path in inputs:
|
|
if not os.path.exists(path):
|
|
continue
|
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
for line in f:
|
|
value = line.strip().lstrip("\ufeff")
|
|
if value and value not in seen:
|
|
values.append(value)
|
|
seen.add(value)
|
|
if dry_run:
|
|
print(f"merge {len(values)} unique line(s) -> {output}")
|
|
return
|
|
mkdir(os.path.dirname(output), dry_run=False)
|
|
with open(output, "a", encoding="utf-8") as f:
|
|
for value in values:
|
|
f.write(value + "\n")
|
|
print(f"appended {len(values)} unique line(s) -> {output}")
|
|
|
|
|
|
def import_huggingface_desktop(layout, desktop_dir=DESKTOP_HF, overwrite=False, dry_run=False):
|
|
if not os.path.isdir(desktop_dir):
|
|
print(f"Desktop HugginFace directory not found: {desktop_dir}")
|
|
return
|
|
jsonl_inputs = [os.path.join(desktop_dir, name) for name in os.listdir(desktop_dir) if fnmatch.fnmatch(name, "found_secrets*.jsonl")]
|
|
import_jsonl_dedupe(jsonl_inputs, os.path.join(layout["results_dir"], "found_secrets.jsonl"), dry_run)
|
|
merge_unique_lines([os.path.join(desktop_dir, "checked.txt")], os.path.join(layout["queue_dir"], "checked_huggingface.txt"), dry_run)
|
|
merge_unique_lines([os.path.join(desktop_dir, "todo.txt")], os.path.join(layout["queue_dir"], "todo_huggingface.txt"), dry_run)
|
|
copy_file(os.path.join(desktop_dir, "proxy.txt"), layout["proxy_file"], overwrite=False, dry_run=dry_run)
|
|
copy_file(os.path.join(desktop_dir, "requirements-keycheckers.txt"), os.path.join(layout["project_dir"], "requirements-keycheckers.txt"), overwrite, dry_run)
|
|
copy_file(os.path.join(desktop_dir, "KEYCHECKERS.md"), os.path.join(layout["project_dir"], "KEYCHECKERS.md"), overwrite, dry_run)
|
|
for service, rel_paths in KEYCHECK_SERVICE_SCRIPTS.items():
|
|
for rel_path in rel_paths:
|
|
src = os.path.join(desktop_dir, rel_path)
|
|
dst = os.path.join(layout["project_dir"], "keycheckers", service, os.path.basename(rel_path))
|
|
copy_file(src, dst, overwrite, dry_run)
|
|
copy_legacy_keychecker_outputs(layout, desktop_dir, overwrite, dry_run)
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description="Copy/import legacy scanner files into the unified D:\\truf layout.")
|
|
parser.add_argument("--config", default="config.yaml")
|
|
parser.add_argument("--source-app", default=os.path.dirname(os.path.abspath(__file__)))
|
|
parser.add_argument("--target-app", help="Destination app directory. Defaults to <root_dir>\\app.")
|
|
parser.add_argument("--in-place", action="store_true", help="Use project_dir from config instead of copying to <root_dir>\\app.")
|
|
parser.add_argument("--old-root", default=r"D:\truf")
|
|
parser.add_argument("--desktop-hf", default=DESKTOP_HF)
|
|
parser.add_argument("--overwrite", action="store_true")
|
|
parser.add_argument("--dry-run", action="store_true")
|
|
parser.add_argument("--apply", action="store_true", help="Retired; production layout mutation is disabled")
|
|
parser.add_argument("--no-app-copy", action="store_true")
|
|
parser.add_argument("--no-desktop-import", action="store_true")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
if args.apply and args.dry_run:
|
|
raise SystemExit('--apply and --dry-run are mutually exclusive')
|
|
if args.apply:
|
|
raise SystemExit(
|
|
'migrate_layout --apply is retired because the production layout is already migrated. '
|
|
'Use reviewed offline backup/restore tooling for any future relocation.'
|
|
)
|
|
config = load_config(args.config)
|
|
layout = config.get('global') or {}
|
|
if not args.in_place:
|
|
layout["project_dir"] = args.target_app or os.path.join(layout["root_dir"], "app")
|
|
dry_run = not args.apply
|
|
load_postgres_environment(os.path.abspath(args.config), config)
|
|
endpoint_dsn = database_url_from_env() or layout.get('database_url')
|
|
if not is_postgres_url(endpoint_dsn):
|
|
raise SystemExit('A caller-selected canonical PostgreSQL DSN is required for maintenance authority')
|
|
with ClusterAuthorityLock(config, endpoint_dsn=endpoint_dsn):
|
|
require_runtime_hardening_stopped(config)
|
|
create_layout(layout, dry_run)
|
|
if not args.no_app_copy:
|
|
copy_app_files(args.source_app, layout, args.overwrite, dry_run, verified_apply=args.apply)
|
|
copy_scanner_runtime(args.old_root, layout, args.overwrite, dry_run, verified_apply=args.apply)
|
|
if not args.no_desktop_import:
|
|
import_huggingface_desktop(layout, args.desktop_hf, args.overwrite, dry_run)
|
|
if dry_run:
|
|
print("Dry-run complete. --apply is retired; use reviewed offline backup/restore tooling for relocation.")
|
|
return 0
|
|
print("Migration copy/import finished. Originals were left in place.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|