provisioning/tools/package/validate-package.nu
2025-10-07 11:12:02 +01:00

939 lines
30 KiB
Plaintext

#!/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
}