mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-10-12 09:27:11 +02:00
Merge pull request #28 from FuzzingLabs/feature/android-workflow-conversion
feat: Android Static Analysis Workflow with ARM64 Support
This commit is contained in:
commit
3b25edef19
33 files changed
+3347
-28
No files matched your search
@@ -47,8 +47,40 @@ for worker in $WORKERS; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check required files
|
# Check Dockerfile (single file or multi-platform pattern)
|
||||||
REQUIRED_FILES=("Dockerfile" "requirements.txt" "worker.py")
|
if [ -f "$WORKER_DIR/Dockerfile" ]; then
|
||||||
|
# Single Dockerfile
|
||||||
|
if ! git ls-files --error-unmatch "$WORKER_DIR/Dockerfile" &> /dev/null; then
|
||||||
|
echo -e "${RED} ❌ File not tracked by git: $WORKER_DIR/Dockerfile${NC}"
|
||||||
|
echo -e "${YELLOW} Check .gitignore patterns!${NC}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo -e "${GREEN} ✓ Dockerfile (tracked)${NC}"
|
||||||
|
fi
|
||||||
|
elif compgen -G "$WORKER_DIR/Dockerfile.*" > /dev/null; then
|
||||||
|
# Multi-platform Dockerfiles (e.g., Dockerfile.amd64, Dockerfile.arm64)
|
||||||
|
PLATFORM_DOCKERFILES=$(ls "$WORKER_DIR"/Dockerfile.* 2>/dev/null)
|
||||||
|
DOCKERFILE_FOUND=false
|
||||||
|
for dockerfile in $PLATFORM_DOCKERFILES; do
|
||||||
|
if git ls-files --error-unmatch "$dockerfile" &> /dev/null; then
|
||||||
|
echo -e "${GREEN} ✓ $(basename "$dockerfile") (tracked)${NC}"
|
||||||
|
DOCKERFILE_FOUND=true
|
||||||
|
else
|
||||||
|
echo -e "${RED} ❌ File not tracked by git: $dockerfile${NC}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$DOCKERFILE_FOUND" = false ]; then
|
||||||
|
echo -e "${RED} ❌ No platform-specific Dockerfiles found${NC}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED} ❌ Missing Dockerfile or Dockerfile.* files${NC}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check other required files
|
||||||
|
REQUIRED_FILES=("requirements.txt" "worker.py")
|
||||||
for file in "${REQUIRED_FILES[@]}"; do
|
for file in "${REQUIRED_FILES[@]}"; do
|
||||||
FILE_PATH="$WORKER_DIR/$file"
|
FILE_PATH="$WORKER_DIR/$file"
|
||||||
|
|
||||||
|
|||||||
+87
-1
@@ -5,7 +5,93 @@ All notable changes to FuzzForge will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [0.7.0] - 2025-01-16
|
## [Unreleased]
|
||||||
|
|
||||||
|
### 🎯 Major Features
|
||||||
|
|
||||||
|
#### Android Static Analysis Workflow
|
||||||
|
- **Added comprehensive Android security testing workflow** (`android_static_analysis`):
|
||||||
|
- Jadx decompiler for APK → Java source code decompilation
|
||||||
|
- OpenGrep/Semgrep static analysis with custom Android security rules
|
||||||
|
- MobSF integration for comprehensive mobile security scanning
|
||||||
|
- SARIF report generation with unified findings format
|
||||||
|
- Test results: Successfully decompiled 4,145 Java files, found 8 security vulnerabilities
|
||||||
|
- Full workflow completes in ~1.5 minutes
|
||||||
|
|
||||||
|
#### Platform-Aware Worker Architecture
|
||||||
|
- **ARM64 (Apple Silicon) support**:
|
||||||
|
- Automatic platform detection (ARM64 vs x86_64) in CLI using `platform.machine()`
|
||||||
|
- Worker metadata convention (`metadata.yaml`) for platform-specific capabilities
|
||||||
|
- Multi-Dockerfile support: `Dockerfile.amd64` (full toolchain) and `Dockerfile.arm64` (optimized)
|
||||||
|
- Conditional module imports for graceful degradation (MobSF skips on ARM64)
|
||||||
|
- Backend path resolution via `FUZZFORGE_HOST_ROOT` for CLI worker management
|
||||||
|
- **Worker selection logic**:
|
||||||
|
- CLI automatically selects appropriate Dockerfile based on detected platform
|
||||||
|
- Multi-strategy path resolution (API → .fuzzforge marker → environment variable)
|
||||||
|
- Platform-specific tool availability documented in metadata
|
||||||
|
|
||||||
|
#### Python SAST Workflow
|
||||||
|
- **Added Python Static Application Security Testing workflow** (`python_sast`):
|
||||||
|
- Bandit for Python security linting (SAST)
|
||||||
|
- MyPy for static type checking
|
||||||
|
- Safety for dependency vulnerability scanning
|
||||||
|
- Integrated SARIF reporter for unified findings format
|
||||||
|
- Auto-start Python worker on-demand
|
||||||
|
|
||||||
|
### ✨ Enhancements
|
||||||
|
|
||||||
|
#### CI/CD Improvements
|
||||||
|
- Added automated worker validation in CI pipeline
|
||||||
|
- Docker build checks for all workers before merge
|
||||||
|
- Worker file change detection for selective builds
|
||||||
|
- Optimized Docker layer caching for faster builds
|
||||||
|
- Dev branch testing workflow triggers
|
||||||
|
|
||||||
|
#### CLI Improvements
|
||||||
|
- Fixed live monitoring bug in `ff monitor live` command
|
||||||
|
- Enhanced `ff findings` command with better table formatting
|
||||||
|
- Improved `ff monitor` with clearer status displays
|
||||||
|
- Auto-start workers on-demand when workflows require them
|
||||||
|
- Better error messages with actionable manual start commands
|
||||||
|
|
||||||
|
#### Worker Management
|
||||||
|
- Standardized worker service names (`worker-python`, `worker-android`, etc.)
|
||||||
|
- Added missing `worker-secrets` to repository
|
||||||
|
- Improved worker naming consistency across codebase
|
||||||
|
|
||||||
|
#### LiteLLM Integration
|
||||||
|
- Centralized LLM provider management with proxy
|
||||||
|
- Governance and request/response routing
|
||||||
|
- OTEL collector integration for observability
|
||||||
|
- Environment-based configurable timeouts
|
||||||
|
- Optional `.env.litellm` configuration
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Fixed MobSF API key generation from secret file (SHA256 hash)
|
||||||
|
- Corrected Temporal activity names (decompile_with_jadx, scan_with_opengrep, scan_with_mobsf)
|
||||||
|
- Resolved linter errors across codebase
|
||||||
|
- Fixed unused import issues to pass CI checks
|
||||||
|
- Removed deprecated workflow parameters
|
||||||
|
- Docker Compose version compatibility fixes
|
||||||
|
|
||||||
|
### 🔧 Technical Changes
|
||||||
|
|
||||||
|
- Conditional import pattern for optional dependencies (MobSF on ARM64)
|
||||||
|
- Multi-platform Dockerfile architecture
|
||||||
|
- Worker metadata convention for capability declaration
|
||||||
|
- Improved CI worker build optimization
|
||||||
|
- Enhanced storage activity error handling
|
||||||
|
|
||||||
|
### 📝 Test Projects
|
||||||
|
|
||||||
|
- Added `test_projects/android_test/` with BeetleBug.apk and shopnest.apk
|
||||||
|
- Android workflow validation with real APK samples
|
||||||
|
- ARM64 platform testing and validation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.7.0] - 2025-10-16
|
||||||
|
|
||||||
### 🎯 Major Features
|
### 🎯 Major Features
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Copyright (c) 2025 FuzzingLabs
|
||||||
|
#
|
||||||
|
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
|
||||||
|
# at the root of this repository for details.
|
||||||
|
#
|
||||||
|
# After the Change Date (four years from publication), this version of the
|
||||||
|
# Licensed Work will be made available under the Apache License, Version 2.0.
|
||||||
|
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Additional attribution and requirements are provided in the NOTICE file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
System information endpoints for FuzzForge API.
|
||||||
|
|
||||||
|
Provides system configuration and filesystem paths to CLI for worker management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/system", tags=["system"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/info")
|
||||||
|
async def get_system_info() -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Get system information including host filesystem paths.
|
||||||
|
|
||||||
|
This endpoint exposes paths needed by the CLI to manage workers via docker-compose.
|
||||||
|
The FUZZFORGE_HOST_ROOT environment variable is set by docker-compose and points
|
||||||
|
to the FuzzForge installation directory on the host machine.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- host_root: Absolute path to FuzzForge root on host
|
||||||
|
- docker_compose_path: Path to docker-compose.yml on host
|
||||||
|
- workers_dir: Path to workers directory on host
|
||||||
|
"""
|
||||||
|
host_root = os.getenv("FUZZFORGE_HOST_ROOT", "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"host_root": host_root,
|
||||||
|
"docker_compose_path": f"{host_root}/docker-compose.yml" if host_root else "",
|
||||||
|
"workers_dir": f"{host_root}/workers" if host_root else "",
|
||||||
|
}
|
||||||
+2
-1
@@ -24,7 +24,7 @@ from fastmcp.server.http import create_sse_app
|
|||||||
|
|
||||||
from src.temporal.manager import TemporalManager
|
from src.temporal.manager import TemporalManager
|
||||||
from src.core.setup import setup_result_storage, validate_infrastructure
|
from src.core.setup import setup_result_storage, validate_infrastructure
|
||||||
from src.api import workflows, runs, fuzzing
|
from src.api import workflows, runs, fuzzing, system
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
@@ -76,6 +76,7 @@ app = FastAPI(
|
|||||||
app.include_router(workflows.router)
|
app.include_router(workflows.router)
|
||||||
app.include_router(runs.router)
|
app.include_router(runs.router)
|
||||||
app.include_router(fuzzing.router)
|
app.include_router(fuzzing.router)
|
||||||
|
app.include_router(system.router)
|
||||||
|
|
||||||
|
|
||||||
def get_temporal_status() -> Dict[str, Any]:
|
def get_temporal_status() -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""
|
||||||
|
Android Security Analysis Modules
|
||||||
|
|
||||||
|
Modules for Android application security testing:
|
||||||
|
- JadxDecompiler: APK decompilation using Jadx
|
||||||
|
- MobSFScanner: Mobile security analysis using MobSF
|
||||||
|
- OpenGrepAndroid: Static analysis using OpenGrep/Semgrep with Android-specific rules
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Copyright (c) 2025 FuzzingLabs
|
||||||
|
#
|
||||||
|
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
|
||||||
|
# at the root of this repository for details.
|
||||||
|
#
|
||||||
|
# After the Change Date (four years from publication), this version of the
|
||||||
|
# Licensed Work will be made available under the Apache License, Version 2.0.
|
||||||
|
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Additional attribution and requirements are provided in the NOTICE file.
|
||||||
|
|
||||||
|
from .jadx_decompiler import JadxDecompiler
|
||||||
|
from .opengrep_android import OpenGrepAndroid
|
||||||
|
|
||||||
|
# MobSF is optional (not available on ARM64 platform)
|
||||||
|
try:
|
||||||
|
from .mobsf_scanner import MobSFScanner
|
||||||
|
__all__ = ["JadxDecompiler", "MobSFScanner", "OpenGrepAndroid"]
|
||||||
|
except ImportError:
|
||||||
|
# MobSF dependencies not available (e.g., ARM64 platform)
|
||||||
|
MobSFScanner = None
|
||||||
|
__all__ = ["JadxDecompiler", "OpenGrepAndroid"]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
rules:
|
||||||
|
- id: clipboard-sensitive-data
|
||||||
|
severity: WARNING
|
||||||
|
languages: [java]
|
||||||
|
message: "Sensitive data may be copied to the clipboard."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
category: security
|
||||||
|
area: clipboard
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/*.java"
|
||||||
|
pattern: "$CLIPBOARD.setPrimaryClip($CLIP)"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
rules:
|
||||||
|
- id: hardcoded-secrets
|
||||||
|
severity: WARNING
|
||||||
|
languages: [java]
|
||||||
|
message: "Possible hardcoded secret found in variable '$NAME'."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
owasp-mobile: M2
|
||||||
|
category: secrets
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/*.java"
|
||||||
|
patterns:
|
||||||
|
- pattern-either:
|
||||||
|
- pattern: 'String $NAME = "$VAL";'
|
||||||
|
- pattern: 'final String $NAME = "$VAL";'
|
||||||
|
- pattern: 'private String $NAME = "$VAL";'
|
||||||
|
- pattern: 'public static String $NAME = "$VAL";'
|
||||||
|
- pattern: 'static final String $NAME = "$VAL";'
|
||||||
|
- pattern-regex: "$NAME =~ /(?i).*(api|key|token|secret|pass|auth|session|bearer|access|private).*/"
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
rules:
|
||||||
|
- id: insecure-data-storage
|
||||||
|
severity: WARNING
|
||||||
|
languages: [java]
|
||||||
|
message: "Potential insecure data storage (external storage)."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
owasp-mobile: M2
|
||||||
|
category: security
|
||||||
|
area: storage
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/*.java"
|
||||||
|
pattern-either:
|
||||||
|
- pattern: "$CTX.openFileOutput($NAME, $MODE)"
|
||||||
|
- pattern: "Environment.getExternalStorageDirectory()"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
rules:
|
||||||
|
- id: insecure-deeplink
|
||||||
|
severity: WARNING
|
||||||
|
languages: [xml]
|
||||||
|
message: "Potential insecure deeplink found in intent-filter."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
category: component
|
||||||
|
area: manifest
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/AndroidManifest.xml"
|
||||||
|
pattern: |
|
||||||
|
<intent-filter>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
rules:
|
||||||
|
- id: insecure-logging
|
||||||
|
severity: WARNING
|
||||||
|
languages: [java]
|
||||||
|
message: "Sensitive data logged via Android Log API."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
owasp-mobile: M2
|
||||||
|
category: logging
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/*.java"
|
||||||
|
patterns:
|
||||||
|
- pattern-either:
|
||||||
|
- pattern: "Log.d($TAG, $MSG)"
|
||||||
|
- pattern: "Log.e($TAG, $MSG)"
|
||||||
|
- pattern: "System.out.println($MSG)"
|
||||||
|
- pattern-regex: "$MSG =~ /(?i).*(password|token|secret|api|auth|session).*/"
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
rules:
|
||||||
|
- id: intent-redirection
|
||||||
|
severity: WARNING
|
||||||
|
languages: [java]
|
||||||
|
message: "Potential intent redirection: using getIntent().getExtras() without validation."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
category: intent
|
||||||
|
area: intercomponent
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/*.java"
|
||||||
|
pattern: "$ACT.getIntent().getExtras()"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
rules:
|
||||||
|
- id: sensitive-data-in-shared-preferences
|
||||||
|
severity: WARNING
|
||||||
|
languages: [java]
|
||||||
|
message: "Sensitive data may be stored in SharedPreferences. Please review the key '$KEY'."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
owasp-mobile: M2
|
||||||
|
category: security
|
||||||
|
area: storage
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/*.java"
|
||||||
|
patterns:
|
||||||
|
- pattern: "$EDITOR.putString($KEY, $VAL);"
|
||||||
|
- pattern-regex: "$KEY =~ /(?i).*(username|password|pass|token|auth_token|api_key|secret|sessionid|email).*/"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
rules:
|
||||||
|
- id: sqlite-injection
|
||||||
|
severity: ERROR
|
||||||
|
languages: [java]
|
||||||
|
message: "Possible SQL injection: concatenated input in rawQuery or execSQL."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
owasp-mobile: M7
|
||||||
|
category: injection
|
||||||
|
area: database
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/*.java"
|
||||||
|
patterns:
|
||||||
|
- pattern-either:
|
||||||
|
- pattern: "$DB.rawQuery($QUERY, ...)"
|
||||||
|
- pattern: "$DB.execSQL($QUERY)"
|
||||||
|
- pattern-regex: "$QUERY =~ /.*\".*\".*\\+.*/"
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
rules:
|
||||||
|
- id: vulnerable-activity
|
||||||
|
severity: WARNING
|
||||||
|
languages: [xml]
|
||||||
|
message: "Activity exported without permission."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
category: component
|
||||||
|
area: manifest
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/AndroidManifest.xml"
|
||||||
|
pattern: |
|
||||||
|
<activity android:exported="true"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
rules:
|
||||||
|
- id: vulnerable-content-provider
|
||||||
|
severity: WARNING
|
||||||
|
languages: [xml]
|
||||||
|
message: "ContentProvider exported without permission."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
category: component
|
||||||
|
area: manifest
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/AndroidManifest.xml"
|
||||||
|
pattern: |
|
||||||
|
<provider android:exported="true"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
rules:
|
||||||
|
- id: vulnerable-service
|
||||||
|
severity: WARNING
|
||||||
|
languages: [xml]
|
||||||
|
message: "Service exported without permission."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
category: component
|
||||||
|
area: manifest
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/AndroidManifest.xml"
|
||||||
|
pattern: |
|
||||||
|
<service android:exported="true"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
rules:
|
||||||
|
- id: webview-javascript-enabled
|
||||||
|
severity: ERROR
|
||||||
|
languages: [java]
|
||||||
|
message: "WebView with JavaScript enabled can be dangerous if loading untrusted content."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
owasp-mobile: M7
|
||||||
|
category: webview
|
||||||
|
area: ui
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/*.java"
|
||||||
|
pattern: "$W.getSettings().setJavaScriptEnabled(true)"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
rules:
|
||||||
|
- id: webview-load-arbitrary-url
|
||||||
|
severity: WARNING
|
||||||
|
languages: [java]
|
||||||
|
message: "Loading unvalidated URL in WebView may cause open redirect or XSS."
|
||||||
|
metadata:
|
||||||
|
authors:
|
||||||
|
- Guerric ELOI (FuzzingLabs)
|
||||||
|
owasp-mobile: M7
|
||||||
|
category: webview
|
||||||
|
area: ui
|
||||||
|
verification-level: [L1]
|
||||||
|
paths:
|
||||||
|
include:
|
||||||
|
- "**/*.java"
|
||||||
|
pattern: "$W.loadUrl($URL)"
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
"""
|
||||||
|
Jadx APK Decompilation Module
|
||||||
|
|
||||||
|
Decompiles Android APK files to Java source code using Jadx.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Copyright (c) 2025 FuzzingLabs
|
||||||
|
#
|
||||||
|
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
|
||||||
|
# at the root of this repository for details.
|
||||||
|
#
|
||||||
|
# After the Change Date (four years from publication), this version of the
|
||||||
|
# Licensed Work will be made available under the Apache License, Version 2.0.
|
||||||
|
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Additional attribution and requirements are provided in the NOTICE file.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import shutil
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from toolbox.modules.base import BaseModule, ModuleMetadata, ModuleResult
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from modules.base import BaseModule, ModuleMetadata, ModuleResult
|
||||||
|
except ImportError:
|
||||||
|
from src.toolbox.modules.base import BaseModule, ModuleMetadata, ModuleResult
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class JadxDecompiler(BaseModule):
|
||||||
|
"""Module for decompiling APK files to Java source code using Jadx"""
|
||||||
|
|
||||||
|
def get_metadata(self) -> ModuleMetadata:
|
||||||
|
return ModuleMetadata(
|
||||||
|
name="jadx_decompiler",
|
||||||
|
version="1.5.0",
|
||||||
|
description="Android APK decompilation using Jadx - converts DEX bytecode to Java source",
|
||||||
|
author="FuzzForge Team",
|
||||||
|
category="android",
|
||||||
|
tags=["android", "jadx", "decompilation", "reverse", "apk"],
|
||||||
|
input_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"apk_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to the APK to decompile (absolute or relative to workspace)",
|
||||||
|
},
|
||||||
|
"output_dir": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Directory (relative to workspace) where Jadx output should be written",
|
||||||
|
"default": "jadx_output",
|
||||||
|
},
|
||||||
|
"overwrite": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Overwrite existing output directory if present",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
"threads": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of Jadx decompilation threads",
|
||||||
|
"default": 4,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 32,
|
||||||
|
},
|
||||||
|
"decompiler_args": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Additional arguments passed directly to Jadx",
|
||||||
|
"default": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["apk_path"],
|
||||||
|
},
|
||||||
|
output_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"output_dir": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to decompiled output directory",
|
||||||
|
},
|
||||||
|
"source_dir": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to decompiled Java sources",
|
||||||
|
},
|
||||||
|
"resource_dir": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to extracted resources",
|
||||||
|
},
|
||||||
|
"java_files": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of Java files decompiled",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requires_workspace=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_config(self, config: Dict[str, Any]) -> bool:
|
||||||
|
"""Validate module configuration"""
|
||||||
|
apk_path = config.get("apk_path")
|
||||||
|
if not apk_path:
|
||||||
|
raise ValueError("'apk_path' must be provided for Jadx decompilation")
|
||||||
|
|
||||||
|
threads = config.get("threads", 4)
|
||||||
|
if not isinstance(threads, int) or threads < 1 or threads > 32:
|
||||||
|
raise ValueError("threads must be between 1 and 32")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
|
||||||
|
"""
|
||||||
|
Execute Jadx decompilation on an APK file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration dict with apk_path, output_dir, etc.
|
||||||
|
workspace: Workspace directory path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ModuleResult with decompilation summary and metadata
|
||||||
|
"""
|
||||||
|
self.start_timer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.validate_config(config)
|
||||||
|
self.validate_workspace(workspace)
|
||||||
|
|
||||||
|
workspace = workspace.resolve()
|
||||||
|
|
||||||
|
# Resolve APK path
|
||||||
|
apk_path = Path(config["apk_path"])
|
||||||
|
if not apk_path.is_absolute():
|
||||||
|
apk_path = (workspace / apk_path).resolve()
|
||||||
|
|
||||||
|
if not apk_path.exists():
|
||||||
|
raise ValueError(f"APK not found: {apk_path}")
|
||||||
|
|
||||||
|
if apk_path.is_dir():
|
||||||
|
raise ValueError(f"APK path must be a file, not a directory: {apk_path}")
|
||||||
|
|
||||||
|
logger.info(f"Decompiling APK: {apk_path}")
|
||||||
|
|
||||||
|
# Resolve output directory
|
||||||
|
output_dir = Path(config.get("output_dir", "jadx_output"))
|
||||||
|
if not output_dir.is_absolute():
|
||||||
|
output_dir = (workspace / output_dir).resolve()
|
||||||
|
|
||||||
|
# Handle existing output directory
|
||||||
|
if output_dir.exists():
|
||||||
|
if config.get("overwrite", True):
|
||||||
|
logger.info(f"Removing existing output directory: {output_dir}")
|
||||||
|
shutil.rmtree(output_dir)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Output directory already exists: {output_dir}. Set overwrite=true to replace it."
|
||||||
|
)
|
||||||
|
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Build Jadx command
|
||||||
|
threads = str(config.get("threads", 4))
|
||||||
|
extra_args = config.get("decompiler_args", []) or []
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"jadx",
|
||||||
|
"--threads-count",
|
||||||
|
threads,
|
||||||
|
"--deobf", # Deobfuscate code
|
||||||
|
"--output-dir",
|
||||||
|
str(output_dir),
|
||||||
|
]
|
||||||
|
cmd.extend(extra_args)
|
||||||
|
cmd.append(str(apk_path))
|
||||||
|
|
||||||
|
logger.info(f"Running Jadx: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
# Execute Jadx
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
cwd=str(workspace),
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
stdout_str = stdout.decode(errors="ignore") if stdout else ""
|
||||||
|
stderr_str = stderr.decode(errors="ignore") if stderr else ""
|
||||||
|
|
||||||
|
if stdout_str:
|
||||||
|
logger.debug(f"Jadx stdout: {stdout_str[:200]}...")
|
||||||
|
if stderr_str:
|
||||||
|
logger.debug(f"Jadx stderr: {stderr_str[:200]}...")
|
||||||
|
|
||||||
|
if process.returncode != 0:
|
||||||
|
error_output = stderr_str or stdout_str or "No error output"
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Jadx failed with exit code {process.returncode}: {error_output[:500]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify output structure
|
||||||
|
source_dir = output_dir / "sources"
|
||||||
|
resource_dir = output_dir / "resources"
|
||||||
|
|
||||||
|
if not source_dir.exists():
|
||||||
|
logger.warning(
|
||||||
|
f"Jadx sources directory not found at expected path: {source_dir}"
|
||||||
|
)
|
||||||
|
# Use output_dir as fallback
|
||||||
|
source_dir = output_dir
|
||||||
|
|
||||||
|
# Count decompiled Java files
|
||||||
|
java_files = 0
|
||||||
|
if source_dir.exists():
|
||||||
|
java_files = sum(1 for _ in source_dir.rglob("*.java"))
|
||||||
|
logger.info(f"Decompiled {java_files} Java files")
|
||||||
|
|
||||||
|
# Log sample files for debugging
|
||||||
|
sample_files = []
|
||||||
|
for idx, file_path in enumerate(source_dir.rglob("*.java")):
|
||||||
|
sample_files.append(str(file_path.relative_to(workspace)))
|
||||||
|
if idx >= 4:
|
||||||
|
break
|
||||||
|
if sample_files:
|
||||||
|
logger.debug(f"Sample Java files: {sample_files}")
|
||||||
|
|
||||||
|
# Create summary
|
||||||
|
summary = {
|
||||||
|
"output_dir": str(output_dir),
|
||||||
|
"source_dir": str(source_dir if source_dir.exists() else output_dir),
|
||||||
|
"resource_dir": str(
|
||||||
|
resource_dir if resource_dir.exists() else output_dir
|
||||||
|
),
|
||||||
|
"java_files": java_files,
|
||||||
|
"apk_name": apk_path.name,
|
||||||
|
"apk_size_bytes": apk_path.stat().st_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"apk_path": str(apk_path),
|
||||||
|
"output_dir": str(output_dir),
|
||||||
|
"source_dir": summary["source_dir"],
|
||||||
|
"resource_dir": summary["resource_dir"],
|
||||||
|
"threads": threads,
|
||||||
|
"decompiler": "jadx",
|
||||||
|
"decompiler_version": "1.5.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"✓ Jadx decompilation completed: {java_files} Java files generated"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.create_result(
|
||||||
|
findings=[], # Jadx doesn't generate findings, only decompiles
|
||||||
|
status="success",
|
||||||
|
summary=summary,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"Jadx decompilation failed: {exc}", exc_info=True)
|
||||||
|
return self.create_result(
|
||||||
|
findings=[],
|
||||||
|
status="failed",
|
||||||
|
error=str(exc),
|
||||||
|
metadata={"decompiler": "jadx", "apk_path": config.get("apk_path")},
|
||||||
|
)
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
"""
|
||||||
|
MobSF Scanner Module
|
||||||
|
|
||||||
|
Mobile Security Framework (MobSF) integration for comprehensive Android app security analysis.
|
||||||
|
Performs static analysis on APK files including permissions, manifest analysis, code analysis, and behavior checks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Copyright (c) 2025 FuzzingLabs
|
||||||
|
#
|
||||||
|
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
|
||||||
|
# at the root of this repository for details.
|
||||||
|
#
|
||||||
|
# After the Change Date (four years from publication), this version of the
|
||||||
|
# Licensed Work will be made available under the Apache License, Version 2.0.
|
||||||
|
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Additional attribution and requirements are provided in the NOTICE file.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
try:
|
||||||
|
from toolbox.modules.base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from modules.base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
|
||||||
|
except ImportError:
|
||||||
|
from src.toolbox.modules.base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MobSFScanner(BaseModule):
|
||||||
|
"""Mobile Security Framework (MobSF) scanner module for Android applications"""
|
||||||
|
|
||||||
|
SEVERITY_MAP = {
|
||||||
|
"dangerous": "critical",
|
||||||
|
"high": "high",
|
||||||
|
"warning": "medium",
|
||||||
|
"medium": "medium",
|
||||||
|
"low": "low",
|
||||||
|
"info": "low",
|
||||||
|
"secure": "low",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_metadata(self) -> ModuleMetadata:
|
||||||
|
return ModuleMetadata(
|
||||||
|
name="mobsf_scanner",
|
||||||
|
version="3.9.7",
|
||||||
|
description="Comprehensive Android security analysis using Mobile Security Framework (MobSF)",
|
||||||
|
author="FuzzForge Team",
|
||||||
|
category="android",
|
||||||
|
tags=["mobile", "android", "mobsf", "sast", "scanner", "security"],
|
||||||
|
input_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mobsf_url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "MobSF server URL",
|
||||||
|
"default": "http://localhost:8877",
|
||||||
|
},
|
||||||
|
"file_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to the APK file to scan (absolute or relative to workspace)",
|
||||||
|
},
|
||||||
|
"api_key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "MobSF API key (if not provided, will try MOBSF_API_KEY env var)",
|
||||||
|
"default": None,
|
||||||
|
},
|
||||||
|
"rescan": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Force rescan even if file was previously analyzed",
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["file_path"],
|
||||||
|
},
|
||||||
|
output_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"findings": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Security findings from MobSF analysis"
|
||||||
|
},
|
||||||
|
"scan_hash": {"type": "string"},
|
||||||
|
"total_findings": {"type": "integer"},
|
||||||
|
"severity_counts": {"type": "object"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requires_workspace=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_config(self, config: Dict[str, Any]) -> bool:
|
||||||
|
"""Validate module configuration"""
|
||||||
|
if "mobsf_url" in config and not isinstance(config["mobsf_url"], str):
|
||||||
|
raise ValueError("mobsf_url must be a string")
|
||||||
|
|
||||||
|
file_path = config.get("file_path")
|
||||||
|
if not file_path:
|
||||||
|
raise ValueError("file_path is required for MobSF scanning")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
|
||||||
|
"""
|
||||||
|
Execute MobSF security analysis on an APK file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration dict with file_path, mobsf_url, api_key
|
||||||
|
workspace: Workspace directory path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ModuleResult with security findings from MobSF
|
||||||
|
"""
|
||||||
|
self.start_timer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.validate_config(config)
|
||||||
|
self.validate_workspace(workspace)
|
||||||
|
|
||||||
|
# Get configuration
|
||||||
|
mobsf_url = config.get("mobsf_url", "http://localhost:8877")
|
||||||
|
file_path_str = config["file_path"]
|
||||||
|
rescan = config.get("rescan", False)
|
||||||
|
|
||||||
|
# Get API key from config or environment
|
||||||
|
api_key = config.get("api_key") or os.environ.get("MOBSF_API_KEY", "")
|
||||||
|
if not api_key:
|
||||||
|
logger.warning("No MobSF API key provided. Some functionality may be limited.")
|
||||||
|
|
||||||
|
# Resolve APK file path
|
||||||
|
file_path = Path(file_path_str)
|
||||||
|
if not file_path.is_absolute():
|
||||||
|
file_path = (workspace / file_path).resolve()
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
raise FileNotFoundError(f"APK file not found: {file_path}")
|
||||||
|
|
||||||
|
if not file_path.is_file():
|
||||||
|
raise ValueError(f"APK path must be a file: {file_path}")
|
||||||
|
|
||||||
|
logger.info(f"Starting MobSF scan of APK: {file_path}")
|
||||||
|
|
||||||
|
# Upload and scan APK
|
||||||
|
scan_hash = await self._upload_file(mobsf_url, file_path, api_key)
|
||||||
|
logger.info(f"APK uploaded to MobSF with hash: {scan_hash}")
|
||||||
|
|
||||||
|
# Start scan
|
||||||
|
await self._start_scan(mobsf_url, scan_hash, api_key, rescan=rescan)
|
||||||
|
logger.info(f"MobSF scan completed for hash: {scan_hash}")
|
||||||
|
|
||||||
|
# Get JSON results
|
||||||
|
scan_results = await self._get_json_results(mobsf_url, scan_hash, api_key)
|
||||||
|
|
||||||
|
# Parse results into findings
|
||||||
|
findings = self._parse_scan_results(scan_results, file_path)
|
||||||
|
|
||||||
|
# Create summary
|
||||||
|
summary = self._create_summary(findings, scan_hash)
|
||||||
|
|
||||||
|
logger.info(f"✓ MobSF scan completed: {len(findings)} findings")
|
||||||
|
|
||||||
|
return self.create_result(
|
||||||
|
findings=findings,
|
||||||
|
status="success",
|
||||||
|
summary=summary,
|
||||||
|
metadata={
|
||||||
|
"tool": "mobsf",
|
||||||
|
"tool_version": "3.9.7",
|
||||||
|
"scan_hash": scan_hash,
|
||||||
|
"apk_file": str(file_path),
|
||||||
|
"mobsf_url": mobsf_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"MobSF scanner failed: {exc}", exc_info=True)
|
||||||
|
return self.create_result(
|
||||||
|
findings=[],
|
||||||
|
status="failed",
|
||||||
|
error=str(exc),
|
||||||
|
metadata={"tool": "mobsf", "file_path": config.get("file_path")}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _upload_file(self, mobsf_url: str, file_path: Path, api_key: str) -> str:
|
||||||
|
"""
|
||||||
|
Upload APK file to MobSF server.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Scan hash for the uploaded file
|
||||||
|
"""
|
||||||
|
headers = {'X-Mobsf-Api-Key': api_key} if api_key else {}
|
||||||
|
|
||||||
|
# Create multipart form data
|
||||||
|
filename = file_path.name
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
data = aiohttp.FormData()
|
||||||
|
data.add_field('file',
|
||||||
|
f,
|
||||||
|
filename=filename,
|
||||||
|
content_type='application/vnd.android.package-archive')
|
||||||
|
|
||||||
|
async with session.post(
|
||||||
|
f"{mobsf_url}/api/v1/upload",
|
||||||
|
headers=headers,
|
||||||
|
data=data,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=300)
|
||||||
|
) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
error_text = await response.text()
|
||||||
|
raise Exception(f"Failed to upload file to MobSF: {error_text}")
|
||||||
|
|
||||||
|
result = await response.json()
|
||||||
|
scan_hash = result.get('hash')
|
||||||
|
if not scan_hash:
|
||||||
|
raise Exception(f"MobSF upload failed: {result}")
|
||||||
|
|
||||||
|
return scan_hash
|
||||||
|
|
||||||
|
async def _start_scan(self, mobsf_url: str, scan_hash: str, api_key: str, rescan: bool = False) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Start MobSF scan for uploaded file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Scan result dictionary
|
||||||
|
"""
|
||||||
|
headers = {'X-Mobsf-Api-Key': api_key} if api_key else {}
|
||||||
|
data = {
|
||||||
|
'hash': scan_hash,
|
||||||
|
're_scan': '1' if rescan else '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
f"{mobsf_url}/api/v1/scan",
|
||||||
|
headers=headers,
|
||||||
|
data=data,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=600) # 10 minutes for scan
|
||||||
|
) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
error_text = await response.text()
|
||||||
|
raise Exception(f"MobSF scan failed: {error_text}")
|
||||||
|
|
||||||
|
result = await response.json()
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _get_json_results(self, mobsf_url: str, scan_hash: str, api_key: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retrieve JSON scan results from MobSF.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Scan results dictionary
|
||||||
|
"""
|
||||||
|
headers = {'X-Mobsf-Api-Key': api_key} if api_key else {}
|
||||||
|
data = {'hash': scan_hash}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
f"{mobsf_url}/api/v1/report_json",
|
||||||
|
headers=headers,
|
||||||
|
data=data,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=60)
|
||||||
|
) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
error_text = await response.text()
|
||||||
|
raise Exception(f"Failed to retrieve MobSF results: {error_text}")
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
def _parse_scan_results(self, scan_data: Dict[str, Any], apk_path: Path) -> List[ModuleFinding]:
|
||||||
|
"""Parse MobSF JSON results into standardized findings"""
|
||||||
|
findings = []
|
||||||
|
|
||||||
|
# Parse permissions
|
||||||
|
if 'permissions' in scan_data:
|
||||||
|
for perm_name, perm_attrs in scan_data['permissions'].items():
|
||||||
|
if isinstance(perm_attrs, dict):
|
||||||
|
severity = self.SEVERITY_MAP.get(
|
||||||
|
perm_attrs.get('status', '').lower(), 'low'
|
||||||
|
)
|
||||||
|
|
||||||
|
finding = self.create_finding(
|
||||||
|
title=f"Android Permission: {perm_name}",
|
||||||
|
description=perm_attrs.get('description', 'No description'),
|
||||||
|
severity=severity,
|
||||||
|
category="android-permission",
|
||||||
|
metadata={
|
||||||
|
'permission': perm_name,
|
||||||
|
'status': perm_attrs.get('status'),
|
||||||
|
'info': perm_attrs.get('info'),
|
||||||
|
'tool': 'mobsf',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
findings.append(finding)
|
||||||
|
|
||||||
|
# Parse manifest analysis
|
||||||
|
if 'manifest_analysis' in scan_data:
|
||||||
|
manifest_findings = scan_data['manifest_analysis'].get('manifest_findings', [])
|
||||||
|
for item in manifest_findings:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
severity = self.SEVERITY_MAP.get(item.get('severity', '').lower(), 'medium')
|
||||||
|
|
||||||
|
finding = self.create_finding(
|
||||||
|
title=item.get('title') or item.get('name') or "Manifest Issue",
|
||||||
|
description=item.get('description', 'No description'),
|
||||||
|
severity=severity,
|
||||||
|
category="android-manifest",
|
||||||
|
metadata={
|
||||||
|
'rule': item.get('rule'),
|
||||||
|
'tool': 'mobsf',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
findings.append(finding)
|
||||||
|
|
||||||
|
# Parse code analysis
|
||||||
|
if 'code_analysis' in scan_data:
|
||||||
|
code_findings = scan_data['code_analysis'].get('findings', {})
|
||||||
|
for finding_name, finding_data in code_findings.items():
|
||||||
|
if isinstance(finding_data, dict):
|
||||||
|
metadata_dict = finding_data.get('metadata', {})
|
||||||
|
severity = self.SEVERITY_MAP.get(
|
||||||
|
metadata_dict.get('severity', '').lower(), 'medium'
|
||||||
|
)
|
||||||
|
|
||||||
|
files_list = finding_data.get('files', [])
|
||||||
|
file_path = files_list[0] if files_list else None
|
||||||
|
|
||||||
|
finding = self.create_finding(
|
||||||
|
title=finding_name,
|
||||||
|
description=metadata_dict.get('description', 'No description'),
|
||||||
|
severity=severity,
|
||||||
|
category="android-code-analysis",
|
||||||
|
file_path=file_path,
|
||||||
|
metadata={
|
||||||
|
'cwe': metadata_dict.get('cwe'),
|
||||||
|
'owasp': metadata_dict.get('owasp'),
|
||||||
|
'files': files_list,
|
||||||
|
'tool': 'mobsf',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
findings.append(finding)
|
||||||
|
|
||||||
|
# Parse behavior analysis
|
||||||
|
if 'behaviour' in scan_data:
|
||||||
|
for key, value in scan_data['behaviour'].items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
metadata_dict = value.get('metadata', {})
|
||||||
|
labels = metadata_dict.get('label', [])
|
||||||
|
label = labels[0] if labels else 'Unknown Behavior'
|
||||||
|
|
||||||
|
severity = self.SEVERITY_MAP.get(
|
||||||
|
metadata_dict.get('severity', '').lower(), 'medium'
|
||||||
|
)
|
||||||
|
|
||||||
|
files_list = value.get('files', [])
|
||||||
|
|
||||||
|
finding = self.create_finding(
|
||||||
|
title=f"Behavior: {label}",
|
||||||
|
description=metadata_dict.get('description', 'No description'),
|
||||||
|
severity=severity,
|
||||||
|
category="android-behavior",
|
||||||
|
metadata={
|
||||||
|
'files': files_list,
|
||||||
|
'tool': 'mobsf',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
findings.append(finding)
|
||||||
|
|
||||||
|
logger.debug(f"Parsed {len(findings)} findings from MobSF results")
|
||||||
|
return findings
|
||||||
|
|
||||||
|
def _create_summary(self, findings: List[ModuleFinding], scan_hash: str) -> Dict[str, Any]:
|
||||||
|
"""Create analysis summary"""
|
||||||
|
severity_counter = Counter()
|
||||||
|
category_counter = Counter()
|
||||||
|
|
||||||
|
for finding in findings:
|
||||||
|
severity_counter[finding.severity] += 1
|
||||||
|
category_counter[finding.category] += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"scan_hash": scan_hash,
|
||||||
|
"total_findings": len(findings),
|
||||||
|
"severity_counts": dict(severity_counter),
|
||||||
|
"category_counts": dict(category_counter),
|
||||||
|
}
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
"""
|
||||||
|
OpenGrep Android Static Analysis Module
|
||||||
|
|
||||||
|
Pattern-based static analysis for Android applications using OpenGrep/Semgrep
|
||||||
|
with Android-specific security rules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Copyright (c) 2025 FuzzingLabs
|
||||||
|
#
|
||||||
|
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
|
||||||
|
# at the root of this repository for details.
|
||||||
|
#
|
||||||
|
# After the Change Date (four years from publication), this version of the
|
||||||
|
# Licensed Work will be made available under the Apache License, Version 2.0.
|
||||||
|
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Additional attribution and requirements are provided in the NOTICE file.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
|
||||||
|
try:
|
||||||
|
from toolbox.modules.base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from modules.base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
|
||||||
|
except ImportError:
|
||||||
|
from src.toolbox.modules.base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenGrepAndroid(BaseModule):
|
||||||
|
"""OpenGrep static analysis module specialized for Android security"""
|
||||||
|
|
||||||
|
def get_metadata(self) -> ModuleMetadata:
|
||||||
|
"""Get module metadata"""
|
||||||
|
return ModuleMetadata(
|
||||||
|
name="opengrep_android",
|
||||||
|
version="1.45.0",
|
||||||
|
description="Android-focused static analysis using OpenGrep/Semgrep with custom security rules for Java/Kotlin",
|
||||||
|
author="FuzzForge Team",
|
||||||
|
category="android",
|
||||||
|
tags=["sast", "android", "opengrep", "semgrep", "java", "kotlin", "security"],
|
||||||
|
input_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"config": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["auto", "p/security-audit", "p/owasp-top-ten", "p/cwe-top-25"],
|
||||||
|
"default": "auto",
|
||||||
|
"description": "Rule configuration to use"
|
||||||
|
},
|
||||||
|
"custom_rules_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to a directory containing custom OpenGrep rules (Android-specific rules recommended)",
|
||||||
|
"default": None,
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Specific languages to analyze (defaults to java, kotlin for Android)",
|
||||||
|
"default": ["java", "kotlin"],
|
||||||
|
},
|
||||||
|
"include_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "File patterns to include",
|
||||||
|
"default": [],
|
||||||
|
},
|
||||||
|
"exclude_patterns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "File patterns to exclude",
|
||||||
|
"default": [],
|
||||||
|
},
|
||||||
|
"max_target_bytes": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 1000000,
|
||||||
|
"description": "Maximum file size to analyze (bytes)"
|
||||||
|
},
|
||||||
|
"timeout": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 300,
|
||||||
|
"description": "Analysis timeout in seconds"
|
||||||
|
},
|
||||||
|
"severity": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "enum": ["ERROR", "WARNING", "INFO"]},
|
||||||
|
"default": ["ERROR", "WARNING", "INFO"],
|
||||||
|
"description": "Minimum severity levels to report"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string", "enum": ["HIGH", "MEDIUM", "LOW"]},
|
||||||
|
"default": ["HIGH", "MEDIUM", "LOW"],
|
||||||
|
"description": "Minimum confidence levels to report"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"findings": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Security findings from OpenGrep analysis"
|
||||||
|
},
|
||||||
|
"total_findings": {"type": "integer"},
|
||||||
|
"severity_counts": {"type": "object"},
|
||||||
|
"files_analyzed": {"type": "integer"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requires_workspace=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_config(self, config: Dict[str, Any]) -> bool:
|
||||||
|
"""Validate configuration"""
|
||||||
|
timeout = config.get("timeout", 300)
|
||||||
|
if not isinstance(timeout, int) or timeout < 30 or timeout > 3600:
|
||||||
|
raise ValueError("Timeout must be between 30 and 3600 seconds")
|
||||||
|
|
||||||
|
max_bytes = config.get("max_target_bytes", 1000000)
|
||||||
|
if not isinstance(max_bytes, int) or max_bytes < 1000 or max_bytes > 10000000:
|
||||||
|
raise ValueError("max_target_bytes must be between 1000 and 10000000")
|
||||||
|
|
||||||
|
custom_rules_path = config.get("custom_rules_path")
|
||||||
|
if custom_rules_path:
|
||||||
|
rules_path = Path(custom_rules_path)
|
||||||
|
if not rules_path.exists():
|
||||||
|
logger.warning(f"Custom rules path does not exist: {custom_rules_path}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
|
||||||
|
"""Execute OpenGrep static analysis on Android code"""
|
||||||
|
self.start_timer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Validate inputs
|
||||||
|
self.validate_config(config)
|
||||||
|
self.validate_workspace(workspace)
|
||||||
|
|
||||||
|
logger.info(f"Running OpenGrep Android analysis on {workspace}")
|
||||||
|
|
||||||
|
# Build opengrep command
|
||||||
|
cmd = ["opengrep", "scan", "--json"]
|
||||||
|
|
||||||
|
# Add configuration
|
||||||
|
custom_rules_path = config.get("custom_rules_path")
|
||||||
|
use_custom_rules = False
|
||||||
|
if custom_rules_path and Path(custom_rules_path).exists():
|
||||||
|
cmd.extend(["--config", custom_rules_path])
|
||||||
|
use_custom_rules = True
|
||||||
|
logger.info(f"Using custom Android rules from: {custom_rules_path}")
|
||||||
|
else:
|
||||||
|
config_type = config.get("config", "auto")
|
||||||
|
if config_type == "auto":
|
||||||
|
cmd.extend(["--config", "auto"])
|
||||||
|
else:
|
||||||
|
cmd.extend(["--config", config_type])
|
||||||
|
|
||||||
|
# Add timeout
|
||||||
|
cmd.extend(["--timeout", str(config.get("timeout", 300))])
|
||||||
|
|
||||||
|
# Add max target bytes
|
||||||
|
cmd.extend(["--max-target-bytes", str(config.get("max_target_bytes", 1000000))])
|
||||||
|
|
||||||
|
# Add languages if specified (but NOT when using custom rules)
|
||||||
|
languages = config.get("languages", ["java", "kotlin"])
|
||||||
|
if languages and not use_custom_rules:
|
||||||
|
langs = ",".join(languages)
|
||||||
|
cmd.extend(["--lang", langs])
|
||||||
|
logger.debug(f"Analyzing languages: {langs}")
|
||||||
|
|
||||||
|
# Add include patterns
|
||||||
|
include_patterns = config.get("include_patterns", [])
|
||||||
|
for pattern in include_patterns:
|
||||||
|
cmd.extend(["--include", pattern])
|
||||||
|
|
||||||
|
# Add exclude patterns
|
||||||
|
exclude_patterns = config.get("exclude_patterns", [])
|
||||||
|
for pattern in exclude_patterns:
|
||||||
|
cmd.extend(["--exclude", pattern])
|
||||||
|
|
||||||
|
# Add severity filter if single level requested
|
||||||
|
severity_levels = config.get("severity", ["ERROR", "WARNING", "INFO"])
|
||||||
|
if severity_levels and len(severity_levels) == 1:
|
||||||
|
cmd.extend(["--severity", severity_levels[0]])
|
||||||
|
|
||||||
|
# Disable metrics collection
|
||||||
|
cmd.append("--disable-version-check")
|
||||||
|
cmd.append("--no-git-ignore")
|
||||||
|
|
||||||
|
# Add target directory
|
||||||
|
cmd.append(str(workspace))
|
||||||
|
|
||||||
|
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
# Run OpenGrep
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
cwd=workspace
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
|
||||||
|
# Parse results
|
||||||
|
findings = []
|
||||||
|
if process.returncode in [0, 1]: # 0 = no findings, 1 = findings found
|
||||||
|
findings = self._parse_opengrep_output(stdout.decode(), workspace, config)
|
||||||
|
logger.info(f"OpenGrep found {len(findings)} potential security issues")
|
||||||
|
else:
|
||||||
|
error_msg = stderr.decode()
|
||||||
|
logger.error(f"OpenGrep failed: {error_msg}")
|
||||||
|
return self.create_result(
|
||||||
|
findings=[],
|
||||||
|
status="failed",
|
||||||
|
error=f"OpenGrep execution failed (exit code {process.returncode}): {error_msg[:500]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create summary
|
||||||
|
summary = self._create_summary(findings)
|
||||||
|
|
||||||
|
return self.create_result(
|
||||||
|
findings=findings,
|
||||||
|
status="success",
|
||||||
|
summary=summary,
|
||||||
|
metadata={
|
||||||
|
"tool": "opengrep",
|
||||||
|
"tool_version": "1.45.0",
|
||||||
|
"languages": languages,
|
||||||
|
"custom_rules": bool(custom_rules_path),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"OpenGrep Android module failed: {e}", exc_info=True)
|
||||||
|
return self.create_result(
|
||||||
|
findings=[],
|
||||||
|
status="failed",
|
||||||
|
error=str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_opengrep_output(self, output: str, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]:
|
||||||
|
"""Parse OpenGrep JSON output into findings"""
|
||||||
|
findings = []
|
||||||
|
|
||||||
|
if not output.strip():
|
||||||
|
return findings
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(output)
|
||||||
|
results = data.get("results", [])
|
||||||
|
logger.debug(f"OpenGrep returned {len(results)} raw results")
|
||||||
|
|
||||||
|
# Get filtering criteria
|
||||||
|
allowed_severities = set(config.get("severity", ["ERROR", "WARNING", "INFO"]))
|
||||||
|
allowed_confidences = set(config.get("confidence", ["HIGH", "MEDIUM", "LOW"]))
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
# Extract basic info
|
||||||
|
rule_id = result.get("check_id", "unknown")
|
||||||
|
message = result.get("message", "")
|
||||||
|
extra = result.get("extra", {})
|
||||||
|
severity = extra.get("severity", "INFO").upper()
|
||||||
|
|
||||||
|
# File location info
|
||||||
|
path_info = result.get("path", "")
|
||||||
|
start_line = result.get("start", {}).get("line", 0)
|
||||||
|
end_line = result.get("end", {}).get("line", 0)
|
||||||
|
|
||||||
|
# Code snippet
|
||||||
|
lines = extra.get("lines", "")
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
rule_metadata = extra.get("metadata", {})
|
||||||
|
cwe = rule_metadata.get("cwe", [])
|
||||||
|
owasp = rule_metadata.get("owasp", [])
|
||||||
|
confidence = extra.get("confidence", rule_metadata.get("confidence", "MEDIUM")).upper()
|
||||||
|
|
||||||
|
# Apply severity filter
|
||||||
|
if severity not in allowed_severities:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Apply confidence filter
|
||||||
|
if confidence not in allowed_confidences:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Make file path relative to workspace
|
||||||
|
if path_info:
|
||||||
|
try:
|
||||||
|
rel_path = Path(path_info).relative_to(workspace)
|
||||||
|
path_info = str(rel_path)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Map severity to our standard levels
|
||||||
|
finding_severity = self._map_severity(severity)
|
||||||
|
|
||||||
|
# Create finding
|
||||||
|
finding = self.create_finding(
|
||||||
|
title=f"Android Security: {rule_id}",
|
||||||
|
description=message or f"OpenGrep rule {rule_id} triggered",
|
||||||
|
severity=finding_severity,
|
||||||
|
category=self._get_category(rule_id, extra),
|
||||||
|
file_path=path_info if path_info else None,
|
||||||
|
line_start=start_line if start_line > 0 else None,
|
||||||
|
line_end=end_line if end_line > 0 and end_line != start_line else None,
|
||||||
|
code_snippet=lines.strip() if lines else None,
|
||||||
|
recommendation=self._get_recommendation(rule_id, extra),
|
||||||
|
metadata={
|
||||||
|
"rule_id": rule_id,
|
||||||
|
"opengrep_severity": severity,
|
||||||
|
"confidence": confidence,
|
||||||
|
"cwe": cwe,
|
||||||
|
"owasp": owasp,
|
||||||
|
"fix": extra.get("fix", ""),
|
||||||
|
"impact": extra.get("impact", ""),
|
||||||
|
"likelihood": extra.get("likelihood", ""),
|
||||||
|
"references": extra.get("references", []),
|
||||||
|
"tool": "opengrep",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
findings.append(finding)
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.warning(f"Failed to parse OpenGrep output: {e}. Output snippet: {output[:200]}...")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error processing OpenGrep results: {e}", exc_info=True)
|
||||||
|
|
||||||
|
return findings
|
||||||
|
|
||||||
|
def _map_severity(self, opengrep_severity: str) -> str:
|
||||||
|
"""Map OpenGrep severity to our standard severity levels"""
|
||||||
|
severity_map = {
|
||||||
|
"ERROR": "high",
|
||||||
|
"WARNING": "medium",
|
||||||
|
"INFO": "low"
|
||||||
|
}
|
||||||
|
return severity_map.get(opengrep_severity.upper(), "medium")
|
||||||
|
|
||||||
|
def _get_category(self, rule_id: str, extra: Dict[str, Any]) -> str:
|
||||||
|
"""Determine finding category based on rule and metadata"""
|
||||||
|
rule_metadata = extra.get("metadata", {})
|
||||||
|
cwe_list = rule_metadata.get("cwe", [])
|
||||||
|
owasp_list = rule_metadata.get("owasp", [])
|
||||||
|
|
||||||
|
rule_lower = rule_id.lower()
|
||||||
|
|
||||||
|
# Android-specific categories
|
||||||
|
if "injection" in rule_lower or "sql" in rule_lower:
|
||||||
|
return "injection"
|
||||||
|
elif "intent" in rule_lower:
|
||||||
|
return "android-intent"
|
||||||
|
elif "webview" in rule_lower:
|
||||||
|
return "android-webview"
|
||||||
|
elif "deeplink" in rule_lower:
|
||||||
|
return "android-deeplink"
|
||||||
|
elif "storage" in rule_lower or "sharedpreferences" in rule_lower:
|
||||||
|
return "android-storage"
|
||||||
|
elif "logging" in rule_lower or "log" in rule_lower:
|
||||||
|
return "android-logging"
|
||||||
|
elif "clipboard" in rule_lower:
|
||||||
|
return "android-clipboard"
|
||||||
|
elif "activity" in rule_lower or "service" in rule_lower or "provider" in rule_lower:
|
||||||
|
return "android-component"
|
||||||
|
elif "crypto" in rule_lower or "encrypt" in rule_lower:
|
||||||
|
return "cryptography"
|
||||||
|
elif "hardcode" in rule_lower or "secret" in rule_lower:
|
||||||
|
return "secrets"
|
||||||
|
elif "auth" in rule_lower:
|
||||||
|
return "authentication"
|
||||||
|
elif cwe_list:
|
||||||
|
return f"cwe-{cwe_list[0]}"
|
||||||
|
elif owasp_list:
|
||||||
|
return f"owasp-{owasp_list[0].replace(' ', '-').lower()}"
|
||||||
|
else:
|
||||||
|
return "android-security"
|
||||||
|
|
||||||
|
def _get_recommendation(self, rule_id: str, extra: Dict[str, Any]) -> str:
|
||||||
|
"""Generate recommendation based on rule and metadata"""
|
||||||
|
fix_suggestion = extra.get("fix", "")
|
||||||
|
if fix_suggestion:
|
||||||
|
return fix_suggestion
|
||||||
|
|
||||||
|
rule_lower = rule_id.lower()
|
||||||
|
|
||||||
|
# Android-specific recommendations
|
||||||
|
if "injection" in rule_lower or "sql" in rule_lower:
|
||||||
|
return "Use parameterized queries or Room database with type-safe queries to prevent SQL injection."
|
||||||
|
elif "intent" in rule_lower:
|
||||||
|
return "Validate all incoming Intent data and use explicit Intents when possible to prevent Intent manipulation attacks."
|
||||||
|
elif "webview" in rule_lower and "javascript" in rule_lower:
|
||||||
|
return "Disable JavaScript in WebView if not needed, or implement proper JavaScript interfaces with @JavascriptInterface annotation."
|
||||||
|
elif "deeplink" in rule_lower:
|
||||||
|
return "Validate all deeplink URLs and sanitize user input to prevent deeplink hijacking attacks."
|
||||||
|
elif "storage" in rule_lower or "sharedpreferences" in rule_lower:
|
||||||
|
return "Encrypt sensitive data before storing in SharedPreferences or use EncryptedSharedPreferences for Android API 23+."
|
||||||
|
elif "logging" in rule_lower:
|
||||||
|
return "Remove sensitive data from logs in production builds. Use ProGuard/R8 to strip logging statements."
|
||||||
|
elif "clipboard" in rule_lower:
|
||||||
|
return "Avoid placing sensitive data on the clipboard. If necessary, clear clipboard data when no longer needed."
|
||||||
|
elif "crypto" in rule_lower:
|
||||||
|
return "Use modern cryptographic algorithms (AES-GCM, RSA-OAEP) and Android Keystore for key management."
|
||||||
|
elif "hardcode" in rule_lower or "secret" in rule_lower:
|
||||||
|
return "Remove hardcoded secrets. Use Android Keystore, environment variables, or secure configuration management."
|
||||||
|
else:
|
||||||
|
return "Review this Android security issue and apply appropriate fixes based on Android security best practices."
|
||||||
|
|
||||||
|
def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]:
|
||||||
|
"""Create analysis summary"""
|
||||||
|
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
||||||
|
category_counts = {}
|
||||||
|
rule_counts = {}
|
||||||
|
|
||||||
|
for finding in findings:
|
||||||
|
# Count by severity
|
||||||
|
severity_counts[finding.severity] += 1
|
||||||
|
|
||||||
|
# Count by category
|
||||||
|
category = finding.category
|
||||||
|
category_counts[category] = category_counts.get(category, 0) + 1
|
||||||
|
|
||||||
|
# Count by rule
|
||||||
|
rule_id = finding.metadata.get("rule_id", "unknown")
|
||||||
|
rule_counts[rule_id] = rule_counts.get(rule_id, 0) + 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_findings": len(findings),
|
||||||
|
"severity_counts": severity_counts,
|
||||||
|
"category_counts": category_counts,
|
||||||
|
"top_rules": dict(sorted(rule_counts.items(), key=lambda x: x[1], reverse=True)[:10]),
|
||||||
|
"files_analyzed": len(set(f.file_path for f in findings if f.file_path))
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
Android Static Analysis Workflow
|
||||||
|
|
||||||
|
Comprehensive Android application security testing combining:
|
||||||
|
- Jadx APK decompilation
|
||||||
|
- OpenGrep/Semgrep static analysis with Android-specific rules
|
||||||
|
- MobSF mobile security framework analysis
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Copyright (c) 2025 FuzzingLabs
|
||||||
|
#
|
||||||
|
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
|
||||||
|
# at the root of this repository for details.
|
||||||
|
#
|
||||||
|
# After the Change Date (four years from publication), this version of the
|
||||||
|
# Licensed Work will be made available under the Apache License, Version 2.0.
|
||||||
|
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Additional attribution and requirements are provided in the NOTICE file.
|
||||||
|
|
||||||
|
from .workflow import AndroidStaticAnalysisWorkflow
|
||||||
|
from .activities import (
|
||||||
|
decompile_with_jadx_activity,
|
||||||
|
scan_with_opengrep_activity,
|
||||||
|
scan_with_mobsf_activity,
|
||||||
|
generate_android_sarif_activity,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AndroidStaticAnalysisWorkflow",
|
||||||
|
"decompile_with_jadx_activity",
|
||||||
|
"scan_with_opengrep_activity",
|
||||||
|
"scan_with_mobsf_activity",
|
||||||
|
"generate_android_sarif_activity",
|
||||||
|
]
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
"""
|
||||||
|
Android Static Analysis Workflow Activities
|
||||||
|
|
||||||
|
Activities for the Android security testing workflow:
|
||||||
|
- decompile_with_jadx_activity: Decompile APK using Jadx
|
||||||
|
- scan_with_opengrep_activity: Analyze code with OpenGrep/Semgrep
|
||||||
|
- scan_with_mobsf_activity: Scan APK with MobSF
|
||||||
|
- generate_android_sarif_activity: Generate combined SARIF report
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Copyright (c) 2025 FuzzingLabs
|
||||||
|
#
|
||||||
|
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
|
||||||
|
# at the root of this repository for details.
|
||||||
|
#
|
||||||
|
# After the Change Date (four years from publication), this version of the
|
||||||
|
# Licensed Work will be made available under the Apache License, Version 2.0.
|
||||||
|
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Additional attribution and requirements are provided in the NOTICE file.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from temporalio import activity
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Add toolbox to path for module imports
|
||||||
|
sys.path.insert(0, '/app/toolbox')
|
||||||
|
|
||||||
|
|
||||||
|
@activity.defn(name="decompile_with_jadx")
|
||||||
|
async def decompile_with_jadx_activity(workspace_path: str, config: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Decompile Android APK to Java source code using Jadx.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workspace_path: Path to the workspace directory
|
||||||
|
config: JadxDecompiler configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decompilation results dictionary
|
||||||
|
"""
|
||||||
|
logger.info(f"Activity: decompile_with_jadx (workspace={workspace_path})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from modules.android import JadxDecompiler
|
||||||
|
|
||||||
|
workspace = Path(workspace_path)
|
||||||
|
if not workspace.exists():
|
||||||
|
raise FileNotFoundError(f"Workspace not found: {workspace_path}")
|
||||||
|
|
||||||
|
decompiler = JadxDecompiler()
|
||||||
|
result = await decompiler.execute(config, workspace)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"✓ Jadx decompilation completed: "
|
||||||
|
f"{result.summary.get('java_files', 0)} Java files generated"
|
||||||
|
)
|
||||||
|
return result.dict()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Jadx decompilation failed: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@activity.defn(name="scan_with_opengrep")
|
||||||
|
async def scan_with_opengrep_activity(workspace_path: str, config: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Analyze Android code for security issues using OpenGrep/Semgrep.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workspace_path: Path to the workspace directory
|
||||||
|
config: OpenGrepAndroid configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Analysis results dictionary
|
||||||
|
"""
|
||||||
|
logger.info(f"Activity: scan_with_opengrep (workspace={workspace_path})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from modules.android import OpenGrepAndroid
|
||||||
|
|
||||||
|
workspace = Path(workspace_path)
|
||||||
|
if not workspace.exists():
|
||||||
|
raise FileNotFoundError(f"Workspace not found: {workspace_path}")
|
||||||
|
|
||||||
|
analyzer = OpenGrepAndroid()
|
||||||
|
result = await analyzer.execute(config, workspace)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"✓ OpenGrep analysis completed: "
|
||||||
|
f"{result.summary.get('total_findings', 0)} security issues found"
|
||||||
|
)
|
||||||
|
return result.dict()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"OpenGrep analysis failed: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@activity.defn(name="scan_with_mobsf")
|
||||||
|
async def scan_with_mobsf_activity(workspace_path: str, config: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Analyze Android APK for security issues using MobSF.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
workspace_path: Path to the workspace directory
|
||||||
|
config: MobSFScanner configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Scan results dictionary (or skipped status if MobSF unavailable)
|
||||||
|
"""
|
||||||
|
logger.info(f"Activity: scan_with_mobsf (workspace={workspace_path})")
|
||||||
|
|
||||||
|
# Check if MobSF is installed (graceful degradation for ARM64 platform)
|
||||||
|
mobsf_path = Path("/app/mobsf")
|
||||||
|
if not mobsf_path.exists():
|
||||||
|
logger.warning("MobSF not installed on this platform (ARM64/Rosetta limitation)")
|
||||||
|
return {
|
||||||
|
"status": "skipped",
|
||||||
|
"findings": [],
|
||||||
|
"summary": {
|
||||||
|
"total_findings": 0,
|
||||||
|
"skip_reason": "MobSF unavailable on ARM64 platform (Rosetta 2 incompatibility)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
from modules.android import MobSFScanner
|
||||||
|
|
||||||
|
workspace = Path(workspace_path)
|
||||||
|
if not workspace.exists():
|
||||||
|
raise FileNotFoundError(f"Workspace not found: {workspace_path}")
|
||||||
|
|
||||||
|
scanner = MobSFScanner()
|
||||||
|
result = await scanner.execute(config, workspace)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"✓ MobSF scan completed: "
|
||||||
|
f"{result.summary.get('total_findings', 0)} findings"
|
||||||
|
)
|
||||||
|
return result.dict()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"MobSF scan failed: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@activity.defn(name="generate_android_sarif")
|
||||||
|
async def generate_android_sarif_activity(
|
||||||
|
jadx_result: dict,
|
||||||
|
opengrep_result: dict,
|
||||||
|
mobsf_result: dict,
|
||||||
|
config: dict,
|
||||||
|
workspace_path: str
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Generate combined SARIF report from all Android security findings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
jadx_result: Jadx decompilation results
|
||||||
|
opengrep_result: OpenGrep analysis results
|
||||||
|
mobsf_result: MobSF scan results (may be None if disabled)
|
||||||
|
config: Reporter configuration
|
||||||
|
workspace_path: Workspace path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SARIF report dictionary
|
||||||
|
"""
|
||||||
|
logger.info("Activity: generate_android_sarif")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from modules.reporter import SARIFReporter
|
||||||
|
|
||||||
|
workspace = Path(workspace_path)
|
||||||
|
|
||||||
|
# Collect all findings
|
||||||
|
all_findings = []
|
||||||
|
all_findings.extend(opengrep_result.get("findings", []))
|
||||||
|
|
||||||
|
if mobsf_result:
|
||||||
|
all_findings.extend(mobsf_result.get("findings", []))
|
||||||
|
|
||||||
|
# Prepare reporter config
|
||||||
|
reporter_config = {
|
||||||
|
**(config or {}),
|
||||||
|
"findings": all_findings,
|
||||||
|
"tool_name": "FuzzForge Android Static Analysis",
|
||||||
|
"tool_version": "1.0.0",
|
||||||
|
"metadata": {
|
||||||
|
"jadx_version": "1.5.0",
|
||||||
|
"opengrep_version": "1.45.0",
|
||||||
|
"mobsf_version": "3.9.7",
|
||||||
|
"java_files_decompiled": jadx_result.get("summary", {}).get("java_files", 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reporter = SARIFReporter()
|
||||||
|
result = await reporter.execute(reporter_config, workspace)
|
||||||
|
|
||||||
|
sarif_report = result.dict().get("sarif", {})
|
||||||
|
|
||||||
|
logger.info(f"✓ SARIF report generated with {len(all_findings)} findings")
|
||||||
|
|
||||||
|
return sarif_report
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SARIF report generation failed: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
name: android_static_analysis
|
||||||
|
version: "1.0.0"
|
||||||
|
vertical: android
|
||||||
|
description: "Comprehensive Android application security testing using Jadx decompilation, OpenGrep static analysis, and MobSF mobile security framework"
|
||||||
|
author: "FuzzForge Team"
|
||||||
|
tags:
|
||||||
|
- "android"
|
||||||
|
- "mobile"
|
||||||
|
- "static-analysis"
|
||||||
|
- "security"
|
||||||
|
- "opengrep"
|
||||||
|
- "semgrep"
|
||||||
|
- "mobsf"
|
||||||
|
- "jadx"
|
||||||
|
- "apk"
|
||||||
|
- "sarif"
|
||||||
|
|
||||||
|
# Workspace isolation mode
|
||||||
|
# Using "shared" mode for read-only APK analysis (no file modifications except decompilation output)
|
||||||
|
workspace_isolation: "shared"
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
apk_path:
|
||||||
|
type: string
|
||||||
|
description: "Path to the APK file to analyze (relative to uploaded target or absolute within workspace)"
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
decompile_apk:
|
||||||
|
type: boolean
|
||||||
|
description: "Whether to decompile APK with Jadx before OpenGrep analysis"
|
||||||
|
default: true
|
||||||
|
|
||||||
|
jadx_config:
|
||||||
|
type: object
|
||||||
|
description: "Jadx decompiler configuration"
|
||||||
|
properties:
|
||||||
|
output_dir:
|
||||||
|
type: string
|
||||||
|
description: "Output directory for decompiled sources"
|
||||||
|
default: "jadx_output"
|
||||||
|
overwrite:
|
||||||
|
type: boolean
|
||||||
|
description: "Overwrite existing decompilation output"
|
||||||
|
default: true
|
||||||
|
threads:
|
||||||
|
type: integer
|
||||||
|
description: "Number of decompilation threads"
|
||||||
|
default: 4
|
||||||
|
minimum: 1
|
||||||
|
maximum: 32
|
||||||
|
decompiler_args:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: "Additional Jadx arguments"
|
||||||
|
default: []
|
||||||
|
|
||||||
|
opengrep_config:
|
||||||
|
type: object
|
||||||
|
description: "OpenGrep/Semgrep static analysis configuration"
|
||||||
|
properties:
|
||||||
|
config:
|
||||||
|
type: string
|
||||||
|
enum: ["auto", "p/security-audit", "p/owasp-top-ten", "p/cwe-top-25"]
|
||||||
|
description: "Preset OpenGrep ruleset (ignored if custom_rules_path is set)"
|
||||||
|
default: "auto"
|
||||||
|
custom_rules_path:
|
||||||
|
type: string
|
||||||
|
description: "Path to custom OpenGrep rules directory (use Android-specific rules for best results)"
|
||||||
|
default: "/app/toolbox/modules/android/custom_rules"
|
||||||
|
languages:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: "Programming languages to analyze (defaults to java, kotlin for Android)"
|
||||||
|
default: ["java", "kotlin"]
|
||||||
|
include_patterns:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: "File patterns to include in scan"
|
||||||
|
default: []
|
||||||
|
exclude_patterns:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: "File patterns to exclude from scan"
|
||||||
|
default: []
|
||||||
|
max_target_bytes:
|
||||||
|
type: integer
|
||||||
|
description: "Maximum file size to analyze (bytes)"
|
||||||
|
default: 1000000
|
||||||
|
timeout:
|
||||||
|
type: integer
|
||||||
|
description: "Analysis timeout in seconds"
|
||||||
|
default: 300
|
||||||
|
severity:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum: ["ERROR", "WARNING", "INFO"]
|
||||||
|
description: "Severity levels to include in results"
|
||||||
|
default: ["ERROR", "WARNING", "INFO"]
|
||||||
|
confidence:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum: ["HIGH", "MEDIUM", "LOW"]
|
||||||
|
description: "Confidence levels to include in results"
|
||||||
|
default: ["HIGH", "MEDIUM", "LOW"]
|
||||||
|
|
||||||
|
mobsf_config:
|
||||||
|
type: object
|
||||||
|
description: "MobSF scanner configuration"
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: "Enable MobSF analysis (requires APK file)"
|
||||||
|
default: true
|
||||||
|
mobsf_url:
|
||||||
|
type: string
|
||||||
|
description: "MobSF server URL"
|
||||||
|
default: "http://localhost:8877"
|
||||||
|
api_key:
|
||||||
|
type: string
|
||||||
|
description: "MobSF API key (if not provided, uses MOBSF_API_KEY env var)"
|
||||||
|
default: null
|
||||||
|
rescan:
|
||||||
|
type: boolean
|
||||||
|
description: "Force rescan even if APK was previously analyzed"
|
||||||
|
default: false
|
||||||
|
|
||||||
|
reporter_config:
|
||||||
|
type: object
|
||||||
|
description: "SARIF reporter configuration"
|
||||||
|
properties:
|
||||||
|
include_code_flows:
|
||||||
|
type: boolean
|
||||||
|
description: "Include code flow information in SARIF output"
|
||||||
|
default: false
|
||||||
|
logical_id:
|
||||||
|
type: string
|
||||||
|
description: "Custom identifier for the SARIF report"
|
||||||
|
default: null
|
||||||
|
|
||||||
|
output_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
sarif:
|
||||||
|
type: object
|
||||||
|
description: "SARIF-formatted findings from all Android security tools"
|
||||||
|
summary:
|
||||||
|
type: object
|
||||||
|
description: "Android security analysis summary"
|
||||||
|
properties:
|
||||||
|
total_findings:
|
||||||
|
type: integer
|
||||||
|
decompiled_java_files:
|
||||||
|
type: integer
|
||||||
|
description: "Number of Java files decompiled by Jadx"
|
||||||
|
opengrep_findings:
|
||||||
|
type: integer
|
||||||
|
description: "Findings from OpenGrep/Semgrep analysis"
|
||||||
|
mobsf_findings:
|
||||||
|
type: integer
|
||||||
|
description: "Findings from MobSF analysis"
|
||||||
|
severity_distribution:
|
||||||
|
type: object
|
||||||
|
category_distribution:
|
||||||
|
type: object
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
"""
|
||||||
|
Android Static Analysis Workflow - Temporal Version
|
||||||
|
|
||||||
|
Comprehensive security testing for Android applications using Jadx, OpenGrep, and MobSF.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Copyright (c) 2025 FuzzingLabs
|
||||||
|
#
|
||||||
|
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
|
||||||
|
# at the root of this repository for details.
|
||||||
|
#
|
||||||
|
# After the Change Date (four years from publication), this version of the
|
||||||
|
# Licensed Work will be made available under the Apache License, Version 2.0.
|
||||||
|
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Additional attribution and requirements are provided in the NOTICE file.
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from temporalio import workflow
|
||||||
|
from temporalio.common import RetryPolicy
|
||||||
|
|
||||||
|
# Import activity interfaces (will be executed by worker)
|
||||||
|
with workflow.unsafe.imports_passed_through():
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@workflow.defn
|
||||||
|
class AndroidStaticAnalysisWorkflow:
|
||||||
|
"""
|
||||||
|
Android Static Application Security Testing workflow.
|
||||||
|
|
||||||
|
This workflow:
|
||||||
|
1. Downloads target (APK) from MinIO
|
||||||
|
2. (Optional) Decompiles APK using Jadx
|
||||||
|
3. Runs OpenGrep/Semgrep static analysis on decompiled code
|
||||||
|
4. (Optional) Runs MobSF comprehensive security scan
|
||||||
|
5. Generates a SARIF report with all findings
|
||||||
|
6. Uploads results to MinIO
|
||||||
|
7. Cleans up cache
|
||||||
|
"""
|
||||||
|
|
||||||
|
@workflow.run
|
||||||
|
async def run(
|
||||||
|
self,
|
||||||
|
target_id: str,
|
||||||
|
apk_path: Optional[str] = None,
|
||||||
|
decompile_apk: bool = True,
|
||||||
|
jadx_config: Optional[Dict[str, Any]] = None,
|
||||||
|
opengrep_config: Optional[Dict[str, Any]] = None,
|
||||||
|
mobsf_config: Optional[Dict[str, Any]] = None,
|
||||||
|
reporter_config: Optional[Dict[str, Any]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Main workflow execution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_id: UUID of the uploaded target (APK) in MinIO
|
||||||
|
apk_path: Path to APK file within target (if target is not a single APK)
|
||||||
|
decompile_apk: Whether to decompile APK with Jadx before OpenGrep
|
||||||
|
jadx_config: Configuration for Jadx decompiler
|
||||||
|
opengrep_config: Configuration for OpenGrep analyzer
|
||||||
|
mobsf_config: Configuration for MobSF scanner
|
||||||
|
reporter_config: Configuration for SARIF reporter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing SARIF report and summary
|
||||||
|
"""
|
||||||
|
workflow_id = workflow.info().workflow_id
|
||||||
|
|
||||||
|
workflow.logger.info(
|
||||||
|
f"Starting AndroidStaticAnalysisWorkflow "
|
||||||
|
f"(workflow_id={workflow_id}, target_id={target_id})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Default configurations
|
||||||
|
if not jadx_config:
|
||||||
|
jadx_config = {
|
||||||
|
"output_dir": "jadx_output",
|
||||||
|
"overwrite": True,
|
||||||
|
"threads": 4,
|
||||||
|
"decompiler_args": []
|
||||||
|
}
|
||||||
|
|
||||||
|
if not opengrep_config:
|
||||||
|
opengrep_config = {
|
||||||
|
"config": "auto",
|
||||||
|
"custom_rules_path": "/app/toolbox/modules/android/custom_rules",
|
||||||
|
"languages": ["java", "kotlin"],
|
||||||
|
"severity": ["ERROR", "WARNING", "INFO"],
|
||||||
|
"confidence": ["HIGH", "MEDIUM", "LOW"],
|
||||||
|
"timeout": 300,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not mobsf_config:
|
||||||
|
mobsf_config = {
|
||||||
|
"enabled": True,
|
||||||
|
"mobsf_url": "http://localhost:8877",
|
||||||
|
"api_key": None,
|
||||||
|
"rescan": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not reporter_config:
|
||||||
|
reporter_config = {
|
||||||
|
"include_code_flows": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Activity retry policy
|
||||||
|
retry_policy = RetryPolicy(
|
||||||
|
initial_interval=timedelta(seconds=1),
|
||||||
|
maximum_interval=timedelta(seconds=60),
|
||||||
|
maximum_attempts=3,
|
||||||
|
backoff_coefficient=2.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Phase 0: Download target from MinIO
|
||||||
|
workflow.logger.info(f"Phase 0: Downloading target from MinIO (target_id={target_id})")
|
||||||
|
workspace_path = await workflow.execute_activity(
|
||||||
|
"get_target",
|
||||||
|
args=[target_id, workflow.info().workflow_id, "shared"],
|
||||||
|
start_to_close_timeout=timedelta(minutes=10),
|
||||||
|
retry_policy=retry_policy,
|
||||||
|
)
|
||||||
|
workflow.logger.info(f"✓ Target downloaded to: {workspace_path}")
|
||||||
|
|
||||||
|
# Handle case where workspace_path is a file (single APK upload)
|
||||||
|
# vs. a directory containing files
|
||||||
|
workspace_path_obj = Path(workspace_path)
|
||||||
|
|
||||||
|
# Determine actual workspace directory and APK path
|
||||||
|
if apk_path:
|
||||||
|
# User explicitly provided apk_path
|
||||||
|
actual_apk_path = apk_path
|
||||||
|
# workspace_path could be either a file or directory
|
||||||
|
# If it's a file and apk_path matches the filename, use parent as workspace
|
||||||
|
if workspace_path_obj.name == apk_path:
|
||||||
|
workspace_path = str(workspace_path_obj.parent)
|
||||||
|
workflow.logger.info(f"Adjusted workspace to parent directory: {workspace_path}")
|
||||||
|
else:
|
||||||
|
# No apk_path provided - check if workspace_path is an APK file
|
||||||
|
if workspace_path_obj.suffix.lower() == '.apk' or workspace_path_obj.name.endswith('.apk'):
|
||||||
|
# workspace_path is the APK file itself
|
||||||
|
actual_apk_path = workspace_path_obj.name
|
||||||
|
workspace_path = str(workspace_path_obj.parent)
|
||||||
|
workflow.logger.info(f"Detected single APK file: {actual_apk_path}, workspace: {workspace_path}")
|
||||||
|
else:
|
||||||
|
# workspace_path is a directory, need to find APK within it
|
||||||
|
actual_apk_path = None
|
||||||
|
workflow.logger.info("Workspace is a directory, APK detection will be handled by modules")
|
||||||
|
|
||||||
|
# Phase 1: Jadx decompilation (if enabled and APK provided)
|
||||||
|
jadx_result = None
|
||||||
|
analysis_workspace = workspace_path
|
||||||
|
|
||||||
|
if decompile_apk and actual_apk_path:
|
||||||
|
workflow.logger.info(f"Phase 1: Decompiling APK with Jadx (apk={actual_apk_path})")
|
||||||
|
|
||||||
|
jadx_activity_config = {
|
||||||
|
**jadx_config,
|
||||||
|
"apk_path": actual_apk_path
|
||||||
|
}
|
||||||
|
|
||||||
|
jadx_result = await workflow.execute_activity(
|
||||||
|
"decompile_with_jadx",
|
||||||
|
args=[workspace_path, jadx_activity_config],
|
||||||
|
start_to_close_timeout=timedelta(minutes=15),
|
||||||
|
retry_policy=retry_policy,
|
||||||
|
)
|
||||||
|
|
||||||
|
if jadx_result.get("status") == "success":
|
||||||
|
# Use decompiled sources as workspace for OpenGrep
|
||||||
|
source_dir = jadx_result.get("summary", {}).get("source_dir")
|
||||||
|
if source_dir:
|
||||||
|
analysis_workspace = source_dir
|
||||||
|
workflow.logger.info(
|
||||||
|
f"✓ Jadx decompiled {jadx_result.get('summary', {}).get('java_files', 0)} Java files"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
workflow.logger.warning(f"Jadx decompilation failed: {jadx_result.get('error')}")
|
||||||
|
else:
|
||||||
|
workflow.logger.info("Phase 1: Jadx decompilation skipped")
|
||||||
|
|
||||||
|
# Phase 2: OpenGrep static analysis
|
||||||
|
workflow.logger.info(f"Phase 2: OpenGrep analysis on {analysis_workspace}")
|
||||||
|
|
||||||
|
opengrep_result = await workflow.execute_activity(
|
||||||
|
"scan_with_opengrep",
|
||||||
|
args=[analysis_workspace, opengrep_config],
|
||||||
|
start_to_close_timeout=timedelta(minutes=20),
|
||||||
|
retry_policy=retry_policy,
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow.logger.info(
|
||||||
|
f"✓ OpenGrep completed: {opengrep_result.get('summary', {}).get('total_findings', 0)} findings"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Phase 3: MobSF analysis (if enabled and APK provided)
|
||||||
|
mobsf_result = None
|
||||||
|
|
||||||
|
if mobsf_config.get("enabled", True) and actual_apk_path:
|
||||||
|
workflow.logger.info(f"Phase 3: MobSF scan on APK: {actual_apk_path}")
|
||||||
|
|
||||||
|
mobsf_activity_config = {
|
||||||
|
**mobsf_config,
|
||||||
|
"file_path": actual_apk_path
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
mobsf_result = await workflow.execute_activity(
|
||||||
|
"scan_with_mobsf",
|
||||||
|
args=[workspace_path, mobsf_activity_config],
|
||||||
|
start_to_close_timeout=timedelta(minutes=30),
|
||||||
|
retry_policy=RetryPolicy(
|
||||||
|
maximum_attempts=2 # MobSF can be flaky, limit retries
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle skipped or completed status
|
||||||
|
if mobsf_result.get("status") == "skipped":
|
||||||
|
workflow.logger.warning(
|
||||||
|
f"⚠️ MobSF skipped: {mobsf_result.get('summary', {}).get('skip_reason', 'Unknown reason')}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
workflow.logger.info(
|
||||||
|
f"✓ MobSF completed: {mobsf_result.get('summary', {}).get('total_findings', 0)} findings"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
workflow.logger.warning(f"MobSF scan failed (continuing without it): {e}")
|
||||||
|
mobsf_result = None
|
||||||
|
else:
|
||||||
|
workflow.logger.info("Phase 3: MobSF scan skipped (disabled or no APK)")
|
||||||
|
|
||||||
|
# Phase 4: Generate SARIF report
|
||||||
|
workflow.logger.info("Phase 4: Generating SARIF report")
|
||||||
|
|
||||||
|
sarif_report = await workflow.execute_activity(
|
||||||
|
"generate_android_sarif",
|
||||||
|
args=[jadx_result or {}, opengrep_result, mobsf_result, reporter_config, workspace_path],
|
||||||
|
start_to_close_timeout=timedelta(minutes=5),
|
||||||
|
retry_policy=retry_policy,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Phase 5: Upload results to MinIO
|
||||||
|
workflow.logger.info("Phase 5: Uploading results to MinIO")
|
||||||
|
|
||||||
|
result_url = await workflow.execute_activity(
|
||||||
|
"upload_results",
|
||||||
|
args=[workflow.info().workflow_id, sarif_report, "sarif"],
|
||||||
|
start_to_close_timeout=timedelta(minutes=10),
|
||||||
|
retry_policy=retry_policy,
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow.logger.info(f"✓ Results uploaded: {result_url}")
|
||||||
|
|
||||||
|
# Phase 6: Cleanup cache
|
||||||
|
workflow.logger.info("Phase 6: Cleaning up cache")
|
||||||
|
|
||||||
|
await workflow.execute_activity(
|
||||||
|
"cleanup_cache",
|
||||||
|
args=[workspace_path, "shared"],
|
||||||
|
start_to_close_timeout=timedelta(minutes=5),
|
||||||
|
retry_policy=RetryPolicy(maximum_attempts=1), # Don't retry cleanup
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate summary
|
||||||
|
total_findings = len(sarif_report.get("runs", [{}])[0].get("results", []))
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"workflow": "android_static_analysis",
|
||||||
|
"target_id": target_id,
|
||||||
|
"total_findings": total_findings,
|
||||||
|
"decompiled_java_files": (jadx_result or {}).get("summary", {}).get("java_files", 0) if jadx_result else 0,
|
||||||
|
"opengrep_findings": opengrep_result.get("summary", {}).get("total_findings", 0),
|
||||||
|
"mobsf_findings": mobsf_result.get("summary", {}).get("total_findings", 0) if mobsf_result else 0,
|
||||||
|
"result_url": result_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow.logger.info(
|
||||||
|
f"✅ AndroidStaticAnalysisWorkflow completed successfully: {total_findings} findings"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"sarif": sarif_report,
|
||||||
|
"summary": summary,
|
||||||
|
}
|
||||||
@@ -15,11 +15,15 @@ Manages on-demand startup and shutdown of Temporal workers using Docker Compose.
|
|||||||
# Additional attribution and requirements are provided in the NOTICE file.
|
# Additional attribution and requirements are provided in the NOTICE file.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import yaml
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -57,27 +61,181 @@ class WorkerManager:
|
|||||||
|
|
||||||
def _find_compose_file(self) -> Path:
|
def _find_compose_file(self) -> Path:
|
||||||
"""
|
"""
|
||||||
Auto-detect docker-compose.yml location.
|
Auto-detect docker-compose.yml location using multiple strategies.
|
||||||
|
|
||||||
Searches upward from current directory to find the compose file.
|
Strategies (in order):
|
||||||
|
1. Query backend API for host path
|
||||||
|
2. Search upward for .fuzzforge marker directory
|
||||||
|
3. Use FUZZFORGE_ROOT environment variable
|
||||||
|
4. Fallback to current directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to docker-compose.yml
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If docker-compose.yml cannot be located
|
||||||
"""
|
"""
|
||||||
current = Path.cwd()
|
# Strategy 1: Ask backend for location
|
||||||
|
try:
|
||||||
|
backend_url = os.getenv("FUZZFORGE_API_URL", "http://localhost:8000")
|
||||||
|
response = requests.get(f"{backend_url}/system/info", timeout=2)
|
||||||
|
if response.ok:
|
||||||
|
info = response.json()
|
||||||
|
if compose_path_str := info.get("docker_compose_path"):
|
||||||
|
compose_path = Path(compose_path_str)
|
||||||
|
if compose_path.exists():
|
||||||
|
logger.debug(f"Found docker-compose.yml via backend API: {compose_path}")
|
||||||
|
return compose_path
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Backend API not reachable for path lookup: {e}")
|
||||||
|
|
||||||
# Try current directory and parents
|
# Strategy 2: Search upward for .fuzzforge marker directory
|
||||||
|
current = Path.cwd()
|
||||||
for parent in [current] + list(current.parents):
|
for parent in [current] + list(current.parents):
|
||||||
compose_path = parent / "docker-compose.yml"
|
if (parent / ".fuzzforge").exists():
|
||||||
|
compose_path = parent / "docker-compose.yml"
|
||||||
|
if compose_path.exists():
|
||||||
|
logger.debug(f"Found docker-compose.yml via .fuzzforge marker: {compose_path}")
|
||||||
|
return compose_path
|
||||||
|
|
||||||
|
# Strategy 3: Environment variable
|
||||||
|
if fuzzforge_root := os.getenv("FUZZFORGE_ROOT"):
|
||||||
|
compose_path = Path(fuzzforge_root) / "docker-compose.yml"
|
||||||
if compose_path.exists():
|
if compose_path.exists():
|
||||||
|
logger.debug(f"Found docker-compose.yml via FUZZFORGE_ROOT: {compose_path}")
|
||||||
return compose_path
|
return compose_path
|
||||||
|
|
||||||
# Fallback to default location
|
# Strategy 4: Fallback to current directory
|
||||||
return Path("docker-compose.yml")
|
compose_path = Path("docker-compose.yml")
|
||||||
|
if compose_path.exists():
|
||||||
|
return compose_path
|
||||||
|
|
||||||
def _run_docker_compose(self, *args: str) -> subprocess.CompletedProcess:
|
raise FileNotFoundError(
|
||||||
|
"Cannot find docker-compose.yml. Ensure backend is running, "
|
||||||
|
"run from FuzzForge directory, or set FUZZFORGE_ROOT environment variable."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_workers_dir(self) -> Path:
|
||||||
"""
|
"""
|
||||||
Run docker-compose command.
|
Get the workers directory path.
|
||||||
|
|
||||||
|
Uses same strategy as _find_compose_file():
|
||||||
|
1. Query backend API
|
||||||
|
2. Derive from compose_file location
|
||||||
|
3. Use FUZZFORGE_ROOT
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to workers directory
|
||||||
|
"""
|
||||||
|
# Strategy 1: Ask backend
|
||||||
|
try:
|
||||||
|
backend_url = os.getenv("FUZZFORGE_API_URL", "http://localhost:8000")
|
||||||
|
response = requests.get(f"{backend_url}/system/info", timeout=2)
|
||||||
|
if response.ok:
|
||||||
|
info = response.json()
|
||||||
|
if workers_dir_str := info.get("workers_dir"):
|
||||||
|
workers_dir = Path(workers_dir_str)
|
||||||
|
if workers_dir.exists():
|
||||||
|
return workers_dir
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Strategy 2: Derive from compose file location
|
||||||
|
if self.compose_file.exists():
|
||||||
|
workers_dir = self.compose_file.parent / "workers"
|
||||||
|
if workers_dir.exists():
|
||||||
|
return workers_dir
|
||||||
|
|
||||||
|
# Strategy 3: Use environment variable
|
||||||
|
if fuzzforge_root := os.getenv("FUZZFORGE_ROOT"):
|
||||||
|
workers_dir = Path(fuzzforge_root) / "workers"
|
||||||
|
if workers_dir.exists():
|
||||||
|
return workers_dir
|
||||||
|
|
||||||
|
# Fallback
|
||||||
|
return Path("workers")
|
||||||
|
|
||||||
|
def _detect_platform(self) -> str:
|
||||||
|
"""
|
||||||
|
Detect the current platform.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Platform string: "linux/amd64" or "linux/arm64"
|
||||||
|
"""
|
||||||
|
machine = platform.machine().lower()
|
||||||
|
if machine in ["x86_64", "amd64"]:
|
||||||
|
return "linux/amd64"
|
||||||
|
elif machine in ["arm64", "aarch64"]:
|
||||||
|
return "linux/arm64"
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
def _read_worker_metadata(self, vertical: str) -> dict:
|
||||||
|
"""
|
||||||
|
Read worker metadata.yaml for a vertical.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vertical: Worker vertical name (e.g., "android", "python")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing metadata, or empty dict if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
workers_dir = self._get_workers_dir()
|
||||||
|
metadata_file = workers_dir / vertical / "metadata.yaml"
|
||||||
|
|
||||||
|
if not metadata_file.exists():
|
||||||
|
logger.debug(f"No metadata.yaml found for {vertical}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
with open(metadata_file, 'r') as f:
|
||||||
|
return yaml.safe_load(f) or {}
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Failed to read metadata for {vertical}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _select_dockerfile(self, vertical: str) -> str:
|
||||||
|
"""
|
||||||
|
Select the appropriate Dockerfile for the current platform.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vertical: Worker vertical name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dockerfile name (e.g., "Dockerfile.amd64", "Dockerfile.arm64")
|
||||||
|
"""
|
||||||
|
detected_platform = self._detect_platform()
|
||||||
|
metadata = self._read_worker_metadata(vertical)
|
||||||
|
|
||||||
|
if not metadata:
|
||||||
|
# No metadata: use default Dockerfile
|
||||||
|
logger.debug(f"No metadata for {vertical}, using Dockerfile")
|
||||||
|
return "Dockerfile"
|
||||||
|
|
||||||
|
platforms = metadata.get("platforms", {})
|
||||||
|
|
||||||
|
# Try detected platform first
|
||||||
|
if detected_platform in platforms:
|
||||||
|
dockerfile = platforms[detected_platform].get("dockerfile", "Dockerfile")
|
||||||
|
logger.debug(f"Selected {dockerfile} for {vertical} on {detected_platform}")
|
||||||
|
return dockerfile
|
||||||
|
|
||||||
|
# Fallback to default platform
|
||||||
|
default_platform = metadata.get("default_platform", "linux/amd64")
|
||||||
|
if default_platform in platforms:
|
||||||
|
dockerfile = platforms[default_platform].get("dockerfile", "Dockerfile.amd64")
|
||||||
|
logger.debug(f"Using default platform {default_platform}: {dockerfile}")
|
||||||
|
return dockerfile
|
||||||
|
|
||||||
|
# Last resort
|
||||||
|
return "Dockerfile"
|
||||||
|
|
||||||
|
def _run_docker_compose(self, *args: str, env: Optional[Dict[str, str]] = None) -> subprocess.CompletedProcess:
|
||||||
|
"""
|
||||||
|
Run docker-compose command with optional environment variables.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
*args: Arguments to pass to docker-compose
|
*args: Arguments to pass to docker-compose
|
||||||
|
env: Optional environment variables to set
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
CompletedProcess with result
|
CompletedProcess with result
|
||||||
@@ -88,11 +246,18 @@ class WorkerManager:
|
|||||||
cmd = ["docker-compose", "-f", str(self.compose_file)] + list(args)
|
cmd = ["docker-compose", "-f", str(self.compose_file)] + list(args)
|
||||||
logger.debug(f"Running: {' '.join(cmd)}")
|
logger.debug(f"Running: {' '.join(cmd)}")
|
||||||
|
|
||||||
|
# Merge with current environment
|
||||||
|
full_env = os.environ.copy()
|
||||||
|
if env:
|
||||||
|
full_env.update(env)
|
||||||
|
logger.debug(f"Environment overrides: {env}")
|
||||||
|
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
check=True
|
check=True,
|
||||||
|
env=full_env
|
||||||
)
|
)
|
||||||
|
|
||||||
def _service_to_container_name(self, service_name: str) -> str:
|
def _service_to_container_name(self, service_name: str) -> str:
|
||||||
@@ -135,21 +300,35 @@ class WorkerManager:
|
|||||||
|
|
||||||
def start_worker(self, service_name: str) -> bool:
|
def start_worker(self, service_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Start a worker service using docker-compose.
|
Start a worker service using docker-compose with platform-specific Dockerfile.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
service_name: Name of the Docker Compose service to start (e.g., "worker-python")
|
service_name: Name of the Docker Compose service to start (e.g., "worker-android")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if started successfully, False otherwise
|
True if started successfully, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
console.print(f"🚀 Starting worker: {service_name}")
|
# Extract vertical name from service name
|
||||||
|
vertical = service_name.replace("worker-", "")
|
||||||
|
|
||||||
# Use docker-compose up to create and start the service
|
# Detect platform and select appropriate Dockerfile
|
||||||
result = self._run_docker_compose("up", "-d", service_name)
|
detected_platform = self._detect_platform()
|
||||||
|
dockerfile = self._select_dockerfile(vertical)
|
||||||
|
|
||||||
logger.info(f"Worker {service_name} started")
|
# Set environment variable for docker-compose
|
||||||
|
env_var_name = f"{vertical.upper()}_DOCKERFILE"
|
||||||
|
env = {env_var_name: dockerfile}
|
||||||
|
|
||||||
|
console.print(
|
||||||
|
f"🚀 Starting worker: {service_name} "
|
||||||
|
f"(platform: {detected_platform}, using {dockerfile})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use docker-compose up with --build to ensure correct Dockerfile is used
|
||||||
|
result = self._run_docker_compose("up", "-d", "--build", service_name, env=env)
|
||||||
|
|
||||||
|
logger.info(f"Worker {service_name} started with {dockerfile}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
|
|||||||
+4
-1
@@ -342,7 +342,7 @@ services:
|
|||||||
worker-android:
|
worker-android:
|
||||||
build:
|
build:
|
||||||
context: ./workers/android
|
context: ./workers/android
|
||||||
dockerfile: Dockerfile
|
dockerfile: ${ANDROID_DOCKERFILE:-Dockerfile.amd64}
|
||||||
container_name: fuzzforge-worker-android
|
container_name: fuzzforge-worker-android
|
||||||
profiles:
|
profiles:
|
||||||
- workers
|
- workers
|
||||||
@@ -430,6 +430,9 @@ services:
|
|||||||
PYTHONPATH: /app
|
PYTHONPATH: /app
|
||||||
PYTHONUNBUFFERED: 1
|
PYTHONUNBUFFERED: 1
|
||||||
|
|
||||||
|
# Host filesystem paths (for CLI worker management)
|
||||||
|
FUZZFORGE_HOST_ROOT: ${PWD}
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL: INFO
|
LOG_LEVEL: INFO
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,695 @@
|
|||||||
|
{
|
||||||
|
"tool": {
|
||||||
|
"name": "FuzzForge Security Assessment",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"total_issues": 68,
|
||||||
|
"by_severity": {
|
||||||
|
"warning": 51,
|
||||||
|
"error": 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"findings": [
|
||||||
|
{
|
||||||
|
"rule_id": "sensitive_file_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potentially sensitive file at .env",
|
||||||
|
"location": {
|
||||||
|
"file": ".env",
|
||||||
|
"line": null,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sensitive_file_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potentially sensitive file at .git-credentials",
|
||||||
|
"location": {
|
||||||
|
"file": ".git-credentials",
|
||||||
|
"line": null,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sensitive_file_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potentially sensitive file at private_key.pem",
|
||||||
|
"location": {
|
||||||
|
"file": "private_key.pem",
|
||||||
|
"line": null,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sensitive_file_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potentially sensitive file at wallet.json",
|
||||||
|
"location": {
|
||||||
|
"file": "wallet.json",
|
||||||
|
"line": null,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sensitive_file_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potentially sensitive file at SECRETS_GROUND_TRUTH.json",
|
||||||
|
"location": {
|
||||||
|
"file": "SECRETS_GROUND_TRUTH.json",
|
||||||
|
"line": null,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sensitive_file_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potentially sensitive file at .npmrc",
|
||||||
|
"location": {
|
||||||
|
"file": ".npmrc",
|
||||||
|
"line": null,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sensitive_file_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potentially sensitive file at .fuzzforge/.env",
|
||||||
|
"location": {
|
||||||
|
"file": ".fuzzforge/.env",
|
||||||
|
"line": null,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sensitive_file_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potentially sensitive file at .fuzzforge/.env.template",
|
||||||
|
"location": {
|
||||||
|
"file": ".fuzzforge/.env.template",
|
||||||
|
"line": null,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sensitive_file_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potentially sensitive file at data/credentials.json",
|
||||||
|
"location": {
|
||||||
|
"file": "data/credentials.json",
|
||||||
|
"line": null,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sensitive_file_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potentially sensitive file at data/api_keys.txt",
|
||||||
|
"location": {
|
||||||
|
"file": "data/api_keys.txt",
|
||||||
|
"line": null,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via F-string in SQL query",
|
||||||
|
"location": {
|
||||||
|
"file": "app.py",
|
||||||
|
"line": 31,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Found potential hardcoded API Key in src/api_handler.py",
|
||||||
|
"location": {
|
||||||
|
"file": "src/api_handler.py",
|
||||||
|
"line": 25,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potential hardcoded Authentication Token in src/api_handler.py",
|
||||||
|
"location": {
|
||||||
|
"file": "src/api_handler.py",
|
||||||
|
"line": 21,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function eval(): Arbitrary code execution",
|
||||||
|
"location": {
|
||||||
|
"file": "src/api_handler.py",
|
||||||
|
"line": 34,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function eval(): Arbitrary code execution",
|
||||||
|
"location": {
|
||||||
|
"file": "src/api_handler.py",
|
||||||
|
"line": 54,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function exec(): Arbitrary code execution",
|
||||||
|
"location": {
|
||||||
|
"file": "src/api_handler.py",
|
||||||
|
"line": 49,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function os.system(): Command injection risk",
|
||||||
|
"location": {
|
||||||
|
"file": "src/api_handler.py",
|
||||||
|
"line": 44,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function os.system(): Command injection risk",
|
||||||
|
"location": {
|
||||||
|
"file": "src/api_handler.py",
|
||||||
|
"line": 71,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function subprocess with shell=True: Command injection risk",
|
||||||
|
"location": {
|
||||||
|
"file": "src/api_handler.py",
|
||||||
|
"line": 39,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via String concatenation in SQL",
|
||||||
|
"location": {
|
||||||
|
"file": "src/database.py",
|
||||||
|
"line": 43,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via String formatting in SQL",
|
||||||
|
"location": {
|
||||||
|
"file": "src/database.py",
|
||||||
|
"line": 50,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via String formatting in SQL",
|
||||||
|
"location": {
|
||||||
|
"file": "src/database.py",
|
||||||
|
"line": 57,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via F-string in SQL query",
|
||||||
|
"location": {
|
||||||
|
"file": "src/database.py",
|
||||||
|
"line": 50,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via Dynamic query building",
|
||||||
|
"location": {
|
||||||
|
"file": "src/database.py",
|
||||||
|
"line": 43,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via Dynamic query building",
|
||||||
|
"location": {
|
||||||
|
"file": "src/database.py",
|
||||||
|
"line": 75,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function os.system(): Command injection risk",
|
||||||
|
"location": {
|
||||||
|
"file": "src/database.py",
|
||||||
|
"line": 69,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function pickle.load(): Deserialization vulnerability",
|
||||||
|
"location": {
|
||||||
|
"file": "src/database.py",
|
||||||
|
"line": 64,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Found potential hardcoded Private Key in scripts/backup.js",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/backup.js",
|
||||||
|
"line": 81,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potential hardcoded Potential Secret Hash in scripts/backup.js",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/backup.js",
|
||||||
|
"line": 81,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function eval(): Arbitrary code execution",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/backup.js",
|
||||||
|
"line": 23,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function new Function(): Arbitrary code execution",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/backup.js",
|
||||||
|
"line": 28,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function innerHTML: XSS vulnerability",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/backup.js",
|
||||||
|
"line": 33,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function innerHTML: XSS vulnerability",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/backup.js",
|
||||||
|
"line": 37,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function document.write(): XSS vulnerability",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/backup.js",
|
||||||
|
"line": 42,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Found potential hardcoded Private Key in src/Main.java",
|
||||||
|
"location": {
|
||||||
|
"file": "src/Main.java",
|
||||||
|
"line": 77,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via String concatenation in SQL",
|
||||||
|
"location": {
|
||||||
|
"file": "src/Main.java",
|
||||||
|
"line": 23,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via String concatenation in SQL",
|
||||||
|
"location": {
|
||||||
|
"file": "src/Main.java",
|
||||||
|
"line": 29,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via Dynamic query building",
|
||||||
|
"location": {
|
||||||
|
"file": "src/Main.java",
|
||||||
|
"line": 23,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "sql_injection_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Detected potential SQL injection vulnerability via Dynamic query building",
|
||||||
|
"location": {
|
||||||
|
"file": "src/Main.java",
|
||||||
|
"line": 29,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function eval(): Arbitrary code execution",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 28,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function exec(): Command execution",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 22,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function exec(): Command execution",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 23,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function system(): Command execution",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 21,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function shell_exec(): Command execution",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 23,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 12,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 21,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 23,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 24,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 31,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 45,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 50,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 57,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 13,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 22,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 27,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 32,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 40,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 46,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 53,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 54,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 61,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "dangerous_function_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing",
|
||||||
|
"location": {
|
||||||
|
"file": "scripts/deploy.php",
|
||||||
|
"line": 62,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Found potential hardcoded API Key in src/utils.rb",
|
||||||
|
"location": {
|
||||||
|
"file": "src/utils.rb",
|
||||||
|
"line": 64,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potential hardcoded Hardcoded Password in src/utils.rb",
|
||||||
|
"location": {
|
||||||
|
"file": "src/utils.rb",
|
||||||
|
"line": 63,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Found potential hardcoded Private Key in src/app.go",
|
||||||
|
"location": {
|
||||||
|
"file": "src/app.go",
|
||||||
|
"line": 59,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_high",
|
||||||
|
"severity": "error",
|
||||||
|
"message": "Found potential hardcoded Private Key in src/app.go",
|
||||||
|
"location": {
|
||||||
|
"file": "src/app.go",
|
||||||
|
"line": 62,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potential hardcoded Potential Secret Hash in src/app.go",
|
||||||
|
"location": {
|
||||||
|
"file": "src/app.go",
|
||||||
|
"line": 59,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_id": "hardcoded_secret_medium",
|
||||||
|
"severity": "warning",
|
||||||
|
"message": "Found potential hardcoded Potential Secret Hash in src/app.go",
|
||||||
|
"location": {
|
||||||
|
"file": "src/app.go",
|
||||||
|
"line": 62,
|
||||||
|
"column": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# FuzzForge Vertical Worker: Android Security
|
||||||
|
#
|
||||||
|
# Pre-installed tools for Android security analysis:
|
||||||
|
# - Android SDK (adb, aapt)
|
||||||
|
# - apktool (APK decompilation)
|
||||||
|
# - jadx (Dex to Java decompiler)
|
||||||
|
# - Frida (dynamic instrumentation)
|
||||||
|
# - androguard (Python APK analysis)
|
||||||
|
# - MobSF dependencies
|
||||||
|
#
|
||||||
|
# Note: Uses amd64 platform for compatibility with Android 32-bit tools
|
||||||
|
|
||||||
|
FROM --platform=linux/amd64 python:3.11-slim-bookworm
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
# Build essentials
|
||||||
|
build-essential \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
unzip \
|
||||||
|
# Java (required for Android tools)
|
||||||
|
openjdk-17-jdk \
|
||||||
|
# Android tools dependencies (32-bit libraries for emulated amd64)
|
||||||
|
lib32stdc++6 \
|
||||||
|
lib32z1 \
|
||||||
|
# Frida dependencies
|
||||||
|
libc6-dev \
|
||||||
|
# XML/Binary analysis
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt-dev \
|
||||||
|
# Network tools
|
||||||
|
netcat-openbsd \
|
||||||
|
tcpdump \
|
||||||
|
# MobSF dependencies
|
||||||
|
xfonts-75dpi \
|
||||||
|
xfonts-base \
|
||||||
|
# Cleanup
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install wkhtmltopdf (required for MobSF PDF reports)
|
||||||
|
RUN wget -q https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y ./wkhtmltox_0.12.6.1-3.bookworm_amd64.deb && \
|
||||||
|
rm wkhtmltox_0.12.6.1-3.bookworm_amd64.deb && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Android SDK Command Line Tools
|
||||||
|
ENV ANDROID_HOME=/opt/android-sdk
|
||||||
|
ENV PATH="${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}"
|
||||||
|
|
||||||
|
RUN mkdir -p ${ANDROID_HOME}/cmdline-tools && \
|
||||||
|
cd ${ANDROID_HOME}/cmdline-tools && \
|
||||||
|
wget -q https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip && \
|
||||||
|
unzip -q commandlinetools-linux-9477386_latest.zip && \
|
||||||
|
mv cmdline-tools latest && \
|
||||||
|
rm commandlinetools-linux-9477386_latest.zip && \
|
||||||
|
# Accept licenses
|
||||||
|
yes | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --licenses && \
|
||||||
|
# Install platform tools (adb, fastboot)
|
||||||
|
${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager "platform-tools" "build-tools;33.0.0"
|
||||||
|
|
||||||
|
# Install apktool
|
||||||
|
RUN wget -q https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool -O /usr/local/bin/apktool && \
|
||||||
|
wget -q https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.9.3.jar -O /usr/local/bin/apktool.jar && \
|
||||||
|
chmod +x /usr/local/bin/apktool
|
||||||
|
|
||||||
|
# Install jadx (Dex to Java decompiler)
|
||||||
|
RUN wget -q https://github.com/skylot/jadx/releases/download/v1.4.7/jadx-1.4.7.zip -O /tmp/jadx.zip && \
|
||||||
|
unzip -q /tmp/jadx.zip -d /opt/jadx && \
|
||||||
|
ln -s /opt/jadx/bin/jadx /usr/local/bin/jadx && \
|
||||||
|
ln -s /opt/jadx/bin/jadx-gui /usr/local/bin/jadx-gui && \
|
||||||
|
rm /tmp/jadx.zip
|
||||||
|
|
||||||
|
# Install Python dependencies for Android security tools
|
||||||
|
COPY requirements.txt /tmp/requirements.txt
|
||||||
|
RUN pip3 install --no-cache-dir -r /tmp/requirements.txt && \
|
||||||
|
rm /tmp/requirements.txt
|
||||||
|
|
||||||
|
# Install androguard (Python APK analysis framework)
|
||||||
|
RUN pip3 install --no-cache-dir androguard pyaxmlparser
|
||||||
|
|
||||||
|
# Install Frida
|
||||||
|
RUN pip3 install --no-cache-dir frida-tools frida
|
||||||
|
|
||||||
|
# Install OpenGrep/Semgrep (expose as opengrep command)
|
||||||
|
RUN pip3 install --no-cache-dir semgrep==1.45.0 && \
|
||||||
|
ln -sf /usr/local/bin/semgrep /usr/local/bin/opengrep
|
||||||
|
|
||||||
|
# Install MobSF (Mobile Security Framework)
|
||||||
|
RUN git clone --depth 1 --branch v3.9.7 https://github.com/MobSF/Mobile-Security-Framework-MobSF.git /app/mobsf && \
|
||||||
|
cd /app/mobsf && \
|
||||||
|
./setup.sh
|
||||||
|
|
||||||
|
# Install aiohttp for async HTTP requests (used by MobSF scanner module)
|
||||||
|
RUN pip3 install --no-cache-dir aiohttp
|
||||||
|
|
||||||
|
# Create cache directory
|
||||||
|
RUN mkdir -p /cache && chmod 755 /cache
|
||||||
|
|
||||||
|
# Copy worker entrypoint (generic, works for all verticals)
|
||||||
|
COPY worker.py /app/worker.py
|
||||||
|
|
||||||
|
# Create startup script that runs MobSF in background and then starts worker
|
||||||
|
RUN echo '#!/bin/bash\n\
|
||||||
|
# Start MobSF server in background with sync workers (avoid Rosetta syscall issues)\n\
|
||||||
|
echo "Starting MobSF server in background..."\n\
|
||||||
|
cd /app/mobsf && python3 -m poetry run gunicorn -b 127.0.0.1:8877 \\\n\
|
||||||
|
mobsf.MobSF.wsgi:application \\\n\
|
||||||
|
--worker-class=sync \\\n\
|
||||||
|
--workers=2 \\\n\
|
||||||
|
--timeout=3600 \\\n\
|
||||||
|
--log-level=error \\\n\
|
||||||
|
> /tmp/mobsf.log 2>&1 &\n\
|
||||||
|
MOBSF_PID=$!\n\
|
||||||
|
echo "MobSF started with PID: $MOBSF_PID"\n\
|
||||||
|
\n\
|
||||||
|
# Wait for MobSF to initialize\n\
|
||||||
|
sleep 10\n\
|
||||||
|
\n\
|
||||||
|
# Generate and store MobSF API key\n\
|
||||||
|
if [ -f /root/.MobSF/secret ]; then\n\
|
||||||
|
SECRET=$(cat /root/.MobSF/secret)\n\
|
||||||
|
export MOBSF_API_KEY=$(echo -n "$SECRET" | sha256sum | cut -d " " -f1)\n\
|
||||||
|
echo "MobSF API key: $MOBSF_API_KEY"\n\
|
||||||
|
fi\n\
|
||||||
|
\n\
|
||||||
|
# Start worker\n\
|
||||||
|
echo "Starting Temporal worker..."\n\
|
||||||
|
exec python3 /app/worker.py\n\
|
||||||
|
' > /app/start.sh && chmod +x /app/start.sh
|
||||||
|
|
||||||
|
# Add toolbox to Python path (mounted at runtime)
|
||||||
|
ENV PYTHONPATH="/app:/app/toolbox:${PYTHONPATH}"
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||||
|
ENV MOBSF_PORT=8877
|
||||||
|
|
||||||
|
# Healthcheck
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
|
||||||
|
CMD python3 -c "import sys; sys.exit(0)"
|
||||||
|
|
||||||
|
# Run startup script (starts MobSF + worker)
|
||||||
|
CMD ["/app/start.sh"]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# FuzzForge Vertical Worker: Android Security
|
# FuzzForge Vertical Worker: Android Security (ARM64)
|
||||||
#
|
#
|
||||||
# Pre-installed tools for Android security analysis:
|
# Pre-installed tools for Android security analysis:
|
||||||
# - Android SDK (adb, aapt)
|
# - Android SDK (adb, aapt)
|
||||||
@@ -6,9 +6,11 @@
|
|||||||
# - jadx (Dex to Java decompiler)
|
# - jadx (Dex to Java decompiler)
|
||||||
# - Frida (dynamic instrumentation)
|
# - Frida (dynamic instrumentation)
|
||||||
# - androguard (Python APK analysis)
|
# - androguard (Python APK analysis)
|
||||||
# - MobSF dependencies
|
#
|
||||||
|
# Note: MobSF is excluded due to Rosetta 2 syscall incompatibility
|
||||||
|
# Note: Uses amd64 platform for compatibility with Android 32-bit tools
|
||||||
|
|
||||||
FROM python:3.11-slim-bookworm
|
FROM --platform=linux/amd64 python:3.11-slim-bookworm
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -23,7 +25,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
unzip \
|
unzip \
|
||||||
# Java (required for Android tools)
|
# Java (required for Android tools)
|
||||||
openjdk-17-jdk \
|
openjdk-17-jdk \
|
||||||
# Android tools dependencies
|
# Android tools dependencies (32-bit libraries for emulated amd64)
|
||||||
lib32stdc++6 \
|
lib32stdc++6 \
|
||||||
lib32z1 \
|
lib32z1 \
|
||||||
# Frida dependencies
|
# Frida dependencies
|
||||||
@@ -75,20 +77,34 @@ RUN pip3 install --no-cache-dir androguard pyaxmlparser
|
|||||||
# Install Frida
|
# Install Frida
|
||||||
RUN pip3 install --no-cache-dir frida-tools frida
|
RUN pip3 install --no-cache-dir frida-tools frida
|
||||||
|
|
||||||
|
# Install OpenGrep/Semgrep (expose as opengrep command)
|
||||||
|
RUN pip3 install --no-cache-dir semgrep==1.45.0 && \
|
||||||
|
ln -sf /usr/local/bin/semgrep /usr/local/bin/opengrep
|
||||||
|
|
||||||
|
# NOTE: MobSF is NOT installed on ARM64 platform due to Rosetta 2 incompatibility
|
||||||
|
# The workflow will gracefully skip MobSF analysis on this platform
|
||||||
|
|
||||||
# Create cache directory
|
# Create cache directory
|
||||||
RUN mkdir -p /cache && chmod 755 /cache
|
RUN mkdir -p /cache && chmod 755 /cache
|
||||||
|
|
||||||
# Copy worker entrypoint (generic, works for all verticals)
|
# Copy worker entrypoint (generic, works for all verticals)
|
||||||
COPY worker.py /app/worker.py
|
COPY worker.py /app/worker.py
|
||||||
|
|
||||||
|
# Create simplified startup script (no MobSF)
|
||||||
|
RUN echo '#!/bin/bash\n\
|
||||||
|
# ARM64 worker - MobSF disabled due to Rosetta 2 limitations\n\
|
||||||
|
echo "Starting Temporal worker (ARM64 platform - MobSF disabled)..."\n\
|
||||||
|
exec python3 /app/worker.py\n\
|
||||||
|
' > /app/start.sh && chmod +x /app/start.sh
|
||||||
|
|
||||||
# Add toolbox to Python path (mounted at runtime)
|
# Add toolbox to Python path (mounted at runtime)
|
||||||
ENV PYTHONPATH="/app:/app/toolbox:${PYTHONPATH}"
|
ENV PYTHONPATH="/app:/app/toolbox:${PYTHONPATH}"
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||||
|
|
||||||
# Healthcheck
|
# Healthcheck
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
|
||||||
CMD python3 -c "import sys; sys.exit(0)"
|
CMD python3 -c "import sys; sys.exit(0)"
|
||||||
|
|
||||||
# Run worker
|
# Run startup script
|
||||||
CMD ["python3", "/app/worker.py"]
|
CMD ["/app/start.sh"]
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Android Worker Metadata
|
||||||
|
#
|
||||||
|
# Platform-specific configuration for Android security analysis worker.
|
||||||
|
# This file defines which Dockerfile to use for each platform and what tools
|
||||||
|
# are available on that platform.
|
||||||
|
|
||||||
|
name: android
|
||||||
|
version: "1.0.0"
|
||||||
|
description: "Android application security testing worker with Jadx, OpenGrep, and MobSF"
|
||||||
|
|
||||||
|
# Default platform when auto-detection fails or metadata is not platform-aware
|
||||||
|
default_platform: linux/amd64
|
||||||
|
|
||||||
|
# Platform-specific configurations
|
||||||
|
platforms:
|
||||||
|
# x86_64 / Intel / AMD platform (full toolchain including MobSF)
|
||||||
|
linux/amd64:
|
||||||
|
dockerfile: Dockerfile.amd64
|
||||||
|
description: "Full Android toolchain with MobSF support"
|
||||||
|
supported_tools:
|
||||||
|
- jadx # APK decompiler
|
||||||
|
- opengrep # Static analysis with custom Android rules
|
||||||
|
- mobsf # Mobile Security Framework
|
||||||
|
- frida # Dynamic instrumentation
|
||||||
|
- androguard # Python APK analysis
|
||||||
|
|
||||||
|
# ARM64 / Apple Silicon platform (MobSF excluded due to Rosetta limitations)
|
||||||
|
linux/arm64:
|
||||||
|
dockerfile: Dockerfile.arm64
|
||||||
|
description: "Android toolchain without MobSF (ARM64/Apple Silicon compatible)"
|
||||||
|
supported_tools:
|
||||||
|
- jadx # APK decompiler
|
||||||
|
- opengrep # Static analysis with custom Android rules
|
||||||
|
- frida # Dynamic instrumentation
|
||||||
|
- androguard # Python APK analysis
|
||||||
|
disabled_tools:
|
||||||
|
mobsf: "Incompatible with Rosetta 2 emulation (requires syscall 284: copy_file_range)"
|
||||||
|
notes: |
|
||||||
|
MobSF cannot run under Rosetta 2 on Apple Silicon Macs due to missing
|
||||||
|
syscall implementations. The workflow will gracefully skip MobSF analysis
|
||||||
|
on this platform while still providing comprehensive security testing via
|
||||||
|
Jadx decompilation and OpenGrep static analysis.
|
||||||
Reference in new issue
Block a user