Skip to content

Commit 94ade42

Browse files
authored
feat: show encrypted / successful / attempted passwords in view/scan commands (#22)
Co-authored-by: Ayoub Faouzi <ayoubfaouzi@users.noreply.github.com>
1 parent 1a272cf commit 94ade42

4 files changed

Lines changed: 111 additions & 40 deletions

File tree

cmd/scan.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,16 @@ func init() {
4646
}
4747

4848
type scanSummary struct {
49-
SHA256 string `json:"sha256"`
50-
Size int64 `json:"size"`
51-
Classification string `json:"classification"`
52-
FileFormat string `json:"file_format"`
53-
FileExtension string `json:"file_extension"`
54-
MultiAV *avSummary `json:"multiav,omitempty"`
49+
SHA256 string `json:"sha256"`
50+
Size int64 `json:"size"`
51+
Classification string `json:"classification"`
52+
FileFormat string `json:"file_format"`
53+
FileExtension string `json:"file_extension"`
54+
Encrypted bool `json:"encrypted,omitempty"`
55+
DecryptionSuccess *bool `json:"decryption_success,omitempty"`
56+
SuccessfulPassword string `json:"successful_password,omitempty"`
57+
AttemptedPasswords []string `json:"attempted_passwords,omitempty"`
58+
MultiAV *avSummary `json:"multiav,omitempty"`
5559
}
5660

5761
type avSummary struct {
@@ -61,11 +65,15 @@ type avSummary struct {
6165

6266
func buildScanSummary(file entity.File) scanSummary {
6367
s := scanSummary{
64-
SHA256: file.SHA256,
65-
Size: file.Size,
66-
Classification: file.Classification,
67-
FileFormat: file.Format,
68-
FileExtension: file.Extension,
68+
SHA256: file.SHA256,
69+
Size: file.Size,
70+
Classification: file.Classification,
71+
FileFormat: file.Format,
72+
FileExtension: file.Extension,
73+
Encrypted: file.Encrypted,
74+
DecryptionSuccess: file.DecryptionSuccess,
75+
SuccessfulPassword: file.SuccessfulPassword,
76+
AttemptedPasswords: file.AttemptedPasswords,
6977
}
7078

7179
if lastScan, ok := file.MultiAV["last_scan"].(map[string]any); ok {

cmd/scanui.go

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"os"
1010
"path/filepath"
11+
"strings"
1112
"time"
1213

1314
"github.com/charmbracelet/bubbles/spinner"
@@ -107,7 +108,7 @@ func uploadFileCmd(index int, web webapi.Service, filename, token string) tea.Cm
107108
sha256: file.SHA256,
108109
size: file.Size,
109110
isArchive: file.IsArchive,
110-
childHashes: file.ArchiveFiles,
111+
childHashes: derivedHashes(file.DerivedFiles),
111112
}
112113
} else if forceRescanFlag {
113114
// Fetch the existing file to check if it's an archive.
@@ -116,19 +117,19 @@ func uploadFileCmd(index int, web webapi.Service, filename, token string) tea.Cm
116117
return fileUploadedMsg{index: index, err: fmt.Errorf("get file: %w", err)}
117118
}
118119

119-
if file.IsArchive && len(file.ArchiveFiles) > 0 {
120+
if file.IsArchive && len(file.DerivedFiles) > 0 {
120121
// Archive: rescan each child, not the container itself.
121-
for _, childHash := range file.ArchiveFiles {
122-
if err := web.Rescan(childHash, token, osFlag, enableDetonationFlag, timeoutFlag); err != nil {
123-
return fileUploadedMsg{index: index, err: fmt.Errorf("rescan child %s: %w", childHash[:12], err)}
122+
for _, df := range file.DerivedFiles {
123+
if err := web.Rescan(df.SHA256, token, osFlag, enableDetonationFlag, timeoutFlag); err != nil {
124+
return fileUploadedMsg{index: index, err: fmt.Errorf("rescan child %s: %w", df.SHA256[:12], err)}
124125
}
125126
}
126127
return fileUploadedMsg{
127128
index: index,
128129
sha256: sha256,
129130
size: file.Size,
130131
isArchive: true,
131-
childHashes: file.ArchiveFiles,
132+
childHashes: derivedHashes(file.DerivedFiles),
132133
}
133134
}
134135

@@ -181,18 +182,18 @@ func rescanFileCmd(index int, web webapi.Service, sha256, token string) tea.Cmd
181182
return fileUploadedMsg{index: index, err: fmt.Errorf("get file: %w", err)}
182183
}
183184

184-
if file.IsArchive && len(file.ArchiveFiles) > 0 {
185-
for _, childHash := range file.ArchiveFiles {
186-
if err := web.Rescan(childHash, token, osFlag, enableDetonationFlag, timeoutFlag); err != nil {
187-
return fileUploadedMsg{index: index, err: fmt.Errorf("rescan child %s: %w", childHash[:12], err)}
185+
if file.IsArchive && len(file.DerivedFiles) > 0 {
186+
for _, df := range file.DerivedFiles {
187+
if err := web.Rescan(df.SHA256, token, osFlag, enableDetonationFlag, timeoutFlag); err != nil {
188+
return fileUploadedMsg{index: index, err: fmt.Errorf("rescan child %s: %w", df.SHA256[:12], err)}
188189
}
189190
}
190191
return fileUploadedMsg{
191192
index: index,
192193
sha256: sha256,
193194
size: file.Size,
194195
isArchive: true,
195-
childHashes: file.ArchiveFiles,
196+
childHashes: derivedHashes(file.DerivedFiles),
196197
}
197198
}
198199

@@ -307,11 +308,12 @@ func (m scanModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
307308
m.files[i].sha256 = msg.sha256
308309

309310
if msg.isArchive && len(msg.childHashes) > 0 {
310-
// Archive container: mark it as done immediately and track children.
311-
m.files[i].state = stateDone
311+
// Archive container: poll parent for completion and track children.
312+
m.files[i].state = stateScanning
312313
m.files[i].isArchive = true
313314
m.files[i].childCount = len(msg.childHashes)
314315
m.files[i].size = msg.size
316+
cmds = append(cmds, pollStatusCmd(i, m.web, msg.sha256))
315317

316318
archiveName := filepath.Base(m.files[i].filename)
317319
for _, childHash := range msg.childHashes {
@@ -502,6 +504,9 @@ func (m scanModel) View() string {
502504
f.result.MultiAV.Positives, f.result.MultiAV.EnginesCount)
503505
}
504506
}
507+
if f.result != nil && f.result.Encrypted {
508+
line += renderEncryptionStatus(f.result)
509+
}
505510
doneRows = append(doneRows, doneRow{line})
506511
case stateError:
507512
line := styleError.Render("✗") + " " + name + " " + styleError.Render(f.err.Error())
@@ -541,3 +546,29 @@ func truncSha(sha string) string {
541546
return sha
542547
}
543548

549+
func derivedHashes(files []entity.DerivedFile) []string {
550+
hashes := make([]string, len(files))
551+
for i, f := range files {
552+
hashes[i] = f.SHA256
553+
}
554+
return hashes
555+
}
556+
557+
func renderEncryptionStatus(s *scanSummary) string {
558+
if s.DecryptionSuccess == nil {
559+
return " " + styleWarning.Render("encrypted")
560+
}
561+
if *s.DecryptionSuccess {
562+
out := " " + styleSuccess.Render("decrypted")
563+
if s.SuccessfulPassword != "" {
564+
out += " " + styleDim.Render("(pwd: "+s.SuccessfulPassword+")")
565+
}
566+
return out
567+
}
568+
out := " " + styleError.Render("decryption failed")
569+
if len(s.AttemptedPasswords) > 0 {
570+
out += " " + styleDim.Render("(tried: "+strings.Join(s.AttemptedPasswords, ", ")+")")
571+
}
572+
return out
573+
}
574+

cmd/view.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,26 @@ func printFileReport(file entity.File, webSvc webapi.Service) {
8888
printKV("Packer", strings.Join(file.Packer, ", "))
8989
}
9090
if file.IsArchive {
91-
printKV("Archive", fmt.Sprintf("yes (%d files)", len(file.ArchiveFiles)))
91+
printKV("Archive", fmt.Sprintf("yes (%d files)", len(file.DerivedFiles)))
9292
}
93-
if file.ArchiveSHA256 != "" {
94-
printKV("Parent", file.ArchiveSHA256)
93+
if file.Encrypted {
94+
printKV("Encrypted", "yes")
95+
if file.DecryptionSuccess != nil {
96+
if *file.DecryptionSuccess {
97+
printKV("Decryption", cleanStyle.Render("successful"))
98+
if file.SuccessfulPassword != "" {
99+
printKV("Password", file.SuccessfulPassword)
100+
}
101+
} else {
102+
printKV("Decryption", detectStyle.Render("failed"))
103+
if len(file.AttemptedPasswords) > 0 {
104+
printKV("Attempted", strings.Join(file.AttemptedPasswords, ", "))
105+
}
106+
}
107+
}
108+
}
109+
if file.ParentSHA256 != "" {
110+
printKV("Parent", file.ParentSHA256)
95111
}
96112
if file.FirstSeen != 0 {
97113
printKV("First Seen", formatTimestamp(file.FirstSeen))
@@ -103,8 +119,8 @@ func printFileReport(file entity.File, webSvc webapi.Service) {
103119

104120
if file.IsArchive {
105121
// Archives only scan their children, skip verdict and AV results.
106-
if len(file.ArchiveFiles) > 0 {
107-
printArchiveChildren(file.ArchiveFiles, webSvc)
122+
if len(file.DerivedFiles) > 0 {
123+
printArchiveChildren(file.DerivedFiles, webSvc)
108124
}
109125
} else {
110126
// Classification.
@@ -122,6 +138,7 @@ type childSummary struct {
122138
sha256 string
123139
classification string
124140
format string
141+
size int64
125142
positives int
126143
enginesCount int
127144
err error
@@ -136,6 +153,7 @@ func fetchChildSummary(sha256 string, webSvc webapi.Service) childSummary {
136153
sha256: sha256,
137154
classification: file.Classification,
138155
format: file.Format,
156+
size: file.Size,
139157
}
140158
if file.Extension != "" {
141159
cs.format += "/" + file.Extension
@@ -153,28 +171,30 @@ func fetchChildSummary(sha256 string, webSvc webapi.Service) childSummary {
153171
return cs
154172
}
155173

156-
func printArchiveChildren(archiveFiles []string, webSvc webapi.Service) {
157-
fmt.Println(headerStyle.Render(fmt.Sprintf("Archive Contents (%d files)", len(archiveFiles))))
174+
func printArchiveChildren(derivedFiles []entity.DerivedFile, webSvc webapi.Service) {
175+
fmt.Println(headerStyle.Render(fmt.Sprintf("Archive Contents (%d files)", len(derivedFiles))))
158176
fmt.Println()
159177

160178
// Table header.
161179
fmtCol := lipgloss.NewStyle().Width(16)
180+
sizeCol := lipgloss.NewStyle().Width(10)
162181
avCol := lipgloss.NewStyle().Width(14)
163182
clsCol := lipgloss.NewStyle().Width(12)
164183

165-
fmt.Printf(" %s %s %s %s\n",
184+
fmt.Printf(" %s %s %s %s %s\n",
166185
styleDim.Render(fmt.Sprintf("%-64s", "SHA256")),
167186
styleDim.Render(fmtCol.Render("FORMAT")),
187+
styleDim.Render(sizeCol.Render("SIZE")),
168188
styleDim.Render(avCol.Render("DETECTIONS")),
169189
styleDim.Render(clsCol.Render("VERDICT")),
170190
)
171-
fmt.Printf(" %s\n", styleDim.Render(strings.Repeat("─", 108)))
191+
fmt.Printf(" %s\n", styleDim.Render(strings.Repeat("─", 119)))
172192

173-
for _, sha := range archiveFiles {
174-
cs := fetchChildSummary(sha, webSvc)
193+
for _, df := range derivedFiles {
194+
cs := fetchChildSummary(df.SHA256, webSvc)
175195
if cs.err != nil {
176196
fmt.Printf(" %s %s\n",
177-
sha,
197+
df.SHA256,
178198
styleError.Render("error: "+cs.err.Error()),
179199
)
180200
continue
@@ -187,9 +207,10 @@ func printArchiveChildren(archiveFiles []string, webSvc webapi.Service) {
187207
detStr = cleanStyle.Render(detStr)
188208
}
189209

190-
fmt.Printf(" %s %s %s %s\n",
210+
fmt.Printf(" %s %s %s %s %s\n",
191211
cs.sha256,
192212
fmtCol.Render(cs.format),
213+
sizeCol.Render(formatSize(cs.size)),
193214
avCol.Render(detStr),
194215
clsCol.Render(renderClassification(cs.classification)),
195216
)

internal/entity/file.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,20 @@ type File struct {
3434
BehaviorReportID string `json:"behavior_report_id,omitempty"`
3535
Status int `json:"status,omitempty"`
3636
Classification string `json:"classification,omitempty"`
37-
IsArchive bool `json:"is_archive,omitempty"`
38-
ArchiveFiles []string `json:"archive_files,omitempty"`
39-
ArchiveSHA256 string `json:"archive_sha256,omitempty"`
37+
IsArchive bool `json:"is_archive,omitempty"`
38+
DerivedFiles []DerivedFile `json:"derived_files,omitempty"`
39+
ParentSHA256 string `json:"parent_sha256,omitempty"`
40+
Encrypted bool `json:"encrypted"`
41+
DecryptionSuccess *bool `json:"decryption_success,omitempty"`
42+
SuccessfulPassword string `json:"successful_password,omitempty"`
43+
AttemptedPasswords []string `json:"attempted_passwords,omitempty"`
44+
}
45+
46+
// DerivedFile is a child file produced during analysis of a parent — either a
47+
// member extracted from an archive or the decrypted payload of a protected document.
48+
type DerivedFile struct {
49+
Name string `json:"name"`
50+
SHA256 string `json:"sha256"`
4051
}
4152

4253
// Submission represents a file submission.

0 commit comments

Comments
 (0)