Skip to content
14 changes: 8 additions & 6 deletions internal/commands/asca/ascaconfig/asca-linux-amd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
14 changes: 8 additions & 6 deletions internal/commands/asca/ascaconfig/asca-linux-arm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
14 changes: 8 additions & 6 deletions internal/commands/asca/ascaconfig/asca-mac-amd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
14 changes: 8 additions & 6 deletions internal/commands/asca/ascaconfig/asca-mac-arm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
14 changes: 8 additions & 6 deletions internal/commands/asca/ascaconfig/asca-windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
35 changes: 35 additions & 0 deletions internal/services/osinstaller/os-installer-structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package osinstaller
import (
"os"
"path/filepath"
"strings"

"github.com/pkg/errors"
)

type InstallationConfiguration struct {
Expand All @@ -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 {
Expand Down Expand Up @@ -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.")
}
113 changes: 112 additions & 1 deletion internal/services/osinstaller/os-installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/checkmarx/ast-cli/internal/logger"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Loading