adb-toolkit/backend_apkaudit_test.go

177 lines
5.5 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"testing"
)
// TestAuditMiniAPK is a manual smoke test against a real APK on disk.
// Run: go test -run TestAuditMiniAPK -v
func TestAuditMiniAPK(t *testing.T) {
apk := os.Getenv("AUDIT_APK")
if apk == "" {
apk = "/home/xyz/.local/share/apktool/framework/1.apk"
}
if _, err := os.Stat(apk); err != nil {
t.Skipf("test apk not present: %v", err)
}
app := NewApp()
res, err := app.AuditAPK(apk)
if err != nil {
t.Fatalf("AuditAPK error: %v", err)
}
dumpAudit(t, res)
// Explorer: read the (binary) manifest and a resource entry.
ent, err := app.ReadAPKEntry(res.LocalPath, "AndroidManifest.xml")
if err != nil {
t.Errorf("ReadAPKEntry manifest: %v", err)
} else {
fmt.Printf("\nentry AndroidManifest.xml: kind=%s size=%d truncated=%v hexlines=%d\n",
ent.Kind, ent.Size, ent.Truncated, len(splitLines(ent.Hex)))
}
// Export builders (skip the GUI save dialog; just validate serialization).
csv := auditToCSV(res)
fmt.Printf("CSV rows=%d firstline=%q\n", len(splitLines(csv)), firstLine(csv))
if b, err := json.Marshal(auditToSARIF(res)); err != nil {
t.Errorf("SARIF marshal: %v", err)
} else {
fmt.Printf("SARIF bytes=%d\n", len(b))
}
}
// TestParity compares the reference (aapt2/apksigner) parse against the pure-Go
// fallback on the same APK, so we can confirm the hybrid behaves the same.
// Run: go test -run TestParity -v (uses AUDIT_APK or the framework apk)
func TestParity(t *testing.T) {
apk := os.Getenv("AUDIT_APK")
if apk == "" {
apk = "/home/xyz/.local/share/apktool/framework/1.apk"
}
if _, err := os.Stat(apk); err != nil {
t.Skipf("test apk not present: %v", err)
}
app := NewApp()
ref, err := app.AuditAPK(apk) // reference path (tools present on this box)
if err != nil {
t.Fatalf("reference audit: %v", err)
}
var go_ APKAudit
go_.Counts = map[string]int{}
parseManifestGo(apk, &go_)
parseCertGo(apk, &go_)
finalizeCert(&go_)
expCount := func(a APKAudit) (n int) {
for _, c := range a.Components {
if c.Exported || c.ExportedImplicit {
n++
}
}
return
}
fmt.Printf("\n%-16s | %-28s | %-28s\n", "field", "aapt2/apksigner (ref)", "pure-Go (fallback)")
row := func(label, a, b string) {
flag := ""
if a != b {
flag = " <-- DIFF"
}
fmt.Printf("%-16s | %-28s | %-28s%s\n", label, a, b, flag)
}
row("package", ref.PackageName, go_.PackageName)
row("versionName", ref.VersionName, go_.VersionName)
row("versionCode", ref.VersionCode, go_.VersionCode)
row("minSdk", ref.MinSDK, go_.MinSDK)
row("targetSdk", ref.TargetSDK, go_.TargetSDK)
row("permissions", itoa(len(ref.Permissions)), itoa(len(go_.Permissions)))
row("components", itoa(len(ref.Components)), itoa(len(go_.Components)))
row("exported", itoa(expCount(ref)), itoa(expCount(go_)))
row("debuggable", b2s(ref.Debuggable), b2s(go_.Debuggable))
row("allowBackup", b2s(ref.AllowBackup), b2s(go_.AllowBackup))
row("cert.verified", b2s(ref.Cert.Verified), b2s(go_.Cert.Verified))
row("cert.v1/v2/v3", schemes(ref.Cert), schemes(go_.Cert))
row("cert.sha256", trunc16(ref.Cert.SHA256), trunc16(go_.Cert.SHA256))
if ref.PackageName != go_.PackageName {
t.Errorf("package mismatch: %q vs %q", ref.PackageName, go_.PackageName)
}
if ref.Cert.SHA256 != "" && go_.Cert.SHA256 != "" && ref.Cert.SHA256 != go_.Cert.SHA256 {
t.Errorf("cert SHA-256 mismatch: %q vs %q", ref.Cert.SHA256, go_.Cert.SHA256)
}
}
func itoa(n int) string { return fmt.Sprintf("%d", n) }
func b2s(b bool) string { return fmt.Sprintf("%v", b) }
func schemes(c APKCertInfo) string { return fmt.Sprintf("%v/%v/%v", c.V1, c.V2, c.V3) }
func trunc16(s string) string {
if len(s) > 16 {
return s[:16] + "…"
}
return s
}
func splitLines(s string) []string {
if s == "" {
return nil
}
var n []string
for _, l := range strings.Split(s, "\n") {
if l != "" {
n = append(n, l)
}
}
return n
}
// TestAuditInstalled audits a package off the connected device.
// Run: AUDIT_PKG=com.android.settings go test -run TestAuditInstalled -v
func TestAuditInstalled(t *testing.T) {
pkg := os.Getenv("AUDIT_PKG")
if pkg == "" {
t.Skip("set AUDIT_PKG to audit an installed package")
}
app := NewApp()
res, err := app.AuditInstalledApp(pkg)
if err != nil {
t.Fatalf("AuditInstalledApp error: %v", err)
}
dumpAudit(t, res)
}
func dumpAudit(t *testing.T, res APKAudit) {
t.Helper()
fmt.Printf("\n=== %s (%s v%s)\n", res.AppLabel, res.PackageName, res.VersionName)
fmt.Printf("score=%d grade=%s min=%s target=%s\n", res.Score, res.Grade, res.MinSDK, res.TargetSDK)
fmt.Printf("perms=%d components=%d files=%d trackers=%d findings=%d\n",
len(res.Permissions), len(res.Components), len(res.Files), len(res.Trackers), len(res.Findings))
fmt.Printf("cert: verified=%v v1=%v v2=%v v3=%v debug=%v err=%q\n",
res.Cert.Verified, res.Cert.V1, res.Cert.V2, res.Cert.V3, res.Cert.IsDebug, res.Cert.Error)
fmt.Printf("flags: debuggable=%v allowBackup=%v cleartext=%v nsc=%v\n",
res.Debuggable, res.AllowBackup, res.UsesCleartext, res.HasNSC)
fmt.Println("counts:", res.Counts)
fmt.Println("--- findings ---")
for _, f := range res.Findings {
fmt.Printf("[%-8s] %-40s (%d matches) %s %s\n", f.Severity, f.Title, len(f.Matches), f.CWE, f.Masvs)
}
fmt.Println("--- trackers ---")
for _, tr := range res.Trackers {
fmt.Printf(" %-22s %-16s x%d\n", tr.Name, tr.Category, tr.Matches)
}
if res.PackageName == "" {
t.Error("expected a package name from aapt2")
}
// quick JSON round-trip to ensure it serializes for the frontend
if _, err := json.Marshal(res); err != nil {
t.Errorf("json marshal failed: %v", err)
}
}