diff --git a/internal/commands/asca/ascaconfig/asca-linux-amd.go b/internal/commands/asca/ascaconfig/asca-linux-amd.go index babfe4881..78b7bfcbf 100644 --- a/internal/commands/asca/ascaconfig/asca-linux-amd.go +++ b/internal/commands/asca/ascaconfig/asca-linux-amd.go @@ -7,10 +7,12 @@ import ( ) var Params = osinstaller.InstallationConfiguration{ - ExecutableFile: "vorpal_linux_x64", - DownloadURL: "https://download.checkmarx.com/vorpal-binary/vorpal_linux_x64.tar.gz", - HashDownloadURL: "https://download.checkmarx.com/vorpal-binary/hash.txt", - FileName: "vorpal.tar.gz", - HashFileName: "hash.txt", - WorkingDirName: "CxVorpal", + ExecutableFile: "vorpal_linux_x64", + DownloadURL: "https://download.checkmarx.com/vorpal-binary/vorpal_linux_x64.tar.gz", + HashDownloadURL: "https://download.checkmarx.com/vorpal-binary/hash.txt", + FileName: "vorpal.tar.gz", + HashFileName: "hash.txt", + WorkingDirName: "CxVorpal", + ArchiveChecksumDownloadURL: "https://download.checkmarx.com/vorpal-binary/checksums.sha256", + ArchiveChecksumFileName: "checksums.sha256", } diff --git a/internal/commands/asca/ascaconfig/asca-linux-arm.go b/internal/commands/asca/ascaconfig/asca-linux-arm.go index 5763acb15..1825ff3ab 100644 --- a/internal/commands/asca/ascaconfig/asca-linux-arm.go +++ b/internal/commands/asca/ascaconfig/asca-linux-arm.go @@ -7,10 +7,12 @@ import ( ) var Params = osinstaller.InstallationConfiguration{ - ExecutableFile: "vorpal_linux_arm64", - DownloadURL: "https://download.checkmarx.com/vorpal-binary/vorpal_linux_arm64.tar.gz", - HashDownloadURL: "https://download.checkmarx.com/vorpal-binary/hash.txt", - FileName: "vorpal.tar.gz", - HashFileName: "hash.txt", - WorkingDirName: "CxVorpal", + ExecutableFile: "vorpal_linux_arm64", + DownloadURL: "https://download.checkmarx.com/vorpal-binary/vorpal_linux_arm64.tar.gz", + HashDownloadURL: "https://download.checkmarx.com/vorpal-binary/hash.txt", + FileName: "vorpal.tar.gz", + HashFileName: "hash.txt", + WorkingDirName: "CxVorpal", + ArchiveChecksumDownloadURL: "https://download.checkmarx.com/vorpal-binary/checksums.sha256", + ArchiveChecksumFileName: "checksums.sha256", } diff --git a/internal/commands/asca/ascaconfig/asca-mac-amd.go b/internal/commands/asca/ascaconfig/asca-mac-amd.go index 5a05c2100..8c67e93ba 100644 --- a/internal/commands/asca/ascaconfig/asca-mac-amd.go +++ b/internal/commands/asca/ascaconfig/asca-mac-amd.go @@ -7,10 +7,12 @@ import ( ) var Params = osinstaller.InstallationConfiguration{ - ExecutableFile: "vorpal_darwin_x64", - DownloadURL: "https://download.checkmarx.com/vorpal-binary/vorpal_darwin_x64.tar.gz", - HashDownloadURL: "https://download.checkmarx.com/vorpal-binary/hash.txt", - FileName: "vorpal.tar.gz", - HashFileName: "hash.txt", - WorkingDirName: "CxVorpal", + ExecutableFile: "vorpal_darwin_x64", + DownloadURL: "https://download.checkmarx.com/vorpal-binary/vorpal_darwin_x64.tar.gz", + HashDownloadURL: "https://download.checkmarx.com/vorpal-binary/hash.txt", + FileName: "vorpal.tar.gz", + HashFileName: "hash.txt", + WorkingDirName: "CxVorpal", + ArchiveChecksumDownloadURL: "https://download.checkmarx.com/vorpal-binary/checksums.sha256", + ArchiveChecksumFileName: "checksums.sha256", } diff --git a/internal/commands/asca/ascaconfig/asca-mac-arm.go b/internal/commands/asca/ascaconfig/asca-mac-arm.go index 49bfa7625..cd75418e5 100644 --- a/internal/commands/asca/ascaconfig/asca-mac-arm.go +++ b/internal/commands/asca/ascaconfig/asca-mac-arm.go @@ -7,10 +7,12 @@ import ( ) var Params = osinstaller.InstallationConfiguration{ - ExecutableFile: "vorpal_darwin_arm64", - DownloadURL: "https://download.checkmarx.com/vorpal-binary/vorpal_darwin_arm64.tar.gz", - HashDownloadURL: "https://download.checkmarx.com/vorpal-binary/hash.txt", - FileName: "vorpal.tar.gz", - HashFileName: "hash.txt", - WorkingDirName: "CxVorpal", + ExecutableFile: "vorpal_darwin_arm64", + DownloadURL: "https://download.checkmarx.com/vorpal-binary/vorpal_darwin_arm64.tar.gz", + HashDownloadURL: "https://download.checkmarx.com/vorpal-binary/hash.txt", + FileName: "vorpal.tar.gz", + HashFileName: "hash.txt", + WorkingDirName: "CxVorpal", + ArchiveChecksumDownloadURL: "https://download.checkmarx.com/vorpal-binary/checksums.sha256", + ArchiveChecksumFileName: "checksums.sha256", } diff --git a/internal/commands/asca/ascaconfig/asca-windows.go b/internal/commands/asca/ascaconfig/asca-windows.go index 43893e60e..f10021d71 100644 --- a/internal/commands/asca/ascaconfig/asca-windows.go +++ b/internal/commands/asca/ascaconfig/asca-windows.go @@ -7,10 +7,12 @@ import ( ) var Params = osinstaller.InstallationConfiguration{ - ExecutableFile: "vorpal_windows_x64.exe", - DownloadURL: "https://download.checkmarx.com/vorpal-binary/vorpal_windows_x64.zip", - HashDownloadURL: "https://download.checkmarx.com/vorpal-binary/hash.txt", - FileName: "vorpal.zip", - HashFileName: "hash.txt", - WorkingDirName: "CxVorpal", + ExecutableFile: "vorpal_windows_x64.exe", + DownloadURL: "https://download.checkmarx.com/vorpal-binary/vorpal_windows_x64.zip", + HashDownloadURL: "https://download.checkmarx.com/vorpal-binary/hash.txt", + FileName: "vorpal.zip", + HashFileName: "hash.txt", + WorkingDirName: "CxVorpal", + ArchiveChecksumDownloadURL: "https://download.checkmarx.com/vorpal-binary/checksums.sha256", + ArchiveChecksumFileName: "checksums.sha256", } diff --git a/internal/services/osinstaller/os-installer-structs.go b/internal/services/osinstaller/os-installer-structs.go index 12f61cc52..38e18a335 100644 --- a/internal/services/osinstaller/os-installer-structs.go +++ b/internal/services/osinstaller/os-installer-structs.go @@ -3,6 +3,9 @@ package osinstaller import ( "os" "path/filepath" + "strings" + + "github.com/pkg/errors" ) type InstallationConfiguration struct { @@ -12,6 +15,9 @@ type InstallationConfiguration struct { FileName string HashFileName string WorkingDirName string + // Vorpal: per-artifact checksum URL for binary verification + ArchiveChecksumDownloadURL string + ArchiveChecksumFileName string } func (i *InstallationConfiguration) ExecutableFilePath() string { @@ -40,3 +46,32 @@ func (i *InstallationConfiguration) WorkingDir() string { } return filepath.Join(basePath, i.WorkingDirName) } + +// BinaryFilePath returns the path to the downloaded archive on disk (before extraction). +func (i *InstallationConfiguration) BinaryFilePath() string { + return filepath.Join(i.WorkingDir(), i.FileName) +} + +// ArchiveChecksumFilePath is the local path for the optional per-artifact checksum file. +func (i *InstallationConfiguration) ArchiveChecksumFilePath() string { + if i.ArchiveChecksumFileName == "" { + return "" + } + return filepath.Join(i.WorkingDir(), i.ArchiveChecksumFileName) +} + +// resolveArchiveChecksumVerification returns the local sha256sum path to verify against, and whether it must be downloaded first. +func (i *InstallationConfiguration) resolveArchiveChecksumVerification() (localPath string, needsExtraDownload bool, err error) { + if i.ArchiveChecksumDownloadURL != "" { + if i.ArchiveChecksumFileName == "" { + return "", false, errors.New("ArchiveChecksumFileName is required when ArchiveChecksumDownloadURL is set") + } + return i.ArchiveChecksumFilePath(), true, nil + } + + if strings.HasSuffix(i.HashFileName, ".sha256sum") { + return i.HashFilePath(), false, nil + } + + return "", false, errors.New("ChecksumFileName is required for sha verification.") +} diff --git a/internal/services/osinstaller/os-installer.go b/internal/services/osinstaller/os-installer.go index f686cbf9c..0b136e801 100644 --- a/internal/services/osinstaller/os-installer.go +++ b/internal/services/osinstaller/os-installer.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "time" "github.com/checkmarx/ast-cli/internal/logger" @@ -73,16 +74,39 @@ func InstallOrUpgrade(installationConfiguration *InstallationConfiguration, asca return false, err } - // Download hash file + // Hash file serves different purposes: version check for Vorpal, both version check and verification for SCA err = downloadHashFile(installationConfiguration.HashDownloadURL, installationConfiguration.HashFilePath()) if err != nil { return false, err } + // Must shut down service before replacement to release file locks if ascaWrapper != nil { shutDownAndWait(ascaWrapper) } + checksumPath, needsArchiveChecksumDownload, err := installationConfiguration.resolveArchiveChecksumVerification() + if err != nil { + _ = os.Remove(installationConfiguration.BinaryFilePath()) + return false, errors.Errorf("Installation failed due to an invalid checksum for %s", installationConfiguration.FileName) + } + if needsArchiveChecksumDownload { + err = downloadFile(installationConfiguration.ArchiveChecksumDownloadURL, checksumPath) + if err != nil { + return false, err + } + } + if checksumPath != "" { + err = verifyArchiveAgainstSHA256SumFile(installationConfiguration.BinaryFilePath(), checksumPath, installationConfiguration.DownloadURL) + if err != nil { + _ = os.Remove(installationConfiguration.BinaryFilePath()) + return false, errors.Errorf("Installation failed due to an invalid checksum for %s", installationConfiguration.FileName) + } + } else { + _ = os.Remove(installationConfiguration.BinaryFilePath()) + return false, errors.Errorf("Installation failed due to an invalid checksum for %s", installationConfiguration.FileName) + } + // Unzip or extract downloaded zip depending on which OS is running err = UnzipOrExtractFiles(installationConfiguration) if err != nil { @@ -197,3 +221,90 @@ func shutDownAndWait(ascaWrapper grpcs.AscaWrapper) { } logger.PrintIfVerbose("Timed out waiting for Vorpal service to stop; proceeding anyway.") } + +const ( + sha256SumFileMinFields = 2 + sha256HexLength = 64 + checksumVerificationFailed = "Checksum verification failed." +) + +// verifyArchiveAgainstSHA256SumFile checks archivePath against its digest in a GNU sha256sum-style file, +// matching by downloadURL's filename, or falling back to a single-line checksum format. +func verifyArchiveAgainstSHA256SumFile(archivePath, sha256SumFilePath, downloadURL string) error { + content, err := os.ReadFile(sha256SumFilePath) + if err != nil { + return errors.Errorf(checksumVerificationFailed) + } + + fileContent := strings.TrimSpace(string(content)) + if fileContent == "" { + return errors.New(checksumVerificationFailed) + } + + // Extract the actual platform-specific filename from downloadURL + _, downloadFileName := filepath.Split(downloadURL) + expectedHash := "" + + // Try to find matching filename in checksums file + for _, line := range strings.Split(fileContent, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + fields := strings.Fields(line) + if len(fields) < sha256SumFileMinFields { + continue + } + + hash := strings.ToLower(fields[0]) + filename := fields[len(fields)-1] + + // Check if this line matches the download filename + if filename == downloadFileName { + expectedHash = hash + break + } + } + + // If no exact match found, fall back to first line (single-line format) + if expectedHash == "" { + fields := strings.Fields(fileContent) + if len(fields) < 1 { + return errors.New(checksumVerificationFailed) + } + expectedHash = strings.ToLower(fields[0]) + } + + if len(expectedHash) != sha256HexLength { + return errors.Errorf(checksumVerificationFailed) + } + + actualHash, err := calculateSHA256(archivePath) + if err != nil { + return errors.Errorf(checksumVerificationFailed) + } + + if !strings.EqualFold(expectedHash, actualHash) { + return errors.New(checksumVerificationFailed) + } + return nil +} + +// calculateSHA256 calculates the SHA256 hash of a file +func calculateSHA256(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer func() { + _ = file.Close() + }() + + hasher := sha256.New() + if _, err := io.Copy(hasher, file); err != nil { + return "", err + } + + return fmt.Sprintf("%x", hasher.Sum(nil)), nil +}