Skip to content

Commit 2277975

Browse files
committed
feat: add ability to view file scan results
1 parent 0abc49e commit 2277975

1 file changed

Lines changed: 208 additions & 0 deletions

File tree

cmd/view.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Copyright 2018 Saferwall. All rights reserved.
2+
// Use of this source code is governed by Apache v2 license
3+
// license that can be found in the LICENSE file.
4+
5+
package cmd
6+
7+
import (
8+
"fmt"
9+
"log"
10+
"sort"
11+
"strings"
12+
"time"
13+
14+
"github.com/charmbracelet/lipgloss"
15+
"github.com/saferwall/cli/internal/entity"
16+
"github.com/saferwall/cli/internal/webapi"
17+
"github.com/spf13/cobra"
18+
)
19+
20+
var viewCmd = &cobra.Command{
21+
Use: "view <sha256>",
22+
Short: "View scan results for a file by its SHA256 hash",
23+
Long: `Fetches and displays the scan results summary for a file, including AV detections.`,
24+
Args: cobra.ExactArgs(1),
25+
Run: func(cmd *cobra.Command, args []string) {
26+
sha256 := strings.ToLower(args[0])
27+
28+
webSvc := webapi.New(cfg.Credentials.URL)
29+
token, err := webSvc.Login(cfg.Credentials.Username, cfg.Credentials.Password)
30+
if err != nil {
31+
log.Fatalf("failed to login: %v", err)
32+
}
33+
_ = token
34+
35+
var file entity.File
36+
if err := webSvc.GetFile(sha256, &file); err != nil {
37+
log.Fatalf("failed to get file: %v", err)
38+
}
39+
40+
printFileReport(file)
41+
},
42+
}
43+
44+
func init() {
45+
rootCmd.AddCommand(viewCmd)
46+
}
47+
48+
// Styles for the report output.
49+
var (
50+
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
51+
headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("14"))
52+
keyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
53+
detectStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
54+
cleanStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
55+
avNameStyle = lipgloss.NewStyle().Width(24)
56+
)
57+
58+
func printFileReport(file entity.File) {
59+
fmt.Println()
60+
fmt.Println(titleStyle.Render("File Report"))
61+
fmt.Println(strings.Repeat("─", 60))
62+
63+
// File identification.
64+
fmt.Println(headerStyle.Render("Identification"))
65+
printKV("SHA256", file.SHA256)
66+
printKV("MD5", file.MD5)
67+
printKV("SHA1", file.SHA1)
68+
if file.SSDeep != "" {
69+
printKV("SSDeep", file.SSDeep)
70+
}
71+
printKV("Size", formatSize(file.Size))
72+
fmt.Println()
73+
74+
// File properties.
75+
fmt.Println(headerStyle.Render("Properties"))
76+
if file.Magic != "" {
77+
printKV("Magic", file.Magic)
78+
}
79+
if file.Format != "" {
80+
fmtStr := file.Format
81+
if file.Extension != "" {
82+
fmtStr += " (." + file.Extension + ")"
83+
}
84+
printKV("Format", fmtStr)
85+
}
86+
if len(file.Packer) > 0 {
87+
printKV("Packer", strings.Join(file.Packer, ", "))
88+
}
89+
if file.FirstSeen != 0 {
90+
printKV("First Seen", formatTimestamp(file.FirstSeen))
91+
}
92+
if file.LastScanned != 0 {
93+
printKV("Last Scanned", formatTimestamp(file.LastScanned))
94+
}
95+
fmt.Println()
96+
97+
// Classification.
98+
fmt.Println(headerStyle.Render("Classification"))
99+
printKV("Verdict", renderClassification(file.Classification))
100+
fmt.Println()
101+
102+
// MultiAV results.
103+
printMultiAVResults(file.MultiAV)
104+
}
105+
106+
func printMultiAVResults(multiav map[string]any) {
107+
if multiav == nil {
108+
fmt.Println(headerStyle.Render("Antivirus Results"))
109+
fmt.Println(" No scan results available.")
110+
return
111+
}
112+
113+
lastScan, ok := multiav["last_scan"].(map[string]any)
114+
if !ok {
115+
fmt.Println(headerStyle.Render("Antivirus Results"))
116+
fmt.Println(" No scan results available.")
117+
return
118+
}
119+
120+
// Extract stats.
121+
var positives, enginesCount int
122+
if stats, ok := lastScan["stats"].(map[string]any); ok {
123+
if v, ok := stats["positives"].(float64); ok {
124+
positives = int(v)
125+
}
126+
if v, ok := stats["engines_count"].(float64); ok {
127+
enginesCount = int(v)
128+
}
129+
}
130+
131+
// Summary line.
132+
fmt.Println(headerStyle.Render("Antivirus Results"))
133+
detectionStr := fmt.Sprintf("%d/%d engines detected this file", positives, enginesCount)
134+
if positives > 0 {
135+
fmt.Println(" " + detectStyle.Render(detectionStr))
136+
} else {
137+
fmt.Println(" " + cleanStyle.Render(detectionStr))
138+
}
139+
fmt.Println()
140+
141+
// Collect detected engines only (engines live under last_scan.detections).
142+
type avResult struct {
143+
name string
144+
output string
145+
}
146+
var detected []avResult
147+
var clean []avResult
148+
149+
detections, _ := lastScan["detections"].(map[string]any)
150+
for key, val := range detections {
151+
engine, ok := val.(map[string]any)
152+
if !ok {
153+
continue
154+
}
155+
156+
infected, _ := engine["infected"].(bool)
157+
output, _ := engine["output"].(string)
158+
if infected {
159+
detected = append(detected, avResult{name: key, output: output})
160+
} else {
161+
clean = append(clean, avResult{name: key})
162+
}
163+
}
164+
165+
sort.Slice(detected, func(i, j int) bool { return detected[i].name < detected[j].name })
166+
sort.Slice(clean, func(i, j int) bool { return clean[i].name < clean[j].name })
167+
168+
// Print detections.
169+
if len(detected) > 0 {
170+
for _, r := range detected {
171+
name := avNameStyle.Render(r.name)
172+
fmt.Printf(" %s %s\n", detectStyle.Render("●")+" "+name, detectStyle.Render(r.output))
173+
}
174+
fmt.Println()
175+
}
176+
177+
// Print clean engines.
178+
if len(clean) > 0 {
179+
cleanNames := make([]string, len(clean))
180+
for i, r := range clean {
181+
cleanNames[i] = r.name
182+
}
183+
fmt.Printf(" %s %s\n", cleanStyle.Render("○"), styleDim.Render("No detection: "+strings.Join(cleanNames, ", ")))
184+
fmt.Println()
185+
}
186+
}
187+
188+
func printKV(key, value string) {
189+
fmt.Printf(" %s %s\n", keyStyle.Render(fmt.Sprintf("%-14s", key+":")), value)
190+
}
191+
192+
func formatSize(size int64) string {
193+
switch {
194+
case size >= 1<<30:
195+
return fmt.Sprintf("%.2f GB (%d bytes)", float64(size)/float64(1<<30), size)
196+
case size >= 1<<20:
197+
return fmt.Sprintf("%.2f MB (%d bytes)", float64(size)/float64(1<<20), size)
198+
case size >= 1<<10:
199+
return fmt.Sprintf("%.2f KB (%d bytes)", float64(size)/float64(1<<10), size)
200+
default:
201+
return fmt.Sprintf("%d bytes", size)
202+
}
203+
}
204+
205+
func formatTimestamp(ts int64) string {
206+
t := time.Unix(ts, 0)
207+
return t.Format("2006-01-02 15:04:05 UTC")
208+
}

0 commit comments

Comments
 (0)