Skip to content

Commit 3013d2a

Browse files
committed
feat: default structured output to CWD when --outDir not set; enrich summary details and fix hasDecompiled flag
1 parent b682290 commit 3013d2a

1 file changed

Lines changed: 158 additions & 49 deletions

File tree

cmd/output.go

Lines changed: 158 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,17 @@ import (
1111
)
1212

1313
func outputResults(results *models.Results, config *CLIConfig) error {
14-
15-
if config.OutputDir != "" {
16-
return saveStructuredOutput(results, config)
17-
}
18-
19-
// Otherwise, output to stdout in requested format
20-
var output []byte
21-
var err error
22-
23-
switch config.OutputFormat {
24-
case "json":
25-
output, err = json.MarshalIndent(results, "", " ")
14+
// If outDir is not specified, default to current directory
15+
// and create the package-named folder there.
16+
if config.OutputDir == "" {
17+
cwd, err := os.Getwd()
2618
if err != nil {
27-
return fmt.Errorf("failed to marshal results: %w", err)
19+
return fmt.Errorf("failed to get current directory: %w", err)
2820
}
29-
case "text":
30-
output = []byte(formatTextReport(results))
31-
default:
32-
return fmt.Errorf("unsupported output format: %s", config.OutputFormat)
21+
config.OutputDir = cwd
3322
}
3423

35-
fmt.Println(string(output))
36-
return nil
24+
return saveStructuredOutput(results, config)
3725
}
3826

3927
func saveStructuredOutput(results *models.Results, config *CLIConfig) error {
@@ -175,12 +163,15 @@ func saveStructuredOutput(results *models.Results, config *CLIConfig) error {
175163
}
176164
}
177165

166+
hasDecompiled := false
178167
if results.DecompiledDirPath != "" {
179168
decompiledSrc := results.DecompiledDirPath
180169
decompiledDest := filepath.Join(outDir, "decompiled")
181170

182171
if _, err := os.Stat(decompiledSrc); err == nil {
172+
hasDecompiled = true
183173
if err := copyDirectory(decompiledSrc, decompiledDest); err != nil {
174+
hasDecompiled = false
184175
if config.Verbose {
185176
fmt.Printf("Warning: failed to copy decompiled directory: %v\n", err)
186177
}
@@ -201,54 +192,165 @@ func saveStructuredOutput(results *models.Results, config *CLIConfig) error {
201192
return fmt.Errorf("failed to write analysis: %w", err)
202193
}
203194

204-
displayOutputSummary(results, outDir, allURLs, len(allAssets), results.DecompiledDirPath != "", config.Verbose)
195+
displayOutputSummary(results, outDir, allURLs, len(allAssets), hasDecompiled, config.Verbose)
205196

206197
return nil
207198
}
208199

209200
func displayOutputSummary(results *models.Results, outDir string, allURLs []string, assetCount int, hasDecompiled bool, verbose bool) {
210-
fmt.Fprintf(os.Stderr, "\n📊 Analysis Results:\n")
211-
fmt.Fprintf(os.Stderr, " Saved to: %s\n\n", outDir)
212-
213-
stats := make([]string, 0)
214-
215-
if len(results.Emails) > 0 {
216-
stats = append(stats, fmt.Sprintf(" 📧 Emails: %d", len(results.Emails)))
201+
// App identity
202+
pkg := ""
203+
verName := ""
204+
verCode := ""
205+
minSDK := ""
206+
targetSDK := ""
207+
if results.AAPT2Metadata != nil && results.AAPT2Metadata.Badging != nil {
208+
b := results.AAPT2Metadata.Badging
209+
if b.PackageName != "" {
210+
pkg = b.PackageName
211+
}
212+
verName = b.VersionName
213+
verCode = b.VersionCode
214+
minSDK = b.MinSdkVersion
215+
targetSDK = b.TargetSdkVersion
217216
}
218-
if len(results.Domains) > 0 {
219-
stats = append(stats, fmt.Sprintf(" 🌐 Domains: %d", len(results.Domains)))
217+
if pkg == "" && results.AppInfo.PackageName != "" {
218+
pkg = results.AppInfo.PackageName
220219
}
221-
if len(allURLs) > 0 {
222-
stats = append(stats, fmt.Sprintf(" 🔗 URLs: %d", len(allURLs)))
220+
if verName == "" && results.AppInfo.VersionName != "" {
221+
verName = results.AppInfo.VersionName
223222
}
224-
if len(results.APIEndpoints) > 0 {
225-
stats = append(stats, fmt.Sprintf(" 🔌 API Endpoints: %d", len(results.APIEndpoints)))
223+
if verCode == "" && results.AppInfo.VersionCode != "" {
224+
verCode = results.AppInfo.VersionCode
226225
}
227-
if len(results.Packages) > 0 {
228-
stats = append(stats, fmt.Sprintf(" 📦 Packages: %d", len(results.Packages)))
226+
if minSDK == "" && results.AppInfo.MinSDKVersion != "" {
227+
minSDK = results.AppInfo.MinSDKVersion
229228
}
230-
if len(results.Permissions) > 0 {
231-
stats = append(stats, fmt.Sprintf(" 🛡️ Permissions: %d", len(results.Permissions)))
229+
if targetSDK == "" && results.AppInfo.TargetSDK != "" {
230+
targetSDK = results.AppInfo.TargetSDK
232231
}
233-
if len(results.HardcodedKeys) > 0 {
234-
stats = append(stats, fmt.Sprintf(" 🔑 Secrets: %d", len(results.HardcodedKeys)))
232+
233+
fmt.Fprintf(os.Stderr, "\n📊 Analysis Results\n")
234+
fmt.Fprintf(os.Stderr, " Saved to: %s\n", outDir)
235+
if pkg != "" {
236+
if verName != "" || verCode != "" {
237+
fmt.Fprintf(os.Stderr, " App: %s (v%s, code %s)\n", pkg, safeStr(verName), safeStr(verCode))
238+
} else {
239+
fmt.Fprintf(os.Stderr, " App: %s\n", pkg)
240+
}
235241
}
236-
if len(results.Services) > 0 {
237-
stats = append(stats, fmt.Sprintf(" 🔗 Services: %d", len(results.Services)))
242+
if minSDK != "" || targetSDK != "" {
243+
fmt.Fprintf(os.Stderr, " SDK: min %s, target %s\n", safeStr(minSDK), safeStr(targetSDK))
238244
}
239-
if assetCount > 0 {
240-
stats = append(stats, fmt.Sprintf(" 🎨 Assets: %d files", assetCount))
245+
fmt.Fprintln(os.Stderr)
246+
247+
// High-level stats
248+
type kv struct {
249+
k string
250+
v int
251+
}
252+
stats := []kv{
253+
{"📧 Emails", len(results.Emails)},
254+
{"🌐 Domains", len(results.Domains)},
255+
{"🔗 URLs", len(allURLs)},
256+
{"🔌 API Endpoints", len(results.APIEndpoints)},
257+
{"📦 Packages", len(results.Packages)},
258+
{"🛡️ Permissions", len(results.Permissions)},
259+
{"🔑 Secrets", len(results.HardcodedKeys)},
260+
{"🔗 Services", len(results.Services)},
261+
{"🎨 Assets", assetCount},
241262
}
242263
if hasDecompiled {
243-
stats = append(stats, fmt.Sprintf(" 📁 Decompiled: Full APK source"))
264+
fmt.Fprintf(os.Stderr, " 📁 Decompiled: Full APK source\n")
265+
}
266+
// Print stats in two columns
267+
printed := 0
268+
for _, s := range stats {
269+
if s.v > 0 {
270+
fmt.Fprintf(os.Stderr, " %s: %d", s.k, s.v)
271+
printed++
272+
if printed%2 == 0 {
273+
fmt.Fprintln(os.Stderr)
274+
} else {
275+
fmt.Fprint(os.Stderr, " |")
276+
}
277+
}
278+
}
279+
if printed%2 != 0 {
280+
fmt.Fprintln(os.Stderr)
244281
}
245282

246-
for i := 0; i < len(stats); i++ {
247-
fmt.Fprintf(os.Stderr, "%s", stats[i])
248-
if (i+1)%2 == 0 || i == len(stats)-1 {
249-
fmt.Fprintf(os.Stderr, "\n")
283+
// Highlights (top items)
284+
showTop := func(title string, items []string, limit int) {
285+
if len(items) == 0 {
286+
return
287+
}
288+
n := limit
289+
if len(items) < n {
290+
n = len(items)
291+
}
292+
fmt.Fprintf(os.Stderr, "\n%s:\n", title)
293+
for i := 0; i < n; i++ {
294+
fmt.Fprintf(os.Stderr, " • %s\n", items[i])
295+
}
296+
if len(items) > n {
297+
fmt.Fprintf(os.Stderr, " …and %d more\n", len(items)-n)
298+
}
299+
}
300+
301+
if len(results.Domains) > 0 {
302+
showTop("Top domains", results.Domains, 5)
303+
}
304+
if len(results.APIEndpoints) > 0 {
305+
eps := make([]string, 0, len(results.APIEndpoints))
306+
for _, ep := range results.APIEndpoints {
307+
m := ep.Method
308+
if m == "" {
309+
m = "GET"
310+
}
311+
eps = append(eps, fmt.Sprintf("%s %s", m, ep.URL))
312+
}
313+
showTop("API endpoints", eps, 5)
314+
}
315+
if len(results.HardcodedKeys) > 0 {
316+
showTop("Potential secrets", results.HardcodedKeys, 5)
317+
}
318+
319+
// Security quick glance
320+
securityLines := []string{}
321+
if results.CertificateInfo != nil {
322+
cc := len(results.CertificateInfo.Certificates)
323+
if cc > 0 {
324+
selfSigned := 0
325+
expired := 0
326+
for _, c := range results.CertificateInfo.Certificates {
327+
if c.IsSelfSigned {
328+
selfSigned++
329+
}
330+
if c.IsExpired {
331+
expired++
332+
}
333+
}
334+
securityLines = append(securityLines, fmt.Sprintf("Certificates: %d (self-signed: %d, expired: %d)", cc, selfSigned, expired))
335+
}
336+
}
337+
if results.NetworkSecurity != nil {
338+
if results.NetworkSecurity.CleartextAllowed {
339+
securityLines = append(securityLines, "Cleartext traffic: allowed")
250340
} else {
251-
fmt.Fprintf(os.Stderr, " | ")
341+
securityLines = append(securityLines, "Cleartext traffic: disallowed")
342+
}
343+
if results.NetworkSecurity.CertificatePinning {
344+
securityLines = append(securityLines, "Certificate pinning: enabled")
345+
}
346+
}
347+
if results.Obfuscation != nil && results.Obfuscation.LikelyObfuscated {
348+
securityLines = append(securityLines, "Code obfuscation detected")
349+
}
350+
if len(securityLines) > 0 {
351+
fmt.Fprintf(os.Stderr, "\nSecurity:\n")
352+
for _, l := range securityLines {
353+
fmt.Fprintf(os.Stderr, " • %s\n", l)
252354
}
253355
}
254356

@@ -305,6 +407,13 @@ func sanitizeFileName(name string) string {
305407
return name
306408
}
307409

410+
func safeStr(s string) string {
411+
if strings.TrimSpace(s) == "" {
412+
return "-"
413+
}
414+
return s
415+
}
416+
308417
func writeLines(path string, lines []string) error {
309418
content := strings.Join(lines, "\n") + "\n"
310419
return os.WriteFile(path, []byte(content), 0644)

0 commit comments

Comments
 (0)