#!/usr/bin/env nu # Package validation tool - validates package integrity and completeness # # Validates: # - Package structure and required files # - Binary execution and dependencies # - Configuration completeness # - Installation script functionality # - Security and integrity checks use std log def main [ --package-path: string # Path to package file or directory to validate --validation-type: string = "complete" # Validation type: quick, complete, security --temp-workspace: string = "" # Temporary workspace (auto-generated if empty) --cleanup: bool = true # Cleanup workspace after validation --skip-execution: bool = false # Skip binary execution tests --verbose: bool = false # Enable verbose logging --output-format: string = "text" # Output format: text, json, table ] -> record { let package_root = ($package_path | path expand) let temp_workspace = if $temp_workspace == "" { $env.TMPDIR | path join $"package-validation-(random uuid)" } else { $temp_workspace | path expand } let validation_config = { package_path: $package_root validation_type: $validation_type temp_workspace: $temp_workspace cleanup: $cleanup skip_execution: $skip_execution verbose: $verbose output_format: $output_format } log info $"Starting package validation with config: ($validation_config)" # Validate package exists if not ($package_root | path exists) { log error $"Package does not exist: ($package_root)" exit 1 } # Create temporary workspace mkdir ($validation_config.temp_workspace) let validation_results = [] try { # Extract/prepare package for validation let preparation_result = prepare_package_for_validation $validation_config if $preparation_result.status != "success" { log error $"Failed to prepare package for validation: ($preparation_result.reason)" exit 1 } let package_dir = $preparation_result.extracted_path # Run validation tests based on type let validation_categories = match $validation_config.validation_type { "quick" => ["structure", "files"] "complete" => ["structure", "files", "binaries", "configuration", "installation"] "security" => ["structure", "files", "binaries", "configuration", "installation", "security"] _ => ["structure", "files"] } for category in $validation_categories { let category_result = match $category { "structure" => { validate_package_structure $package_dir $validation_config } "files" => { validate_required_files $package_dir $validation_config } "binaries" => { validate_binaries $package_dir $validation_config } "configuration" => { validate_configuration $package_dir $validation_config } "installation" => { validate_installation_scripts $package_dir $validation_config } "security" => { validate_security $package_dir $validation_config } _ => { log warning $"Unknown validation category: ($category)" { category: $category, status: "skipped", reason: "unknown category" } } } let validation_results = ($validation_results | append $category_result) } # Generate validation report let validation_report = generate_validation_report $validation_results $validation_config # Cleanup if requested if $validation_config.cleanup { rm -rf ($validation_config.temp_workspace) log info "Cleaned up validation workspace" } let summary = { total_categories: ($validation_results | length) passed_categories: ($validation_results | where status == "passed" | length) failed_categories: ($validation_results | where status == "failed" | length) warnings: ($validation_results | get warnings | flatten | length) overall_status: (if ($validation_results | where status == "failed" | length) > 0 { "failed" } else { "passed" }) validation_config: $validation_config results: $validation_results report: $validation_report } # Output results in requested format output_validation_results $summary $validation_config if $summary.overall_status == "failed" { log error $"Package validation failed - ($summary.failed_categories) categories failed" exit 1 } else { log info $"Package validation passed - all ($summary.passed_categories) categories validated successfully" } return $summary } catch {|err| # Ensure cleanup even on error if $validation_config.cleanup and ($validation_config.temp_workspace | path exists) { rm -rf ($validation_config.temp_workspace) } log error $"Package validation failed: ($err.msg)" exit 1 } } # Prepare package for validation (extract if needed) def prepare_package_for_validation [validation_config: record] -> record { let package_path = $validation_config.package_path # Determine if package is an archive or directory if ($package_path | path type) == "dir" { # Package is already a directory return { status: "success" extracted_path: $package_path extraction_needed: false } } # Package is an archive, need to extract let extraction_dir = ($validation_config.temp_workspace | path join "extracted") mkdir $extraction_dir try { let package_name = ($package_path | path basename) if ($package_name | str ends-with ".tar.gz") or ($package_name | str ends-with ".tgz") { # Extract tar.gz cd $extraction_dir tar -xzf $package_path } else if ($package_name | str ends-with ".zip") { # Extract zip cd $extraction_dir unzip $package_path } else { return { status: "failed" reason: $"unsupported package format: ($package_name)" } } # Find the extracted directory (usually there's a single top-level directory) let extracted_contents = (ls $extraction_dir) let extracted_path = if ($extracted_contents | length) == 1 and (($extracted_contents | get 0.type) == "dir") { ($extracted_contents | get 0.name) } else { $extraction_dir } return { status: "success" extracted_path: $extracted_path extraction_needed: true } } catch {|err| return { status: "failed" reason: $"extraction failed: ($err.msg)" } } } # Validate package structure def validate_package_structure [ package_dir: string validation_config: record ] -> record { log info "Validating package structure..." let start_time = (date now) let mut structure_issues = [] let mut structure_warnings = [] # Define expected structure for provisioning packages let expected_structure = [ { path: "platform", type: "dir", required: true, description: "Platform binaries directory" }, { path: "core", type: "dir", required: true, description: "Core libraries directory" }, { path: "config", type: "dir", required: true, description: "Configuration directory" }, { path: "README.md", type: "file", required: false, description: "Package documentation" }, { path: "install.sh", type: "file", required: false, description: "Installation script (Unix)" }, { path: "install.bat", type: "file", required: false, description: "Installation script (Windows)" } ] # Check each expected component for component in $expected_structure { let component_path = ($package_dir | path join $component.path) let exists = ($component_path | path exists) let correct_type = if $exists { ($component_path | path type) == $component.type } else { false } if $component.required and not $exists { $structure_issues = ($structure_issues | append { type: "missing_required" path: $component.path description: $component.description severity: "error" }) } else if $exists and not $correct_type { $structure_issues = ($structure_issues | append { type: "incorrect_type" path: $component.path expected: $component.type actual: ($component_path | path type) severity: "error" }) } else if not $component.required and not $exists { $structure_warnings = ($structure_warnings | append { type: "missing_optional" path: $component.path description: $component.description severity: "warning" }) } } # Check for unexpected files at root level let root_files = (ls $package_dir | get name | each { path basename }) let expected_files = ($expected_structure | get path) let unexpected_files = ($root_files | where {|file| not ($file in $expected_files) }) for unexpected in $unexpected_files { $structure_warnings = ($structure_warnings | append { type: "unexpected_file" path: $unexpected severity: "info" }) } let status = if ($structure_issues | length) > 0 { "failed" } else { "passed" } { category: "structure" status: $status issues: $structure_issues warnings: $structure_warnings checks_performed: ($expected_structure | length) duration: ((date now) - $start_time) } } # Validate required files def validate_required_files [ package_dir: string validation_config: record ] -> record { log info "Validating required files..." let start_time = (date now) let mut file_issues = [] let mut file_warnings = [] # Check platform binaries let platform_dir = ($package_dir | path join "platform") if ($platform_dir | path exists) { let binaries = (find $platform_dir -type f -executable) if ($binaries | length) == 0 { $file_issues = ($file_issues | append { type: "no_executables" path: "platform/" description: "No executable binaries found in platform directory" severity: "error" }) } # Check for common binaries let expected_binaries = ["provisioning-orchestrator", "control-center"] for binary in $expected_binaries { let binary_files = ($binaries | where ($it =~ $binary)) if ($binary_files | length) == 0 { $file_warnings = ($file_warnings | append { type: "missing_binary" path: $"platform/($binary)" description: $"Expected binary not found: ($binary)" severity: "warning" }) } } } # Check core libraries let core_dir = ($package_dir | path join "core") if ($core_dir | path exists) { let essential_core_paths = ["lib", "bin/provisioning"] for core_path in $essential_core_paths { let full_path = ($core_dir | path join $core_path) if not ($full_path | path exists) { $file_issues = ($file_issues | append { type: "missing_core_component" path: $"core/($core_path)" description: $"Essential core component missing: ($core_path)" severity: "error" }) } } } # Check configuration files let config_dir = ($package_dir | path join "config") if ($config_dir | path exists) { let config_files = (find $config_dir -name "*.toml" -type f) if ($config_files | length) == 0 { $file_warnings = ($file_warnings | append { type: "no_config_files" path: "config/" description: "No TOML configuration files found" severity: "warning" }) } } # Validate file permissions let permission_issues = validate_file_permissions $package_dir $validation_config $file_issues = ($file_issues | append $permission_issues.issues) $file_warnings = ($file_warnings | append $permission_issues.warnings) let status = if ($file_issues | length) > 0 { "failed" } else { "passed" } { category: "files" status: $status issues: $file_issues warnings: $file_warnings checks_performed: 4 duration: ((date now) - $start_time) } } # Validate file permissions def validate_file_permissions [ package_dir: string validation_config: record ] -> record { let mut issues = [] let mut warnings = [] # Check that executables are actually executable let platform_dir = ($package_dir | path join "platform") if ($platform_dir | path exists) { let files = (ls $platform_dir | where type == file) for file in $files { let is_executable = try { # Check if file has execute permissions let perms = (ls -l $file.name | get 0.permissions) $perms =~ "x" } catch { false } if not $is_executable { $issues = ($issues | append { type: "not_executable" path: ($file.name | str replace $package_dir "") description: "Binary file is not executable" severity: "error" }) } } } return { issues: $issues, warnings: $warnings } } # Validate binaries def validate_binaries [ package_dir: string validation_config: record ] -> record { log info "Validating binaries..." let start_time = (date now) let mut binary_issues = [] let mut binary_warnings = [] let platform_dir = ($package_dir | path join "platform") if not ($platform_dir | path exists) { return { category: "binaries" status: "skipped" reason: "no platform directory found" issues: [] warnings: [] duration: ((date now) - $start_time) } } let binaries = (find $platform_dir -type f -executable) for binary in $binaries { let binary_result = validate_single_binary $binary $validation_config if $binary_result.status == "failed" { $binary_issues = ($binary_issues | append $binary_result.issues) } if ($binary_result.warnings | length) > 0 { $binary_warnings = ($binary_warnings | append $binary_result.warnings) } } let status = if ($binary_issues | length) > 0 { "failed" } else { "passed" } { category: "binaries" status: $status issues: $binary_issues warnings: $binary_warnings binaries_tested: ($binaries | length) checks_performed: ($binaries | length) duration: ((date now) - $start_time) } } # Validate a single binary def validate_single_binary [ binary_path: string validation_config: record ] -> record { let binary_name = ($binary_path | path basename) let mut issues = [] let mut warnings = [] if $validation_config.verbose { log info $"Validating binary: ($binary_name)" } # Check binary format try { let file_info = (file $binary_path) if not (($file_info =~ "ELF") or ($file_info =~ "Mach-O") or ($file_info =~ "PE32")) { $issues = ($issues | append { type: "invalid_binary_format" binary: $binary_name description: $"Binary format not recognized: ($file_info)" severity: "error" }) } # Check if binary is stripped (for release builds) if not ($file_info =~ "stripped") { $warnings = ($warnings | append { type: "not_stripped" binary: $binary_name description: "Binary contains debug symbols (not stripped)" severity: "info" }) } } catch {|err| $issues = ($issues | append { type: "file_analysis_failed" binary: $binary_name description: $"Failed to analyze binary: ($err.msg)" severity: "error" }) } # Test binary execution (if not skipped) if not $validation_config.skip_execution { let execution_result = test_binary_execution $binary_path $validation_config if $execution_result.status == "failed" { $issues = ($issues | append $execution_result.issues) } if ($execution_result.warnings | length) > 0 { $warnings = ($warnings | append $execution_result.warnings) } } let status = if ($issues | length) > 0 { "failed" } else { "passed" } return { status: $status binary: $binary_name issues: $issues warnings: $warnings } } # Test binary execution def test_binary_execution [ binary_path: string validation_config: record ] -> record { let binary_name = ($binary_path | path basename) let mut issues = [] let mut warnings = [] # Test basic execution (--help or --version) let help_commands = ["--help", "-h", "--version", "-V"] let mut execution_success = false for cmd in $help_commands { try { let result = (run-external --redirect-combine $binary_path $cmd | complete) if $result.exit_code == 0 { $execution_success = true break } } catch { # Continue to next command } } if not $execution_success { $issues = ($issues | append { type: "execution_failed" binary: $binary_name description: "Binary does not respond to common help commands" severity: "error" }) } # Check for missing dynamic dependencies (Linux/macOS) try { let ldd_result = match $nu.os-info.name { "linux" => { run-external --redirect-combine "ldd" $binary_path | complete } "macos" => { run-external --redirect-combine "otool" "-L" $binary_path | complete } _ => { { exit_code: 1, stdout: "", stderr: "" } } } if $ldd_result.exit_code == 0 and ($ldd_result.stdout =~ "not found") { $warnings = ($warnings | append { type: "missing_dependencies" binary: $binary_name description: "Binary has missing dynamic dependencies" severity: "warning" }) } } catch { # Dependency checking not available or failed } let status = if ($issues | length) > 0 { "failed" } else { "passed" } return { status: $status issues: $issues warnings: $warnings } } # Validate configuration def validate_configuration [ package_dir: string validation_config: record ] -> record { log info "Validating configuration..." let start_time = (date now) let mut config_issues = [] let mut config_warnings = [] let config_dir = ($package_dir | path join "config") if not ($config_dir | path exists) { return { category: "configuration" status: "skipped" reason: "no config directory found" issues: [] warnings: [] duration: ((date now) - $start_time) } } # Validate TOML files let toml_files = (find $config_dir -name "*.toml" -type f) for toml_file in $toml_files { let toml_result = validate_toml_file $toml_file $validation_config if $toml_result.status == "failed" { $config_issues = ($config_issues | append $toml_result.issues) } if ($toml_result.warnings | length) > 0 { $config_warnings = ($config_warnings | append $toml_result.warnings) } } let status = if ($config_issues | length) > 0 { "failed" } else { "passed" } { category: "configuration" status: $status issues: $config_issues warnings: $config_warnings config_files_tested: ($toml_files | length) checks_performed: ($toml_files | length) duration: ((date now) - $start_time) } } # Validate TOML file def validate_toml_file [ toml_file: string validation_config: record ] -> record { let file_name = ($toml_file | path basename) let mut issues = [] let mut warnings = [] try { # Try to parse TOML file let toml_content = (open $toml_file) # Check for essential sections (if it's a main config file) if ($file_name =~ "config") { let expected_sections = ["paths", "providers", "general"] for section in $expected_sections { if not ($section in ($toml_content | columns)) { $warnings = ($warnings | append { type: "missing_config_section" file: $file_name section: $section description: $"Configuration section missing: ($section)" severity: "warning" }) } } } } catch {|err| $issues = ($issues | append { type: "toml_parse_error" file: $file_name description: $"Failed to parse TOML file: ($err.msg)" severity: "error" }) } let status = if ($issues | length) > 0 { "failed" } else { "passed" } return { status: $status file: $file_name issues: $issues warnings: $warnings } } # Validate installation scripts def validate_installation_scripts [ package_dir: string validation_config: record ] -> record { log info "Validating installation scripts..." let start_time = (date now) let mut script_issues = [] let mut script_warnings = [] # Check for installation scripts let install_scripts = [ { name: "install.sh", platform: "unix" }, { name: "install.bat", platform: "windows" } ] let mut found_scripts = 0 for script in $install_scripts { let script_path = ($package_dir | path join $script.name) if ($script_path | path exists) { $found_scripts = $found_scripts + 1 let script_result = validate_install_script $script_path $script.platform $validation_config if $script_result.status == "failed" { $script_issues = ($script_issues | append $script_result.issues) } if ($script_result.warnings | length) > 0 { $script_warnings = ($script_warnings | append $script_result.warnings) } } } if $found_scripts == 0 { $script_warnings = ($script_warnings | append { type: "no_install_scripts" description: "No installation scripts found" severity: "warning" }) } let status = if ($script_issues | length) > 0 { "failed" } else { "passed" } { category: "installation" status: $status issues: $script_issues warnings: $script_warnings scripts_found: $found_scripts checks_performed: $found_scripts duration: ((date now) - $start_time) } } # Validate install script def validate_install_script [ script_path: string platform: string validation_config: record ] -> record { let script_name = ($script_path | path basename) let mut issues = [] let mut warnings = [] # Check script is executable let is_executable = try { let perms = (ls -l $script_path | get 0.permissions) $perms =~ "x" } catch { false } if not $is_executable and $platform == "unix" { $issues = ($issues | append { type: "script_not_executable" script: $script_name description: "Install script is not executable" severity: "error" }) } # Basic syntax check try { let content = (open $script_path --raw) if $platform == "unix" { # Check for shebang if not ($content | str starts-with "#!") { $warnings = ($warnings | append { type: "missing_shebang" script: $script_name description: "Script missing shebang line" severity: "warning" }) } } # Check for dangerous commands let dangerous_patterns = ["rm -rf /", "sudo rm -rf", "format c:"] for pattern in $dangerous_patterns { if ($content =~ $pattern) { $issues = ($issues | append { type: "dangerous_command" script: $script_name pattern: $pattern description: $"Script contains potentially dangerous command: ($pattern)" severity: "error" }) } } } catch {|err| $issues = ($issues | append { type: "script_read_error" script: $script_name description: $"Failed to read script: ($err.msg)" severity: "error" }) } let status = if ($issues | length) > 0 { "failed" } else { "passed" } return { status: $status script: $script_name issues: $issues warnings: $warnings } } # Validate security aspects def validate_security [ package_dir: string validation_config: record ] -> record { log info "Validating security..." let start_time = (date now) let mut security_issues = [] let mut security_warnings = [] # Check for potentially unsafe files let unsafe_patterns = ["*.tmp", "*.log", "*password*", "*secret*", "*.key", "*.pem"] for pattern in $unsafe_patterns { let found_files = (find $package_dir -name $pattern -type f) for file in $found_files { $security_warnings = ($security_warnings | append { type: "potentially_unsafe_file" path: ($file | str replace $package_dir "") description: $"File may contain sensitive information: ($file | path basename)" severity: "warning" }) } } # Check file permissions for security let world_writable = (find $package_dir -perm -o+w -type f) for file in $world_writable { $security_issues = ($security_issues | append { type: "world_writable_file" path: ($file | str replace $package_dir "") description: "File is world-writable" severity: "error" }) } let status = if ($security_issues | length) > 0 { "failed" } else { "passed" } { category: "security" status: $status issues: $security_issues warnings: $security_warnings checks_performed: 2 duration: ((date now) - $start_time) } } # Generate validation report def generate_validation_report [ validation_results: list validation_config: record ] -> record { let total_issues = ($validation_results | get issues | flatten | length) let total_warnings = ($validation_results | get warnings | flatten | length) let report = { timestamp: (date now) package_path: $validation_config.package_path validation_type: $validation_config.validation_type overall_status: (if ($validation_results | where status == "failed" | length) > 0 { "failed" } else { "passed" }) summary: { total_categories: ($validation_results | length) passed: ($validation_results | where status == "passed" | length) failed: ($validation_results | where status == "failed" | length) skipped: ($validation_results | where status == "skipped" | length) total_issues: $total_issues total_warnings: $total_warnings } categories: $validation_results all_issues: ($validation_results | get issues | flatten) all_warnings: ($validation_results | get warnings | flatten) } # Save report to file let report_file = ($validation_config.temp_workspace | path join "validation-report.json") $report | to json | save $report_file log info $"Validation report saved to: ($report_file)" return $report } # Output validation results in requested format def output_validation_results [ summary: record validation_config: record ] { match $validation_config.output_format { "json" => { $summary | to json } "table" => { print $"Package Validation Results for ($validation_config.package_path)" print $"Overall Status: ($summary.overall_status)" print "" $summary.results | select category status issues warnings | table if ($summary.results | get issues | flatten | length) > 0 { print "\nIssues Found:" $summary.results | get issues | flatten | table } } _ => { # Default text format print $"Package Validation Results" print $"=========================" print $"Package: ($validation_config.package_path)" print $"Validation Type: ($validation_config.validation_type)" print $"Overall Status: ($summary.overall_status)" print "" for result in $summary.results { print $"($result.category | str title): ($result.status | str upcase)" if ($result.issues | length) > 0 { print $" Issues: ($result.issues | length)" } if ($result.warnings | length) > 0 { print $" Warnings: ($result.warnings | length)" } } } } } # Quick validation command def "main quick" [package_path: string] { main $package_path --validation-type quick } # Security validation command def "main security" [package_path: string] { main $package_path --validation-type security }