@@ -11,29 +11,17 @@ import (
1111)
1212
1313func 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
3927func 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
209200func 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 , "\n Security:\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+
308417func writeLines (path string , lines []string ) error {
309418 content := strings .Join (lines , "\n " ) + "\n "
310419 return os .WriteFile (path , []byte (content ), 0644 )
0 commit comments