Skip to content

Commit 1e835fe

Browse files
authored
fix: all virustotal scans erroring and not scanning the files (#95)
* fix: all scans erroring, properly handle errors from previous virustotal analysis * fix: only empty files passed to virustotal * chore: enable govet shadow linter * fix: return virustotal scan error in activity when scan fails
1 parent 860757e commit 1e835fe

File tree

3 files changed

+90
-87
lines changed

3 files changed

+90
-87
lines changed

.golangci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ linters-settings:
1515
- github.com/satisfactorymodding/smr-api/*
1616

1717
govet:
18-
disable:
18+
enable:
1919
- shadow
2020

2121
gocritic:
@@ -54,6 +54,9 @@ issues:
5454
- ./generated/
5555
exclude:
5656
- should pass the context parameter
57+
exclude-rules:
58+
- text: 'shadow: declaration of "err" shadows declaration at'
59+
linters: [ govet ]
5760

5861
linters:
5962
disable-all: true

validation/virustotal.go

Lines changed: 78 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package validation
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/sha256"
7+
"errors"
68
"fmt"
79
"io"
810
"log/slog"
@@ -26,11 +28,8 @@ func InitializeVirusTotal() {
2628

2729
type AnalysisResults struct {
2830
Attributes struct {
29-
Stats *struct {
30-
Suspicious *int `json:"suspicious,omitempty"`
31-
Malicious *int `json:"malicious,omitempty"`
32-
} `json:"stats,omitempty"`
33-
Status string `json:"status"`
31+
Stats *AnalysisStats `json:"stats,omitempty"`
32+
Status string `json:"status"`
3433
} `json:"attributes,omitempty"`
3534
Meta struct {
3635
FileInfo struct {
@@ -41,16 +40,18 @@ type AnalysisResults struct {
4140

4241
type PreviousAnalysisResults struct {
4342
Attributes struct {
44-
Stats *struct {
45-
Suspicious *int `json:"suspicious,omitempty"`
46-
Malicious *int `json:"malicious,omitempty"`
47-
} `json:"last_analysis_stats,omitempty"`
43+
Stats *AnalysisStats `json:"last_analysis_stats,omitempty"`
4844
} `json:"attributes,omitempty"`
4945
}
5046

47+
type AnalysisStats struct {
48+
Suspicious *int `json:"suspicious,omitempty"`
49+
Malicious *int `json:"malicious,omitempty"`
50+
}
51+
5152
type ScanResult struct {
5253
Safe bool
53-
Hash *string
54+
Hash string
5455
FileName string
5556
}
5657

@@ -67,11 +68,7 @@ func ScanFiles(ctx context.Context, files []io.Reader, names []string) ([]ScanRe
6768
if err != nil {
6869
return fmt.Errorf("failed to scan %s: %w", names[count], err)
6970
}
70-
select {
71-
case c <- *scanResult:
72-
case <-gctx.Done():
73-
return gctx.Err()
74-
}
71+
c <- *scanResult
7572
return nil
7673
})
7774
}
@@ -94,85 +91,89 @@ func ScanFiles(ctx context.Context, files []io.Reader, names []string) ([]ScanRe
9491
}
9592

9693
func scanFile(ctx context.Context, file io.Reader, name string) (*ScanResult, error) {
97-
scanResult := ScanResult{
98-
Safe: false,
99-
FileName: name,
94+
hash := sha256.New()
95+
fileBytes, err := io.ReadAll(file)
96+
if err != nil {
97+
return nil, fmt.Errorf("failed read file: %w", err)
10098
}
10199

102-
hash := sha256.New()
103-
if _, err := io.Copy(hash, file); err != nil {
104-
return nil, fmt.Errorf("failed to generate hash for file %w", err)
100+
_, err = hash.Write(fileBytes)
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to hash file: %w", err)
105103
}
106104

107105
checksum := hash.Sum(nil)
108-
var previousAnalysisResults PreviousAnalysisResults
106+
hashStr := fmt.Sprintf("%x", checksum)
109107

110-
_, err := client.GetData(vt.URL("files/%x", checksum), &previousAnalysisResults)
108+
var previousAnalysisResults PreviousAnalysisResults
111109

112-
alreadyScanned := false
113-
analysisID := ""
110+
hasPreviousAnalysis := true
111+
_, err = client.GetData(vt.URL("files/%x", checksum), &previousAnalysisResults)
112+
if err != nil {
113+
var vtErr vt.Error
114+
if errors.As(err, &vtErr) {
115+
if vtErr.Code != "NotFoundError" {
116+
return nil, fmt.Errorf("failed to get previous analysis results: %w", err)
117+
}
118+
} else {
119+
return nil, fmt.Errorf("failed to get previous analysis results: %w", err)
120+
}
121+
hasPreviousAnalysis = false
122+
}
114123

115-
if err == nil {
116-
alreadyScanned = true
117-
slox.Info(ctx, "file already scanned, skipping upload", slog.String("file", name))
118-
hash := fmt.Sprintf("%x", checksum)
119-
scanResult.Hash = &hash
120-
} else {
121-
scan, err := client.NewFileScanner().Scan(file, name, nil)
124+
if hasPreviousAnalysis {
125+
slox.Info(ctx, "file already scanned", slog.String("file", name), slog.String("hash", hashStr))
126+
if previousAnalysisResults.Attributes.Stats == nil {
127+
return nil, fmt.Errorf("no stats available on previous analysis")
128+
}
129+
safe, err := isResultSafe(*previousAnalysisResults.Attributes.Stats)
122130
if err != nil {
123-
return &scanResult, fmt.Errorf("failed to scan file: %w", err)
131+
return nil, fmt.Errorf("failed to determine if file is safe: %w", err)
124132
}
125-
analysisID := scan.ID()
126-
slox.Info(ctx, "uploaded virus scan", slog.String("file", name), slog.String("analysis_id", analysisID))
133+
return &ScanResult{
134+
Safe: safe,
135+
Hash: hashStr,
136+
FileName: name,
137+
}, nil
127138
}
128139

129-
for {
130-
var analysisResults AnalysisResults
131-
var malicious int
132-
var suspicious int
133-
134-
if !alreadyScanned {
135-
time.Sleep(time.Second * 15)
136-
137-
_, err = client.GetData(vt.URL("analyses/%s", analysisID), &analysisResults)
138-
if err != nil {
139-
scanResult.Safe = false
140-
return nil, fmt.Errorf("failed to get analysis results: %w", err)
141-
}
142-
scanResult.Hash = &analysisResults.Meta.FileInfo.SHA256
143-
144-
if !alreadyScanned && analysisResults.Attributes.Status != "completed" {
145-
continue
146-
}
140+
scan, err := client.NewFileScanner().Scan(bytes.NewReader(fileBytes), name, nil)
141+
if err != nil {
142+
return nil, fmt.Errorf("failed to scan file: %w", err)
143+
}
144+
analysisID := scan.ID()
145+
slox.Info(ctx, "uploaded virus scan", slog.String("file", name), slog.String("analysis_id", analysisID))
147146

148-
if analysisResults.Attributes.Stats == nil {
149-
slox.Error(ctx, "no stats available", slog.Any("err", err), slog.String("file", name))
150-
scanResult.Safe = false
151-
return &scanResult, nil
152-
}
147+
var analysisResults AnalysisResults
148+
for analysisResults.Attributes.Status != "completed" {
149+
time.Sleep(time.Second * 15)
153150

154-
if analysisResults.Attributes.Stats.Malicious == nil || analysisResults.Attributes.Stats.Suspicious == nil {
155-
slox.Error(ctx, "unable to determine malicious or suspicious file", slog.Any("err", err), slog.String("file", name))
156-
scanResult.Safe = false
157-
return &scanResult, nil
158-
}
159-
malicious = *analysisResults.Attributes.Stats.Malicious
160-
suspicious = *analysisResults.Attributes.Stats.Suspicious
161-
} else {
162-
malicious = *previousAnalysisResults.Attributes.Stats.Malicious
163-
suspicious = *previousAnalysisResults.Attributes.Stats.Suspicious
151+
_, err := client.GetData(vt.URL("analyses/%s", analysisID), &analysisResults)
152+
if err != nil {
153+
return nil, fmt.Errorf("failed to get analysis results: %w", err)
164154
}
155+
}
165156

166-
// Why 1? Well because some company made a shitty AI and it flags random mods.
167-
if malicious > 1 || suspicious > 1 {
168-
slox.Error(ctx, "suspicious or malicious file found", slog.Any("err", err), slog.String("file", name))
169-
scanResult.Safe = false
170-
return &scanResult, nil
171-
}
157+
if analysisResults.Attributes.Stats == nil {
158+
return nil, fmt.Errorf("no stats available")
159+
}
160+
161+
safe, err := isResultSafe(*analysisResults.Attributes.Stats)
162+
if err != nil {
163+
return nil, fmt.Errorf("failed to determine if file is safe: %w", err)
164+
}
165+
return &ScanResult{
166+
Safe: safe,
167+
Hash: hashStr,
168+
FileName: name,
169+
}, nil
170+
}
172171

173-
scanResult.Safe = true
174-
break
172+
func isResultSafe(stats AnalysisStats) (bool, error) {
173+
if stats.Malicious == nil || stats.Suspicious == nil {
174+
return false, fmt.Errorf("missing malicious or suspicious stats")
175175
}
176176

177-
return &scanResult, nil
177+
// Why 1? Well because some company made a shitty AI and it flags random mods.
178+
return *stats.Malicious <= 1 && *stats.Suspicious <= 1, nil
178179
}

workflows/versionupload/scan_mod_on_virus_total.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,20 @@ func scanAndSaveResults(ctx context.Context, toScan []io.Reader, names []string,
9393
defer span.End()
9494

9595
scanResults, scanErr := validation.ScanFiles(ctx, toScan, names)
96-
if scanErr != nil {
97-
span.SetStatus(codes.Error, scanErr.Error())
98-
span.RecordError(scanErr)
99-
if len(scanResults) == 0 {
100-
return false, scanErr
101-
}
102-
slox.Error(ctx, scanErr.Error())
103-
}
96+
// Check error later, because we can have partial results to save, even in the case of an error
10497

10598
if err := saveScanResults(ctx, scanResults, args); err != nil {
10699
span.SetStatus(codes.Error, err.Error())
107100
span.RecordError(err)
108101
return false, err
109102
}
110103

104+
if scanErr != nil {
105+
span.SetStatus(codes.Error, scanErr.Error())
106+
span.RecordError(scanErr)
107+
return false, scanErr
108+
}
109+
111110
if len(scanResults) != len(toScan) {
112111
return false, nil
113112
}
@@ -127,7 +126,7 @@ func saveScanResults(ctx context.Context, scanResults []validation.ScanResult, a
127126
c.SetSafe(scanResults[i].Safe).
128127
SetVersionID(args.VersionID).
129128
SetFileName(scanResults[i].FileName).
130-
SetHash(*scanResults[i].Hash)
129+
SetHash(scanResults[i].Hash)
131130
},
132131
).OnConflict().
133132
DoNothing().

0 commit comments

Comments
 (0)