Skip to content

Commit bea8ae4

Browse files
committed
master
1 parent be049ef commit bea8ae4

12 files changed

Lines changed: 738 additions & 0 deletions

File tree

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# ApkSentinel
2+
3+
A high-performance APK static analysis tool for secret detection.
4+
5+
## Features
6+
7+
- **APK Decompilation**: Uses `jadx` to decompile APKs.
8+
- **Secret Scanning**: Regex-based detection of API keys, tokens, and URLs.
9+
- **Parallel Processing**: Fast analysis using Go's concurrency.
10+
- **Modular Reporting**: JSON and HTML report outputs.
11+
- **Extensible**: Add custom patterns via JSON/YAML.
12+
13+
## Installation
14+
15+
```bash
16+
go install -v github.com/ismailtsdln/ApkSentinel@latest
17+
```
18+
19+
## Usage
20+
21+
```bash
22+
# Basic scan
23+
apk-sentinel -i app.apk -o ./report -f json
24+
25+
# Scan with custom patterns
26+
apk-sentinel -i app.apk -p my-patterns.json -o ./report
27+
```
28+
29+
## License
30+
31+
MIT

apk-sentinel

6.35 MB
Binary file not shown.

cmd/apk-sentinel.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/ismailtsdln/ApkSentinel/internal/analyzer"
9+
"github.com/ismailtsdln/ApkSentinel/internal/decompiler"
10+
"github.com/ismailtsdln/ApkSentinel/internal/report"
11+
"github.com/ismailtsdln/ApkSentinel/internal/scanner"
12+
"github.com/ismailtsdln/ApkSentinel/internal/utils"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
var (
17+
inputPath string
18+
outputDir string
19+
outputFormat string
20+
customPatterns string
21+
jadxPath string
22+
verbose bool
23+
)
24+
25+
var rootCmd = &cobra.Command{
26+
Use: "apk-sentinel",
27+
Short: "ApkSentinel is a high-performance APK static analysis tool",
28+
Long: `ApkSentinel is a high-performance APK static analysis tool designed to
29+
detect hard-coded secrets, API keys, and sensitive URLs in Android applications.`,
30+
Run: func(cmd *cobra.Command, args []string) {
31+
utils.PrintBanner()
32+
33+
if inputPath == "" {
34+
utils.Error("Input APK path is required.")
35+
cmd.Help()
36+
os.Exit(1)
37+
}
38+
39+
// 1. Decompile
40+
decomp := decompiler.NewDecompiler(jadxPath, filepath.Join(outputDir, "decompiled"))
41+
decompiledDir, err := decomp.Decompile(inputPath)
42+
if err != nil {
43+
utils.Error("Error during decompilation: %v", err)
44+
os.Exit(1)
45+
}
46+
47+
// 2. Scan
48+
patternFile := customPatterns
49+
if patternFile == "" {
50+
// fallback to default patterns
51+
patternFile = "internal/patterns/default_patterns.json"
52+
}
53+
54+
s, err := scanner.NewScanner(patternFile)
55+
if err != nil {
56+
utils.Error("Error initializing scanner: %v", err)
57+
os.Exit(1)
58+
}
59+
60+
utils.Info("Scanning decompiled source code...")
61+
results, err := s.ScanDirectory(decompiledDir)
62+
if err != nil {
63+
utils.Error("Error during scan: %v", err)
64+
os.Exit(1)
65+
}
66+
67+
// 3. Manifest Analysis
68+
manifestPath := filepath.Join(decompiledDir, "resources", "AndroidManifest.xml")
69+
// Sometimes jadx puts it in the root depending on version/config
70+
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
71+
manifestPath = filepath.Join(decompiledDir, "AndroidManifest.xml")
72+
}
73+
74+
var findings []analyzer.SecurityFinding
75+
if _, err := os.Stat(manifestPath); err == nil {
76+
utils.Info("Analyzing AndroidManifest.xml...")
77+
findings, _ = analyzer.AnalyzeManifest(manifestPath)
78+
}
79+
80+
// 4. Report
81+
r := report.Report{
82+
APKPath: inputPath,
83+
Results: results,
84+
Findings: findings,
85+
}
86+
87+
if outputFormat == "json" || outputFormat == "both" {
88+
if err := report.SaveJSON(r, outputDir); err != nil {
89+
utils.Error("Error saving JSON report: %v", err)
90+
}
91+
}
92+
93+
if outputFormat == "html" || outputFormat == "both" {
94+
if err := report.SaveHTML(r, outputDir); err != nil {
95+
utils.Error("Error saving HTML report: %v", err)
96+
}
97+
}
98+
},
99+
}
100+
101+
func Execute() {
102+
if err := rootCmd.Execute(); err != nil {
103+
fmt.Println(err)
104+
os.Exit(1)
105+
}
106+
}
107+
108+
func init() {
109+
rootCmd.PersistentFlags().StringVarP(&inputPath, "input", "i", "", "Input APK path (required)")
110+
rootCmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "./report", "Output directory")
111+
rootCmd.PersistentFlags().StringVarP(&outputFormat, "format", "f", "json", "Output format (json|html)")
112+
rootCmd.PersistentFlags().StringVarP(&customPatterns, "pattern", "p", "", "Custom pattern file (JSON)")
113+
rootCmd.PersistentFlags().StringVar(&jadxPath, "jadx-path", "jadx", "Path to jadx executable")
114+
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show verbose output")
115+
}
116+
117+
func main() {
118+
Execute()
119+
}

