mirror of
https://github.com/safedep/vet.git
synced 2025-12-10 12:07:30 -06:00
311 lines
9.8 KiB
Go
311 lines
9.8 KiB
Go
package inspect
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"buf.build/gen/go/safedep/api/grpc/go/safedep/services/malysis/v1/malysisv1grpc"
|
|
malysisv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1"
|
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
|
malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/malysis/v1"
|
|
"github.com/jedib0t/go-pretty/v6/table"
|
|
"github.com/safedep/dry/adapters"
|
|
"github.com/safedep/dry/api/pb"
|
|
"github.com/safedep/dry/utils"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/safedep/vet/internal/analytics"
|
|
"github.com/safedep/vet/internal/auth"
|
|
"github.com/safedep/vet/internal/ui"
|
|
"github.com/safedep/vet/pkg/common/registry"
|
|
vetutils "github.com/safedep/vet/pkg/common/utils"
|
|
"github.com/safedep/vet/pkg/malysis"
|
|
"github.com/safedep/vet/pkg/reporter"
|
|
)
|
|
|
|
var (
|
|
malwareAnalysisPackageUrl string
|
|
malwareAnalysisTimeout time.Duration
|
|
malwareAnalysisReportJSON string
|
|
malwareAnalysisReportOSV string
|
|
malwareAnalysisNoWait bool
|
|
|
|
malwareReportOSVFinderName string
|
|
malwareReportOSVContacts []string
|
|
malwareReportOSVReferenceURL string
|
|
malwareReportOSVUseRange bool
|
|
)
|
|
|
|
func newPackageMalwareInspectCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "malware",
|
|
Short: "Inspect an OSS package for malware",
|
|
Long: `Inspect an OSS package for malware using SafeDep Malware Analysis API`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
err := executeMalwareAnalysis()
|
|
if err != nil {
|
|
ui.PrintError("Failed: %v", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&malwareAnalysisPackageUrl, "purl", "",
|
|
"Package URL to inspect for malware")
|
|
cmd.Flags().DurationVar(&malwareAnalysisTimeout, "timeout", 5*time.Minute,
|
|
"Timeout for malware analysis")
|
|
cmd.Flags().StringVar(&malwareAnalysisReportJSON, "report-json", "",
|
|
"Path to save malware analysis report in JSON format")
|
|
cmd.Flags().StringVar(&malwareAnalysisReportOSV, "report-osv", "",
|
|
"Dir path to save malware analysis report in OSV format and ossf/malicious-packages format")
|
|
cmd.Flags().BoolVar(&malwareAnalysisNoWait, "no-wait", false,
|
|
"Do not wait for malware analysis to complete")
|
|
cmd.Flags().StringVar(&malwareReportOSVFinderName, "report-osv-finder-name", "",
|
|
"Finder name for malware analysis report in OSV format")
|
|
cmd.Flags().StringSliceVar(&malwareReportOSVContacts, "report-osv-contacts", []string{},
|
|
"Contacts for malware analysis report in OSV format (URL, email, etc.)")
|
|
cmd.Flags().StringVar(&malwareReportOSVReferenceURL, "report-osv-reference-url", "",
|
|
"Custom reference URL for malware analysis report (defaults to app.safedep.io)")
|
|
cmd.Flags().BoolVar(&malwareReportOSVUseRange, "report-osv-with-ranges", false,
|
|
"Use range-based versioning in OSV report (default: use explicit versions)")
|
|
|
|
_ = cmd.MarkFlagRequired("purl")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func executeMalwareAnalysis() error {
|
|
analytics.TrackCommandInspectMalwareAnalysis()
|
|
|
|
err := auth.Verify()
|
|
if err != nil {
|
|
return fmt.Errorf("access to Malicious Package Analysis requires an API key. " +
|
|
"For more details: https://docs.safedep.io/cloud/quickstart/")
|
|
}
|
|
|
|
cc, err := auth.MalwareAnalysisClientConnection("malware-analysis")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
service := malysisv1grpc.NewMalwareAnalysisServiceClient(cc)
|
|
|
|
purl, err := pb.NewPurlPackageVersion(malwareAnalysisPackageUrl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
githubClient, err := adapters.NewGithubClient(adapters.DefaultGitHubClientConfig())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create GitHub client: %v", err)
|
|
}
|
|
|
|
versionResolver, err := registry.NewPackageVersionResolver(githubClient)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create package version resolver: %v", err)
|
|
}
|
|
|
|
packageVersion := purl.PackageVersion()
|
|
|
|
// If package version is empty or latest replace it with actual literal latest version
|
|
// Reference: https://github.com/safedep/vet/issues/446
|
|
if packageVersion.GetVersion() == "" || packageVersion.GetVersion() == "latest" {
|
|
ui.PrintMsg("Resolving package version")
|
|
version, err := versionResolver.ResolvePackageLatestVersion(purl.Ecosystem(), purl.Name())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve package latest version: %v", err)
|
|
}
|
|
|
|
ui.PrintSuccess("Resolved package version: %s", version)
|
|
packageVersion.Version = version
|
|
}
|
|
|
|
ctx := context.Background()
|
|
ctx, cancelFun := context.WithTimeout(ctx, malwareAnalysisTimeout)
|
|
|
|
defer cancelFun()
|
|
|
|
// For GitHub Actions packages, we need to resolve the commit hash
|
|
if packageVersion.GetPackage().GetEcosystem() == packagev1.Ecosystem_ECOSYSTEM_GITHUB_ACTIONS {
|
|
ui.PrintMsg("Resolving commit hash for GitHub Actions package")
|
|
|
|
commitHash, err := resolveGitHubActionsCommitHash(ctx, packageVersion)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve commit hash for GitHub Actions package: %v", err)
|
|
}
|
|
|
|
ui.PrintSuccess("Resolved commit hash for GitHub Actions package: %s", commitHash)
|
|
packageVersion.Version = commitHash
|
|
}
|
|
|
|
analyzePackageResponse, err := service.AnalyzePackage(ctx, &malysisv1.AnalyzePackageRequest{
|
|
Target: &malysisv1pb.PackageAnalysisTarget{
|
|
PackageVersion: packageVersion,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to submit package for malware analysis: %v", err)
|
|
}
|
|
|
|
ui.PrintMsg("Submitted package for malware analysis with ID: %s",
|
|
analyzePackageResponse.GetAnalysisId())
|
|
|
|
if malwareAnalysisNoWait {
|
|
return nil
|
|
}
|
|
|
|
ui.StartSpinner("Waiting for malware analysis to complete")
|
|
var report *malysisv1pb.Report
|
|
var verificationRecord *malysisv1pb.VerificationRecord
|
|
|
|
for {
|
|
reportResponse, err := service.GetAnalysisReport(ctx, &malysisv1.GetAnalysisReportRequest{
|
|
AnalysisId: analyzePackageResponse.GetAnalysisId(),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get malware analysis report: %v", err)
|
|
}
|
|
|
|
if reportResponse.GetStatus() == malysisv1.AnalysisStatus_ANALYSIS_STATUS_FAILED {
|
|
return fmt.Errorf("malware analysis failed: %s", reportResponse.GetErrorMessage())
|
|
}
|
|
|
|
if reportResponse.GetStatus() == malysisv1.AnalysisStatus_ANALYSIS_STATUS_COMPLETED {
|
|
report = reportResponse.GetReport()
|
|
verificationRecord = reportResponse.GetVerificationRecord()
|
|
break
|
|
}
|
|
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
|
|
ui.StopSpinner()
|
|
|
|
if report == nil {
|
|
return fmt.Errorf("malware analysis report is empty")
|
|
}
|
|
|
|
ui.PrintSuccess("Malware analysis completed successfully")
|
|
|
|
if malwareAnalysisReportJSON != "" {
|
|
ui.PrintMsg("Generating JSON report")
|
|
|
|
err = writeJSONReport(report)
|
|
if err != nil {
|
|
ui.PrintError("Failed to render malware analysis report in JSON format: %v", err)
|
|
}
|
|
}
|
|
|
|
if malwareAnalysisReportOSV != "" {
|
|
if !report.GetInference().GetIsMalware() {
|
|
ui.PrintWarning("Report is not malware, skipping OSV report generation")
|
|
return nil
|
|
} else {
|
|
ui.PrintMsg("Generating OSV report in: %s", malwareAnalysisReportOSV)
|
|
|
|
err = writeOSVReport(report)
|
|
if err != nil {
|
|
ui.PrintError("Failed to render malware analysis report in OSV format: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return renderMalwareAnalysisReport(malwareAnalysisPackageUrl,
|
|
analyzePackageResponse.GetAnalysisId(), report, verificationRecord)
|
|
}
|
|
|
|
func writeOSVReport(report *malysisv1pb.Report) error {
|
|
err := os.MkdirAll(malwareAnalysisReportOSV, 0o755)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create directory: %v", err)
|
|
}
|
|
|
|
generator, err := malysis.NewOpenSSFMaliciousPackageReportGenerator(malysis.OpenSSFMaliciousPackageReportGeneratorConfig{
|
|
Dir: malwareAnalysisReportOSV,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create OpenSSF malicious package report generator: %v", err)
|
|
}
|
|
|
|
err = generator.GenerateReport(context.Background(), report, malysis.OpenSSFMaliciousPackageReportParams{
|
|
FinderName: malwareReportOSVFinderName,
|
|
Contacts: malwareReportOSVContacts,
|
|
ReferenceURL: malwareReportOSVReferenceURL,
|
|
UseRange: malwareReportOSVUseRange,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate OpenSSF malicious package report: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func writeJSONReport(report *malysisv1pb.Report) error {
|
|
data, err := utils.ToPbJson(report, " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(malwareAnalysisReportJSON, []byte(data), 0o644)
|
|
}
|
|
|
|
func renderMalwareAnalysisReport(purl string, analysisId string,
|
|
report *malysisv1pb.Report, vr *malysisv1pb.VerificationRecord,
|
|
) error {
|
|
ui.PrintMsg("Malware analysis report for package: %s", purl)
|
|
|
|
tbl := table.NewWriter()
|
|
tbl.SetOutputMirror(os.Stdout)
|
|
tbl.SetStyle(table.StyleLight)
|
|
|
|
tbl.AppendHeader(table.Row{"Package URL", "Status", "Confidence"})
|
|
|
|
status := reporter.InfoBgText(" SAFE ")
|
|
if report.GetInference().GetIsMalware() {
|
|
if vr != nil && vr.IsMalware {
|
|
status = reporter.CriticalBgText(" MALICIOUS ")
|
|
} else {
|
|
status = reporter.WarningBgText(" SUSPICIOUS ")
|
|
}
|
|
}
|
|
|
|
confidence := report.GetInference().GetConfidence().String()
|
|
confidence = strings.TrimPrefix(confidence, "CONFIDENCE_")
|
|
|
|
tbl.AppendRow(table.Row{purl, status, confidence})
|
|
tbl.Render()
|
|
|
|
fmt.Println()
|
|
fmt.Println(reporter.WarningText(fmt.Sprintf("** The full report is available at: %s",
|
|
reportVisualizationUrl(analysisId))))
|
|
fmt.Println()
|
|
|
|
return nil
|
|
}
|
|
|
|
func reportVisualizationUrl(analysisId string) string {
|
|
return malysis.ReportURL(analysisId)
|
|
}
|
|
|
|
func resolveGitHubActionsCommitHash(ctx context.Context, packageVersion *packagev1.PackageVersion) (string, error) {
|
|
gha, err := adapters.NewGithubClient(adapters.DefaultGitHubClientConfig())
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create GitHub client: %v", err)
|
|
}
|
|
|
|
parts := strings.Split(packageVersion.GetPackage().GetName(), "/")
|
|
if len(parts) != 2 {
|
|
return "", fmt.Errorf("invalid repository name: %s - should be in the format <owner>/<repo>", packageVersion.GetPackage().GetName())
|
|
}
|
|
|
|
owner := parts[0]
|
|
repo := parts[1]
|
|
|
|
return vetutils.ResolveGitHubRepositoryCommitSHA(ctx,
|
|
gha, owner, repo, packageVersion.GetVersion())
|
|
}
|