Skip to content

Commit

Permalink
feat: Integrate with SafeDep Malware Analysis Service (#299)
Browse files Browse the repository at this point in the history
* feat: Add support for malware analysis service integration

* feat: Update malware analysis command to poll for report

* fix: Spinner handling in malware analysis command

* fix: Malware analysis output table

* fix: Confidence string handling
  • Loading branch information
abhisek authored Dec 19, 2024
1 parent d98075e commit 7daa072
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 10 deletions.
20 changes: 20 additions & 0 deletions cmd/inspect/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package inspect

import (
"github.com/spf13/cobra"
)

func NewPackageInspectCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "inspect",
Short: "Inspect an OSS package",
Long: `Inspect an OSS package using deep inspection and analysis.
This command will integrate with local and remote analysis services.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}

cmd.AddCommand(newPackageMalwareInspectCommand())
return cmd
}
160 changes: 160 additions & 0 deletions cmd/inspect/malware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
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"
malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/malysis/v1"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/safedep/dry/api/pb"
"github.com/safedep/dry/utils"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
"github.com/spf13/cobra"
)

var (
malwareAnalysisPackageUrl string
malwareAnalysisTimeout time.Duration
malwareAnalysisReportJSON string
)

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.MarkFlagRequired("purl")

return cmd
}

func executeMalwareAnalysis() error {
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
}

ctx := context.Background()
ctx, cancelFun := context.WithTimeout(ctx, malwareAnalysisTimeout)

defer cancelFun()

analyzePackageResponse, err := service.AnalyzePackage(ctx, &malysisv1.AnalyzePackageRequest{
Target: &malysisv1pb.PackageAnalysisTarget{
PackageVersion: purl.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())

ui.StartSpinner("Waiting for malware analysis to complete")
var report *malysisv1pb.Report

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()
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")

err = renderToJSON(report)
if err != nil {
ui.PrintError("Failed to render malware analysis report in JSON format: %v", err)
}

return renderMalwareAnalysisReport(malwareAnalysisPackageUrl, report)
}

func renderToJSON(report *malysisv1pb.Report) error {
if malwareAnalysisReportJSON == "" {
return nil
}

data, err := utils.ToPbJson(report, " ")
if err != nil {
return err
}

return os.WriteFile(malwareAnalysisReportJSON, []byte(data), 0644)
}

func renderMalwareAnalysisReport(purl string, report *malysisv1pb.Report) 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 := text.FgHiGreen.Sprint("SAFE")
if report.GetInference().GetIsMalware() {
status = text.FgHiRed.Sprint("MALWARE")
}

confidence := report.GetInference().GetConfidence().String()
confidence = strings.TrimPrefix(confidence, "CONFIDENCE_")

tbl.AppendRow(table.Row{purl, status, confidence})
tbl.Render()

return nil
}
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module github.com/safedep/vet
go 1.23.2

require (
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20241117092917-2fd4dbf7c52e.1
buf.build/gen/go/safedep/api/protocolbuffers/go v1.35.2-20241117092917-2fd4dbf7c52e.1
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20241127172711-d314452ec756.1
buf.build/gen/go/safedep/api/protocolbuffers/go v1.35.2-20241127172711-d314452ec756.1
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/CycloneDX/cyclonedx-go v0.9.1
github.com/anchore/syft v1.16.0
Expand All @@ -25,7 +25,7 @@ require (
github.com/oklog/ulid/v2 v2.1.0
github.com/owenrumney/go-sarif/v2 v2.3.3
github.com/package-url/packageurl-go v0.1.3
github.com/safedep/dry v0.0.0-20241112071106-48fc2b8dc770
github.com/safedep/dry v0.0.0-20241128083908-2f8ecd48dc2c
github.com/sirupsen/logrus v1.9.3
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82
github.com/spdx/tools-golang v0.5.5
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20240920164238-5a7b106cbb87.1 h1:7QIeAuTdLp173vC/9JojRMDFcpmqtoYrxPmvdHAOynw=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20240920164238-5a7b106cbb87.1/go.mod h1:mnHCFccv4HwuIAOHNGdiIc5ZYbBCvbTWZcodLN5wITI=
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20241117092917-2fd4dbf7c52e.1 h1:4UzreoJwRj2SKKfAyEaBbeF1aDocSRCqI7JNBN6IvgM=
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20241117092917-2fd4dbf7c52e.1/go.mod h1:U1Qaal56kRbId8vgExO27c9qU9OnNenB4+PzV1+RXxQ=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.35.2-20241117092917-2fd4dbf7c52e.1 h1:dxJ6SGFwwCx8l0wjA2JooSHUW7NtEiOqNgRsVFlrveE=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.35.2-20241117092917-2fd4dbf7c52e.1/go.mod h1:471Oa35fKGZy8W6WOLV4R80UcWKMYaXNmIfApMvimf4=
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20241127172711-d314452ec756.1 h1:ecda01eNlmjlQKIaiFIpV/VLbG3xeCh94TiEazvaGwc=
buf.build/gen/go/safedep/api/grpc/go v1.5.1-20241127172711-d314452ec756.1/go.mod h1:M1mSl+19TDBAGjd/L4t7UoCVQ0GlMYDwC2B3iqkpaDI=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.35.2-20241127172711-d314452ec756.1 h1:J4oZ6bbe+gZzFXnJMUcM8BrSKh0FMLdPNpDwWizLeXg=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.35.2-20241127172711-d314452ec756.1/go.mod h1:471Oa35fKGZy8W6WOLV4R80UcWKMYaXNmIfApMvimf4=
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
Expand Down Expand Up @@ -786,8 +786,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/safedep/dry v0.0.0-20241112071106-48fc2b8dc770 h1:NMcL4K+9IC0/LON8qxgtDrg7JcFkyoqI9NQUSFONios=
github.com/safedep/dry v0.0.0-20241112071106-48fc2b8dc770/go.mod h1:6HqxN60ZNx2ReiodmwkSbP6LlmAV7N8WJ/tlF6TLVU4=
github.com/safedep/dry v0.0.0-20241128083908-2f8ecd48dc2c h1:qUSfzPPlEcLKF6cKvkkXU6ddu4NGSz0UdS7Xjcrenvw=
github.com/safedep/dry v0.0.0-20241128083908-2f8ecd48dc2c/go.mod h1:dtGFDAnRo+WqwEyqPc2hTwuVGwWLq2jHnP4Q8BO1u7g=
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
Expand Down
17 changes: 17 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
apiV2UrlEnvKey = "VET_INSIGHTS_API_V2_URL" // gitleaks:allow
syncUrlEnvKey = "VET_SYNC_API_URL"
controlPlaneUrlEnvKey = "VET_CONTROL_PLANE_API_URL"
dataPlaneUrlEnvKey = "VET_DATA_PLANE_API_URL"
apiKeyEnvKey = "VET_API_KEY"
apiKeyAlternateEnvKey = "VET_INSIGHTS_API_KEY"
communityModeEnvKey = "VET_COMMUNITY_MODE"
Expand All @@ -24,6 +25,7 @@ const (
defaultCommunityApiUrl = "https://api.safedep.io/insights-community/v1"

// gRPC service base URL.
defaultDataPlaneApiUrl = "https://api.safedep.io"
defaultSyncApiUrl = "https://api.safedep.io"
defaultInsightsApiV2Url = "https://api.safedep.io"
defaultControlPlaneApiUrl = "https://cloud.safedep.io"
Expand All @@ -41,6 +43,7 @@ type Config struct {
ApiUrl string `yaml:"api_url"`
ApiKey string `yaml:"api_key"`
Community bool `yaml:"community"`
DataPlaneApiUrl string `yaml:"data_plane_api_url"`
ControlPlaneApiUrl string `yaml:"control_api_url"`
SyncApiUrl string `yaml:"sync_api_url"`
InsightsApiV2Url string `yaml:"insights_api_v2_url"`
Expand All @@ -61,6 +64,7 @@ func DefaultConfig() Config {
return Config{
ApiUrl: defaultApiUrl,
Community: false,
DataPlaneApiUrl: defaultDataPlaneApiUrl,
ControlPlaneApiUrl: defaultControlPlaneApiUrl,
SyncApiUrl: defaultSyncApiUrl,
InsightsApiV2Url: defaultInsightsApiV2Url,
Expand Down Expand Up @@ -157,6 +161,19 @@ func CloudRefreshToken() string {
return ""
}

func DataPlaneUrl() string {
envOverride := os.Getenv(dataPlaneUrlEnvKey)
if envOverride != "" {
return envOverride
}

if (globalConfig != nil) && (globalConfig.DataPlaneApiUrl != "") {
return globalConfig.ApiUrl
}

return defaultDataPlaneApiUrl
}

func SyncApiUrl() string {
envOverride := os.Getenv(syncUrlEnvKey)
if envOverride != "" {
Expand Down
4 changes: 4 additions & 0 deletions internal/auth/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ func InsightsV2ClientConnection(name string) (*grpc.ClientConn, error) {
return cloudClientConnection(name, InsightsApiV2Url(), ApiKey())
}

func MalwareAnalysisClientConnection(name string) (*grpc.ClientConn, error) {
return cloudClientConnection(name, DataPlaneUrl(), ApiKey())
}

func cloudClientConnection(name, loc, tok string) (*grpc.ClientConn, error) {
parsedUrl, err := url.Parse(loc)
if err != nil {
Expand Down
8 changes: 7 additions & 1 deletion internal/ui/spinner.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ func StartSpinner(msg string) {
}

func StopSpinner() {
spinnerChan <- true
// Gracefully handle the case where the spinner is already stopped
// and the channel is closed, yet client code calls StopSpinner() again.
defer func() {
_ = recover()
}()

close(spinnerChan)

fmt.Printf("\r")
fmt.Println()
Expand Down
14 changes: 14 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/safedep/dry/utils"
"github.com/safedep/vet/cmd/cloud"
"github.com/safedep/vet/cmd/inspect"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/exceptions"
Expand Down Expand Up @@ -60,6 +61,10 @@ func main() {
cmd.AddCommand(newConnectCommand())
cmd.AddCommand(cloud.NewCloudCommand())

if checkIfPackageInspectCommandEnabled() {
cmd.AddCommand(inspect.NewPackageInspectCommand())
}

cobra.OnInitialize(func() {
printBanner()
loadExceptions()
Expand Down Expand Up @@ -108,6 +113,15 @@ func printBanner() {
ui.PrintBanner(banner)
}

func checkIfPackageInspectCommandEnabled() bool {
bRet, err := strconv.ParseBool(os.Getenv("VET_ENABLE_PACKAGE_INSPECT_COMMAND"))
if err != nil {
return false
}

return bRet
}

// Redirect to file or discard log if empty
func redirectLogToFile(path string) {
logger.Debugf("Redirecting logger output to: %s", path)
Expand Down

0 comments on commit 7daa072

Please sign in to comment.