#!/usr/bin/env python3 """ API Key Migration Tool - Consolidate scattered keys into secure vault Scans all projects and creates a unified key vault """ import os import sys # Add user site-packages to path for cryptography module import site sys.path.extend(site.getusersitepackages() if isinstance(site.getusersitepackages(), list) else [site.getusersitepackages()]) from pathlib import Path from typing import Dict, List, Set, Tuple import json import re from collections import defaultdict from datetime import datetime # Add parent directory to path for imports sys.path.append(str(Path(__file__).parent)) try: from key_vault import SecureKeyVault, CRYPTO_AVAILABLE except ImportError: print("āŒ key_vault.py not found in scripts directory") sys.exit(1) class KeyMigrator: """Migrate and consolidate API keys from multiple sources""" def __init__(self, workspace_root: Path): self.workspace_root = workspace_root self.vault = SecureKeyVault() self.found_keys = defaultdict(set) # key_name -> set of (file, value) tuples self.scan_report = [] def scan_directory(self, directory: Path, recursive: bool = True) -> Dict[str, str]: """Scan a directory for .env files and extract keys""" keys = {} env_files = [] if recursive: env_files = list(directory.rglob(".env*")) else: env_files = list(directory.glob(".env*")) for env_file in env_files: # Skip certain patterns if any(skip in str(env_file) for skip in [ 'node_modules', 'venv', '.venv', '__pycache__', '.git', 'dist', 'build', '.env.example', '.env.template' ]): continue self.scan_report.append(f"šŸ“‚ Scanning: {env_file.relative_to(self.workspace_root)}") try: with open(env_file, 'r') as f: for line in f: line = line.strip() if line and not line.startswith('#'): if '=' in line: key, value = line.split('=', 1) key = key.strip() value = value.strip().strip('"').strip("'") # Track where each key was found self.found_keys[key].add((str(env_file), value)) keys[key] = value except Exception as e: self.scan_report.append(f" āš ļø Error reading {env_file}: {e}") return keys def scan_all_projects(self) -> Dict[str, Dict[str, str]]: """Scan all known projects for API keys""" projects = { "root": self.workspace_root, "trax": self.workspace_root / "apps" / "trax", "youtube-summarizer": self.workspace_root / "apps" / "youtube-summarizer", "pdf-translator": self.workspace_root / "pdf-translator", "mixcloud-rss": self.workspace_root / "mixcloud-rss-generator", "clean-tracks": self.workspace_root / "projects" / "clean-tracks-main", "task-master": self.workspace_root / "tools" / "claude-task-master", "directus-mcp": self.workspace_root / "tools" / "directus-mcp-server", } all_keys = {} print("\nšŸ” Scanning projects for API keys...\n") for project_name, project_path in projects.items(): if project_path.exists(): print(f"šŸ“¦ Project: {project_name}") keys = self.scan_directory(project_path, recursive=False) if keys: all_keys[project_name] = keys print(f" āœ… Found {len(keys)} keys") else: print(f" āš ļø No keys found") else: print(f"šŸ“¦ Project: {project_name} - āš ļø Directory not found") return all_keys def analyze_duplicates(self) -> List[Tuple[str, List[Tuple[str, str]]]]: """Analyze duplicate keys with different values""" conflicts = [] for key_name, locations in self.found_keys.items(): unique_values = {} for file_path, value in locations: if value not in unique_values: unique_values[value] = [] unique_values[value].append(file_path) if len(unique_values) > 1: # Found conflicting values conflict_info = [] for value, files in unique_values.items(): masked_value = value[:8] + "..." if len(value) > 8 else value conflict_info.append((masked_value, files)) conflicts.append((key_name, conflict_info)) return conflicts def generate_migration_report(self) -> str: """Generate a detailed migration report""" report = [] report.append("=" * 60) report.append("API KEY MIGRATION REPORT") report.append("=" * 60) report.append(f"Generated: {datetime.now().isoformat()}") report.append(f"Workspace: {self.workspace_root}") report.append("") # Summary total_keys = len(self.found_keys) total_files = len(set(f for locs in self.found_keys.values() for f, _ in locs)) report.append("SUMMARY") report.append("-" * 40) report.append(f"Total unique keys found: {total_keys}") report.append(f"Total .env files scanned: {total_files}") report.append("") # Key categories categories = defaultdict(list) for key_name in self.found_keys.keys(): if key_name in SecureKeyVault.STANDARD_KEYS: meta = SecureKeyVault.STANDARD_KEYS[key_name] categories[meta.category].append(key_name) else: categories["custom"].append(key_name) report.append("KEYS BY CATEGORY") report.append("-" * 40) for category, keys in sorted(categories.items()): report.append(f"\n{category.upper()} ({len(keys)} keys):") for key in sorted(keys): locations_count = len(self.found_keys[key]) report.append(f" • {key} (found in {locations_count} locations)") report.append("") # Conflicts conflicts = self.analyze_duplicates() if conflicts: report.append("āš ļø CONFLICTS DETECTED") report.append("-" * 40) report.append("The following keys have different values in different locations:") report.append("") for key_name, conflict_info in conflicts: report.append(f" {key_name}:") for masked_value, files in conflict_info: report.append(f" Value: {masked_value}") for file_path in files[:3]: # Limit to 3 files rel_path = Path(file_path).relative_to(self.workspace_root) report.append(f" - {rel_path}") report.append("") # Missing standard keys standard_keys = set(SecureKeyVault.STANDARD_KEYS.keys()) found_standard = set(k for k in self.found_keys.keys() if k in standard_keys) missing_standard = standard_keys - found_standard if missing_standard: report.append("MISSING STANDARD KEYS") report.append("-" * 40) report.append("The following standard keys were not found:") for key in sorted(missing_standard): meta = SecureKeyVault.STANDARD_KEYS[key] report.append(f" • {key} - {meta.description}") report.append(f" Required for: {', '.join(meta.required_for)}") report.append("") # Scan details report.append("SCAN DETAILS") report.append("-" * 40) for entry in self.scan_report[-10:]: # Last 10 entries report.append(entry) report.append("") report.append("=" * 60) return "\n".join(report) def migrate_to_vault(self, interactive: bool = True, resolve_conflicts: str = "ask"): """ Migrate all found keys to the secure vault Args: interactive: Whether to ask for confirmation resolve_conflicts: How to handle conflicts ('ask', 'newest', 'skip') """ if not CRYPTO_AVAILABLE: print("āŒ cryptography package required for vault") print("Install with: pip install cryptography") return False print("\nšŸ” Migrating keys to secure vault...\n") migrated = 0 skipped = 0 errors = 0 for key_name, locations in self.found_keys.items(): # Check for conflicts unique_values = {} for file_path, value in locations: if value not in unique_values: unique_values[value] = [] unique_values[value].append(file_path) if len(unique_values) > 1: # Handle conflict print(f"\nāš ļø Conflict found for {key_name}:") values_list = list(unique_values.items()) for i, (value, files) in enumerate(values_list): masked = value[:8] + "..." if len(value) > 8 else value print(f" {i+1}. {masked}") for f in files[:2]: rel_path = Path(f).relative_to(self.workspace_root) print(f" - {rel_path}") if resolve_conflicts == "ask" and interactive: choice = input(f"Which value to use? (1-{len(values_list)}, s=skip): ").strip() if choice.lower() == 's': skipped += 1 continue try: idx = int(choice) - 1 final_value = values_list[idx][0] except: print(" Skipping...") skipped += 1 continue elif resolve_conflicts == "newest": # Use the most recently modified file's value newest_file = max(unique_values.items(), key=lambda x: max(Path(f).stat().st_mtime for f in x[1])) final_value = newest_file[0] else: skipped += 1 continue else: # No conflict, use the single value final_value = list(unique_values.keys())[0] # Add to vault try: category = None if key_name in SecureKeyVault.STANDARD_KEYS: category = SecureKeyVault.STANDARD_KEYS[key_name].category self.vault.add_key(key_name, final_value, category) migrated += 1 except Exception as e: print(f"āŒ Error migrating {key_name}: {e}") errors += 1 print(f"\nāœ… Migration complete!") print(f" • Migrated: {migrated} keys") print(f" • Skipped: {skipped} keys") print(f" • Errors: {errors} keys") return migrated > 0 def main(): """Main migration workflow""" import argparse parser = argparse.ArgumentParser(description="Migrate API keys to secure vault") parser.add_argument('--scan-only', action='store_true', help='Only scan and report, don\'t migrate') parser.add_argument('--auto', action='store_true', help='Automatic mode (no prompts)') parser.add_argument('--conflict-resolution', choices=['ask', 'newest', 'skip'], default='ask', help='How to resolve conflicts') parser.add_argument('--export-report', type=str, help='Export report to file') parser.add_argument('--workspace', type=Path, default=Path(__file__).parent.parent.parent.parent, help='Workspace root directory') args = parser.parse_args() # Initialize migrator migrator = KeyMigrator(args.workspace) # Scan all projects all_keys = migrator.scan_all_projects() # Generate report report = migrator.generate_migration_report() # Display report print("\n" + report) # Export report if requested if args.export_report: report_path = Path(args.export_report) with open(report_path, 'w') as f: f.write(report) print(f"\nšŸ“„ Report exported to: {report_path}") # Perform migration unless scan-only if not args.scan_only: if args.auto or input("\nšŸ” Migrate keys to secure vault? (y/n): ").lower() == 'y': success = migrator.migrate_to_vault( interactive=not args.auto, resolve_conflicts=args.conflict_resolution ) if success: print("\nšŸ“‹ Next steps:") print("1. Test vault access: python3 scripts/key_vault.py list") print("2. Export to project: python3 scripts/key_vault.py export .env --project=trax") print("3. Sync to all projects: python3 scripts/key_vault.py sync root trax youtube-summarizer") print("4. Validate project keys: python3 scripts/key_vault.py validate trax") if __name__ == "__main__": main()