go.mod

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module github.com/ismailtsdln/ApkSentinel
2+
3+
go 1.25.5
4+
5+
require (
6+
github.com/fatih/color v1.18.0
7+
github.com/spf13/cobra v1.10.2
8+
)
9+
10+
require (
11+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
12+
github.com/mattn/go-colorable v0.1.13 // indirect
13+
github.com/mattn/go-isatty v0.0.20 // indirect
14+
github.com/spf13/pflag v1.0.9 // indirect
15+
golang.org/x/sys v0.25.0 // indirect
16+
)

go.sum

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
2+
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
3+
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
4+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
5+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
6+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
7+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
8+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
9+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
10+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
11+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
12+
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
13+
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
14+
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
15+
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
16+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
17+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
18+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
19+
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
20+
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
21+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

internal/analyzer/manifest.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package analyzer
2+
3+
import (
4+
"encoding/xml"
5+
"fmt"
6+
"os"
7+
)
8+
9+
// Manifest represents a simplified AndroidManifest.xml structure.
10+
type Manifest struct {
11+
XMLName xml.Name `xml:"manifest"`
12+
Application Application `xml:"application"`
13+
}
14+
15+
// Application represents the application tag in AndroidManifest.xml.
16+
type Application struct {
17+
Debuggable string `xml:"http://schemas.android.com/apk/res/android debuggable,attr"`
18+
AllowBackup string `xml:"http://schemas.android.com/apk/res/android allowBackup,attr"`
19+
}
20+
21+
// SecurityFinding represents a security issue found in the manifest or resources.
22+
type SecurityFinding struct {
23+
Type string
24+
Description string
25+
Severity string
26+
}
27+
28+
// AnalyzeManifest parses AndroidManifest.xml and returns security findings.
29+
func AnalyzeManifest(manifestPath string) ([]SecurityFinding, error) {
30+
data, err := os.ReadFile(manifestPath)
31+
if err != nil {
32+
return nil, fmt.Errorf("failed to read manifest: %w", err)
33+
}
34+
35+
var manifest Manifest
36+
if err := xml.Unmarshal(data, &manifest); err != nil {
37+
return nil, fmt.Errorf("failed to unmarshal manifest: %w", err)
38+
}
39+
40+
var findings []SecurityFinding
41+
42+
if manifest.Application.Debuggable == "true" {
43+
findings = append(findings, SecurityFinding{
44+
Type: "Manifest",
45+
Description: "Application is debuggable",
46+
Severity: "high",
47+
})
48+
}
49+
50+
if manifest.Application.AllowBackup != "false" {
51+
findings = append(findings, SecurityFinding{
52+
Type: "Manifest",
53+
Description: "Application allows backup (allowBackup is not false)",
54+
Severity: "medium",
55+
})
56+
}
57+
58+
return findings, nil
59+
}

