provisioning/tools/package/generate-checksums.nu

542 lines
16 KiB
Plaintext
Raw Normal View History

2025-10-07 11:12:02 +01:00
#!/usr/bin/env nu
# Checksum generation tool - generates and verifies checksums for distribution packages
#
# Generates:
# - SHA256 checksums (primary)
# - MD5 checksums (legacy compatibility)
# - SHA512 checksums (high security)
# - File integrity verification
use std log
def main [
--input-dir: string = "packages"
--output-file: string = ""
--algorithms: string = "sha256,md5"
--format: string = "standard"
--verify: string = ""
--recursive
--pattern: string = "*"
--verbose
] {
2025-10-07 11:12:02 +01:00
let input_root = ($input_dir | path expand)
let algorithms_list = if $algorithms == "all" {
["sha256", "md5", "sha512"]
} else {
($algorithms | split row "," | each { str trim })
}
let checksum_config = {
input_dir: $input_root
output_file: (if $output_file == "" {
($input_root | path join $"checksums-($algorithms_list.0).txt")
} else {
$output_file | path expand
})
algorithms: $algorithms_list
format: $format
verify_file: (if $verify == "" { "" } else { $verify | path expand })
recursive: $recursive
pattern: $pattern
verbose: $verbose
}
log info $"Starting checksum generation with config: ($checksum_config)"
# Validate input directory
if not ($input_root | path exists) {
log error $"Input directory does not exist: ($input_root)"
exit 1
}
# If verifying, run verification instead
if $checksum_config.verify_file != "" {
verify_checksums $checksum_config
return
2025-10-07 11:12:02 +01:00
}
# Find files to checksum
let files = find_checksum_files $checksum_config
if ($files | length) == 0 {
log warning "No files found to checksum"
return {
status: "skipped"
reason: "no files found"
files_processed: 0
}
}
log info $"Found ($files | length) files to checksum"
# Generate checksums
let checksum_results = generate_checksums_for_files $files $checksum_config
# Save checksums to file
let save_result = save_checksums $checksum_results $checksum_config
let summary = {
total_files: ($files | length)
successful_files: ($checksum_results | where status == "success" | length)
failed_files: ($checksum_results | where status == "failed" | length)
algorithms_used: $checksum_config.algorithms
output_file: $checksum_config.output_file
checksum_config: $checksum_config
results: $checksum_results
save_result: $save_result
}
if $summary.failed_files > 0 {
log error $"Checksum generation completed with ($summary.failed_files) failures"
exit 1
} else {
log info $"Checksum generation completed successfully - ($summary.successful_files) files processed"
}
return $summary
}
# Find files to generate checksums for
def find_checksum_files [checksum_config: record] {
2025-10-07 11:12:02 +01:00
let find_command = if $checksum_config.recursive {
$"find ($checksum_config.input_dir) -name \"($checksum_config.pattern)\" -type f"
} else {
$"find ($checksum_config.input_dir) -maxdepth 1 -name \"($checksum_config.pattern)\" -type f"
}
let found_files = (bash -c $find_command | lines | where $it != "")
# Filter out checksum files themselves
$found_files | where {|file|
not ($file =~ r"checksum" or $file =~ r"\.sha256$" or $file =~ r"\.md5$" or $file =~ r"\.sha512$")
2025-10-07 11:12:02 +01:00
}
}
# Generate checksums for all files
def generate_checksums_for_files [
files: list
checksum_config: record
] {
2025-10-07 11:12:02 +01:00
$files | each {|file|
generate_checksums_for_file $file $checksum_config
}
}
# Generate checksums for a single file
def generate_checksums_for_file [
file: string
checksum_config: record
] {
2025-10-07 11:12:02 +01:00
if $checksum_config.verbose {
log info $"Generating checksums for: ($file)"
}
let start_time = (date now)
# Get file info
let file_info = (ls $file | get 0)
let relative_path = ($file | str replace $checksum_config.input_dir "" | str trim --left "/")
2025-10-07 11:12:02 +01:00
# Generate checksums for each algorithm
let checksums_list = ($checksum_config.algorithms | each {|algorithm|
let result = (do {
match $algorithm {
2025-10-07 11:12:02 +01:00
"sha256" => { generate_sha256 $file }
"md5" => { generate_md5 $file }
"sha512" => { generate_sha512 $file }
_ => {
error make {msg: $"Unknown algorithm: ($algorithm)"}
2025-10-07 11:12:02 +01:00
}
}
} | complete)
2025-10-07 11:12:02 +01:00
if $result.exit_code == 0 {
{algorithm: $algorithm, checksum: $result.stdout}
} else {
{algorithm: $algorithm, error: $result.stderr}
2025-10-07 11:12:02 +01:00
}
})
# Build checksums record from successful results
let checksums_record = ($checksums_list
| where {|item| $item | has "checksum"}
| each {|item| {($item.algorithm): $item.checksum}}
| reduce {|item, acc| $acc | merge $item})
# Collect errors from failed results
let errors = ($checksums_list | where {|item| $item | has "error"} | each {|item| $item.error})
2025-10-07 11:12:02 +01:00
{
file: $file
relative_path: $relative_path
status: (if ($errors | length) > 0 { "failed" } else { "success" })
size: $file_info.size
modified: $file_info.modified
checksums: $checksums_record
2025-10-07 11:12:02 +01:00
errors: $errors
duration: ((date now) - $start_time)
}
}
# Generate SHA256 checksum
def generate_sha256 [file: string] {
let result = (bash -c $"shasum -a 256 \"($file)\" 2>&1" | complete)
2025-10-07 11:12:02 +01:00
if $result.exit_code == 0 {
($result.stdout | split row " " | get 0)
} else {
# Fallback to other SHA256 tools
let openssl_result = (bash -c $"openssl sha256 \"($file)\" 2>&1" | complete)
2025-10-07 11:12:02 +01:00
if $openssl_result.exit_code == 0 {
($openssl_result.stdout | str replace $"SHA256\\(.*\\)= " "" | str trim)
} else {
error make {msg: $"Failed to generate SHA256 for ($file)"}
2025-10-07 11:12:02 +01:00
}
}
}
# Generate MD5 checksum
def generate_md5 [file: string] {
let result = (bash -c $"md5sum \"($file)\" 2>&1" | complete)
2025-10-07 11:12:02 +01:00
if $result.exit_code == 0 {
($result.stdout | split row " " | get 0)
} else {
# Fallback for macOS
let md5_result = (bash -c $"md5 -r \"($file)\" 2>&1" | complete)
2025-10-07 11:12:02 +01:00
if $md5_result.exit_code == 0 {
($md5_result.stdout | split row " " | get 0)
} else {
# Fallback to openssl
let openssl_result = (bash -c $"openssl md5 \"($file)\" 2>&1" | complete)
2025-10-07 11:12:02 +01:00
if $openssl_result.exit_code == 0 {
($openssl_result.stdout | str replace $"MD5\\(.*\\)= " "" | str trim)
} else {
error make {msg: $"Failed to generate MD5 for ($file)"}
2025-10-07 11:12:02 +01:00
}
}
}
}
# Generate SHA512 checksum
def generate_sha512 [file: string] {
let result = (bash -c $"shasum -a 512 \"($file)\" 2>&1" | complete)
2025-10-07 11:12:02 +01:00
if $result.exit_code == 0 {
($result.stdout | split row " " | get 0)
} else {
# Fallback to openssl
let openssl_result = (bash -c $"openssl sha512 \"($file)\" 2>&1" | complete)
2025-10-07 11:12:02 +01:00
if $openssl_result.exit_code == 0 {
($openssl_result.stdout | str replace $"SHA512\\(.*\\)= " "" | str trim)
} else {
error make {msg: $"Failed to generate SHA512 for ($file)"}
2025-10-07 11:12:02 +01:00
}
}
}
# Save checksums to file
def save_checksums [
checksum_results: list
checksum_config: record
] {
2025-10-07 11:12:02 +01:00
log info $"Saving checksums to: ($checksum_config.output_file)"
let successful_results = ($checksum_results | where status == "success")
let result = (do {
2025-10-07 11:12:02 +01:00
match $checksum_config.format {
"standard" => {
save_standard_format $successful_results $checksum_config
}
"json" => {
save_json_format $successful_results $checksum_config
}
"csv" => {
save_csv_format $successful_results $checksum_config
}
_ => {
error make {msg: $"Unknown format: ($checksum_config.format)"}
2025-10-07 11:12:02 +01:00
}
}
{
status: "success"
format: $checksum_config.format
file: $checksum_config.output_file
entries: ($successful_results | length)
}
} | complete)
2025-10-07 11:12:02 +01:00
if $result.exit_code != 0 {
2025-10-07 11:12:02 +01:00
{
status: "failed"
reason: $result.stderr
2025-10-07 11:12:02 +01:00
}
} else {
$result.stdout
2025-10-07 11:12:02 +01:00
}
}
# Save in standard format (compatible with shasum/md5sum)
def save_standard_format [
results: list
checksum_config: record
] {
# Build header
let header = [
$"# Checksums generated on (date now)"
$"# Algorithms: ($checksum_config.algorithms | str join ', ')"
""
]
# Build checksums sections for each algorithm
let checksums_sections = ($checksum_config.algorithms | each {|algorithm|
([$"# ($algorithm | str upcase) checksums"] +
($results
| where {|result| $algorithm in ($result.checksums | columns)}
| each {|result|
2025-10-07 11:12:02 +01:00
let checksum = ($result.checksums | get $algorithm)
$"($checksum) ($result.relative_path)"
})
+ [""])
} | flatten)
2025-10-07 11:12:02 +01:00
# Combine all lines
(($header + $checksums_sections) | str join "\n") | save $checksum_config.output_file
2025-10-07 11:12:02 +01:00
}
# Save in JSON format
def save_json_format [
results: list
checksum_config: record
] {
let output_data = {
generated_at: (date now)
algorithms: $checksum_config.algorithms
total_files: ($results | length)
files: ($results | each {|result|
{
file: $result.relative_path
size: $result.size
modified: $result.modified
checksums: $result.checksums
}
})
}
$output_data | to json | save $checksum_config.output_file
}
# Save in CSV format
def save_csv_format [
results: list
checksum_config: record
] {
# Create header row
let header = (["file", "size", "modified"] + $checksum_config.algorithms | str join ",")
# Build data rows
let data_rows = ($results | each {|result|
let base_row = [
$result.relative_path
($result.size | into string)
($result.modified | format date "%Y-%m-%d %H:%M:%S")
]
let checksum_row = ($checksum_config.algorithms | each {|algorithm|
if ($algorithm in ($result.checksums | columns)) {
2025-10-07 11:12:02 +01:00
($result.checksums | get $algorithm)
} else {
""
}
})
2025-10-07 11:12:02 +01:00
(($base_row + $checksum_row) | str join ",")
})
2025-10-07 11:12:02 +01:00
# Combine header and data
([$header] + $data_rows | str join "\n") | save $checksum_config.output_file
2025-10-07 11:12:02 +01:00
}
# Verify checksums from existing file
def verify_checksums [checksum_config: record] {
2025-10-07 11:12:02 +01:00
log info $"Verifying checksums from: ($checksum_config.verify_file)"
if not ($checksum_config.verify_file | path exists) {
log error $"Checksum file does not exist: ($checksum_config.verify_file)"
exit 1
}
let checksum_content = (open $checksum_config.verify_file --raw)
let verification_results = parse_and_verify_checksums $checksum_content $checksum_config
let summary = {
total_verified: ($verification_results | length)
successful_verifications: ($verification_results | where status == "success" | length)
failed_verifications: ($verification_results | where status == "failed" | length)
missing_files: ($verification_results | where status == "missing" | length)
checksum_file: $checksum_config.verify_file
results: $verification_results
}
if ($summary.failed_verifications > 0) or ($summary.missing_files > 0) {
log error $"Checksum verification failed: ($summary.failed_verifications) mismatches, ($summary.missing_files) missing files"
exit 1
} else {
log info $"Checksum verification completed successfully - all ($summary.successful_verifications) files verified"
}
return $summary
}
# Parse and verify checksums from file content
def parse_and_verify_checksums [
content: string
checksum_config: record
] {
2025-10-07 11:12:02 +01:00
let lines = ($content | lines | where {|line|
not ($line | str starts-with "#") and ($line | str trim) != ""
})
$lines | each {|line|
let parts = ($line | split row " " | where $it != "")
if ($parts | length) >= 2 {
let expected_checksum = ($parts | get 0)
let file_path = ($parts | get 1)
let full_path = ($checksum_config.input_dir | path join $file_path)
verify_single_checksum $expected_checksum $full_path $file_path
} else {
{
file: "unknown"
status: "failed"
reason: $"Invalid checksum line: ($line)"
}
}
}
}
# Verify a single checksum
def verify_single_checksum [
expected_checksum: string
full_path: string
relative_path: string
] {
2025-10-07 11:12:02 +01:00
if not ($full_path | path exists) {
return {
file: $relative_path
status: "missing"
expected: $expected_checksum
actual: ""
reason: "file not found"
}
}
# Determine algorithm by checksum length
let algorithm = match ($expected_checksum | str length) {
32 => "md5"
64 => "sha256"
128 => "sha512"
_ => "unknown"
}
2025-10-07 11:12:02 +01:00
let result = (do {
2025-10-07 11:12:02 +01:00
let actual_checksum = match $algorithm {
"md5" => { generate_md5 $full_path }
"sha256" => { generate_sha256 $full_path }
"sha512" => { generate_sha512 $full_path }
_ => { "" }
}
if $actual_checksum == $expected_checksum {
{
file: $relative_path
status: "success"
algorithm: $algorithm
expected: $expected_checksum
actual: $actual_checksum
}
} else {
{
file: $relative_path
status: "failed"
algorithm: $algorithm
expected: $expected_checksum
actual: $actual_checksum
reason: "checksum mismatch"
}
}
} | complete)
2025-10-07 11:12:02 +01:00
if $result.exit_code != 0 {
2025-10-07 11:12:02 +01:00
{
file: $relative_path
status: "failed"
expected: $expected_checksum
actual: ""
reason: $result.stderr
2025-10-07 11:12:02 +01:00
}
} else {
$result.stdout
2025-10-07 11:12:02 +01:00
}
}
# Show checksum file info
def "main info" [checksum_file: string = ""] {
if $checksum_file == "" {
# Look for checksum files in current directory
let checksum_files = (bash -c "find . -maxdepth 1 \\( -name '*checksum*' -o -name '*.sha256' -o -name '*.md5' -o -name '*.sha512' \\) -type f" | lines | where $it != "")
2025-10-07 11:12:02 +01:00
return {
current_directory: $env.PWD
checksum_files_found: ($checksum_files | length)
files: $checksum_files
}
}
let checksum_path = ($checksum_file | path expand)
if not ($checksum_path | path exists) {
return { error: "checksum file not found", file: $checksum_path }
}
let content = (open $checksum_path --raw)
let lines = ($content | lines)
let checksum_lines = ($lines | where {|line|
not ($line | str starts-with "#") and ($line | str trim) != ""
})
# Analyze checksum types
let algorithms_detected = ($checksum_lines | each {|line|
let parts = ($line | split row " ")
if ($parts | length) >= 2 {
let checksum = ($parts | get 0)
match ($checksum | str length) {
32 => "md5"
64 => "sha256"
128 => "sha512"
_ => "unknown"
}
} else {
"unknown"
}
} | uniq)
{
file: $checksum_path
total_lines: ($lines | length)
checksum_lines: ($checksum_lines | length)
algorithms_detected: $algorithms_detected
size: (ls $checksum_path | get 0.size)
modified: (ls $checksum_path | get 0.modified)
}
}
# Quick verify command
def "main verify" [
checksum_file: string
--input-dir: string = "."
2025-10-07 11:12:02 +01:00
] {
main --verify $checksum_file --input-dir $input_dir
}