internal/analyzer/obfuscation.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package analyzer
2+
3+
import (
4+
"encoding/base64"
5+
"regexp"
6+
)
7+
8+
// ObfuscationResolver provides methods to decode common obfuscation techniques.
9+
type ObfuscationResolver struct {
10+
base64Regex *regexp.Regexp
11+
}
12+
13+
// NewObfuscationResolver initializes a new ObfuscationResolver.
14+
func NewObfuscationResolver() *ObfuscationResolver {
15+
return &ObfuscationResolver{
16+
// Regex to find potential Base64 strings (minimum length 8 to avoid false positives)
17+
base64Regex: regexp.MustCompile(`[A-Za-z0-9+/]{8,}=*`),
18+
}
19+
}
20+
21+
// ResolveBase64 looks for Base64 encoded strings in a line and returns the decoded versions.
22+
func (or *ObfuscationResolver) ResolveBase64(input string) []string {
23+
var decodedStrings []string
24+
matches := or.base64Regex.FindAllString(input, -1)
25+
26+
for _, match := range matches {
27+
decoded, err := base64.StdEncoding.DecodeString(match)
28+
if err == nil {
29+
// Only add if it contains printable characters (rough check)
30+
if isPrintable(decoded) {
31+
decodedStrings = append(decodedStrings, string(decoded))
32+
}
33+
}
34+
}
35+
return decodedStrings
36+
}
37+
38+
// ResolveXOR attempts simple XOR decoding (future improvement: brute force common keys).
39+
func (or *ObfuscationResolver) ResolveXOR(input string, key byte) string {
40+
data := []byte(input)
41+
for i := range data {
42+
data[i] ^= key
43+
}
44+
return string(data)
45+
}
46+
47+
// isPrintable checks if the byte slice contains mostly printable ASCII characters.
48+
func isPrintable(data []byte) bool {
49+
if len(data) == 0 {
50+
return false
51+
}
52+
printable := 0
53+
for _, b := range data {
54+
if (b >= 32 && b <= 126) || b == '\n' || b == '\r' || b == '\t' {
55+
printable++
56+
}
57+
}
58+
return float64(printable)/float64(len(data)) > 0.8
59+
}

internal/decompiler/decompiler.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package decompiler
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
9+
"github.com/ismailtsdln/ApkSentinel/internal/utils"
10+
)
11+
12+
// Decompiler handles the APK to Java/Smali conversion logic.
13+
type Decompiler struct {
14+
JadxPath string
15+
OutputDir string
16+
}
17+
18+
// NewDecompiler returns a new Decompiler instance.
19+
func NewDecompiler(jadxPath, outputDir string) *Decompiler {
20+
return &Decompiler{
21+
JadxPath: jadxPath,
22+
OutputDir: outputDir,
23+
}
24+
}
25+
26+
// Decompile executes the jadx command to decompile the given APK path.
27+
func (d *Decompiler) Decompile(apkPath string) (string, error) {
28+
if _, err := os.Stat(apkPath); os.IsNotExist(err) {
29+
return "", fmt.Errorf("APK file not found: %s", apkPath)
30+
}
31+
32+
absAPKPath, err := filepath.Abs(apkPath)
33+
if err != nil {
34+
return "", fmt.Errorf("failed to get absolute path of APK: %w", err)
35+
}
36+
37+
// Create output directory if it doesn't exist
38+
if err := os.MkdirAll(d.OutputDir, 0755); err != nil {
39+
return "", fmt.Errorf("failed to create output directory: %w", err)
40+
}
41+
42+
utils.Info("Decompiling %s to %s...", apkPath, d.OutputDir)
43+
44+
// Build the jadx command
45+
// -d: output directory
46+
cmd := exec.Command(d.JadxPath, "-d", d.OutputDir, absAPKPath)
47+
cmd.Stdout = os.Stdout
48+
cmd.Stderr = os.Stderr
49+
50+
if err := cmd.Run(); err != nil {
51+
return "", fmt.Errorf("jadx execution failed: %w", err)
52+
}
53+
54+
utils.Success("Decompilation completed successfully.")
55+
return d.OutputDir, nil
56+
}

0 commit comments

Comments
 (0)