first commit

This commit is contained in:
tmarschutz
2025-10-03 11:45:17 +02:00
parent 09821c1c43
commit 5da3f1e071
10571 changed files with 1386578 additions and 1 deletions
@@ -0,0 +1,59 @@
FROM prefecthq/prefect:3-python3.11
WORKDIR /app
# Install system dependencies for MobSF and Jadx
RUN apt-get update && apt-get install -y \
git \
default-jdk \
wget \
unzip \
xfonts-75dpi \
xfonts-base \
&& rm -rf /var/lib/apt/lists/* \
&& wget 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 Jadx
RUN wget https://github.com/skylot/jadx/releases/download/v1.5.0/jadx-1.5.0.zip -O /tmp/jadx.zip \
&& unzip /tmp/jadx.zip -d /opt/jadx \
&& rm /tmp/jadx.zip \
&& ln -s /opt/jadx/bin/jadx /usr/local/bin/jadx
# The upstream OpenGrep CLI is not yet published on PyPI. Use semgrep (the
# engine that OpenGrep builds upon) and expose it under the `opengrep` name so
# the workflow module can invoke it transparently.
RUN pip install --no-cache-dir semgrep==1.45.0 \
&& ln -sf /usr/local/bin/semgrep /usr/local/bin/opengrep
# Clone and setup MobSF
RUN git clone https://github.com/MobSF/Mobile-Security-Framework-MobSF.git /app/mobsf \
&& cd /app/mobsf \
&& git checkout v3.9.7 \
&& ./setup.sh
# Force rebuild after this point
ARG CACHEBUST=2
# Copy the entire toolbox directory structure
COPY . /app/toolbox
# Copy Android custom rules to a well-known location
COPY ./modules/android/custom_rules /app/custom_opengrep_rules
ENV PYTHONPATH=/app/toolbox:$PYTHONPATH
ENV MOBSF_PORT=8877
# Create startup script to launch MobSF in background and then Prefect
RUN echo '#!/bin/bash\n\
cd /app/mobsf && ./run.sh 127.0.0.1:8877 &\n\
echo "Waiting for MobSF to start..."\n\
sleep 10\n\
echo "Starting Prefect engine..."\n\
exec python -m prefect.engine\n\
' > /app/start.sh && chmod +x /app/start.sh
CMD ["/app/start.sh"]
@@ -0,0 +1,16 @@
# Use existing image with MobSF already installed
FROM localhost:5001/fuzzforge/android_static_analysis:latest
# Install unzip and Jadx
RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/* \
&& wget https://github.com/skylot/jadx/releases/download/v1.5.0/jadx-1.5.0.zip \
&& unzip -o jadx-1.5.0.zip -d /opt/jadx \
&& rm jadx-1.5.0.zip \
&& chmod +x /opt/jadx/bin/jadx \
&& ln -sf /opt/jadx/bin/jadx /usr/local/bin/jadx
# Copy updated toolbox files
COPY . /app/toolbox
# Copy Android custom rules
COPY ./modules/android/custom_rules /app/custom_opengrep_rules
@@ -0,0 +1,6 @@
"""
Android Static Analysis Security Testing (SAST) Workflow
This package contains the Android SAST workflow that combines
multiple static analysis tools optimized for Java code security.
"""
@@ -0,0 +1,135 @@
name: android_static_analysis
version: "1.0.0"
description: "Perform static analysis on Android applications using OpenGrep and MobSF."
author: "FuzzForge Team"
category: "specialized"
tags:
- "android"
- "static-analysis"
- "security"
- "opengrep"
- "semgrep"
- "mobsf"
supported_volume_modes:
- "ro"
- "rw"
default_volume_mode: "ro"
default_target_path: "/workspace/android_test"
requirements:
tools:
- "opengrep"
- "mobsf"
- "sarif_reporter"
resources:
memory: "2Gi"
cpu: "2000m"
timeout: 3600
environment:
python: "3.11"
has_docker: true
default_parameters:
target_path: "/workspace/android_test"
volume_mode: "ro"
apk_path: ""
opengrep_config: {}
custom_rules_path: "/app/custom_opengrep_rules"
reporter_config: {}
parameters:
type: object
properties:
target_path:
type: string
default: "/workspace/android_test"
description: "Path to the decompiled Android source code for OpenGrep analysis."
volume_mode:
type: string
enum: ["ro", "rw"]
default: "ro"
description: "Volume mount mode for the attached workspace."
apk_path:
type: string
default: ""
description: "Path to the APK file for MobSF analysis (relative to workspace parent or absolute). If empty, MobSF analysis will be skipped."
opengrep_config:
type: object
description: "Configuration object forwarded to the OpenGrep module."
properties:
config:
type: string
enum: ["auto", "p/security-audit", "p/owasp-top-ten", "p/cwe-top-25"]
description: "Preset OpenGrep ruleset to run."
custom_rules_path:
type: string
description: "Directory that contains custom OpenGrep rules."
languages:
type: array
items:
type: string
description: "Restrict analysis to specific languages."
include_patterns:
type: array
items:
type: string
description: "File patterns to include in the scan."
exclude_patterns:
type: array
items:
type: string
description: "File patterns to exclude from the scan."
max_target_bytes:
type: integer
description: "Maximum file size to analyze (bytes)."
timeout:
type: integer
description: "Analysis timeout in seconds."
severity:
type: array
items:
type: string
enum: ["ERROR", "WARNING", "INFO"]
description: "Severities to include in the results."
confidence:
type: array
items:
type: string
enum: ["HIGH", "MEDIUM", "LOW"]
description: "Confidence levels to include in the results."
custom_rules_path:
type:
- string
- "null"
default: "/app/custom_opengrep_rules"
description: "Optional in-container path pointing to custom OpenGrep rules."
reporter_config:
type: object
description: "Configuration overrides for the SARIF reporter."
properties:
include_code_flows:
type: boolean
description: "Include code flow information in the SARIF output."
logical_id:
type: string
description: "Custom identifier to attach to the generated SARIF report."
output_schema:
type: object
properties:
sarif:
type: object
description: "SARIF-formatted findings produced by the workflow."
summary:
type: object
description: "Summary information about the analysis execution."
properties:
total_findings:
type: integer
severity_counts:
type: object
tool_metadata:
type: object
@@ -0,0 +1,2 @@
requests
pydantic
@@ -0,0 +1,280 @@
"""
Android Static Analysis Workflow - Analyze APKs using Jadx, MobSF, and OpenGrep
"""
import sys
import os
import logging
import subprocess
import time
import signal
from pathlib import Path
from typing import Dict, Any
from prefect import flow, task
# S'assurer que /app est dans le PYTHONPATH (exécutions Docker)
sys.path.insert(0, "/app")
# Import des modules internes
from toolbox.modules.android.jadx import JadxModule
from toolbox.modules.android.opengrep import OpenGrepModule
from toolbox.modules.reporter import SARIFReporter
from toolbox.modules.android.mobsf import MobSFModule
# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ---------------------- TASKS ---------------------- #
@task(name="jadx_decompilation")
async def run_jadx_task(workspace: Path, config: Dict[str, Any]) -> Dict[str, Any]:
print("Running Jadx APK decompilation")
print(f" APK file: {config.get('apk_path')}")
print(f" Output dir: {config.get('output_dir')}")
module = JadxModule()
result = await module.execute(config, workspace)
print(f"Jadx completed: {result.status}")
if result.error:
print(f"Jadx error: {result.error}")
if result.status == "success":
print(f"Jadx decompiled {result.summary.get('java_files', 0)} Java files")
print(f"Source dir: {result.summary.get('source_dir')}")
return result.dict()
@task(name="opengrep_analysis")
async def run_opengrep_task(workspace: Path, config: Dict[str, Any]) -> Dict[str, Any]:
print("Running OpenGrep static analysis")
print(f" Workspace: {workspace}")
print(f" Config: {config}")
module = OpenGrepModule()
result = await module.execute(config, workspace)
print(f"OpenGrep completed: {result.status}")
print(f"OpenGrep findings count: {len(result.findings)}")
print(f"OpenGrep summary: {result.summary}")
return result.dict()
@task(name="mobsf_analysis")
async def run_mobsf_task(workspace: Path, config: Dict[str, Any]) -> Dict[str, Any]:
print("Running MobSF static analysis")
print(f" APK file: {config.get('file_path')}")
print(f" MobSF URL: {config.get('mobsf_url')}")
module = MobSFModule()
result = await module.execute(config, workspace)
print(f"MobSF scan completed: {result.status}")
print(f"MobSF findings count: {len(result.findings)}")
return result.dict()
@task(name="android_report_generation")
async def generate_android_sarif_report(
opengrep_result: Dict[str, Any],
mobsf_result: Dict[str, Any],
config: Dict[str, Any],
workspace: Path
) -> Dict[str, Any]:
logger.info("Generating SARIF report for Android scan")
reporter = SARIFReporter()
all_findings = []
all_findings.extend(opengrep_result.get("findings", []))
# Add MobSF findings if available
if mobsf_result:
all_findings.extend(mobsf_result.get("findings", []))
reporter_config = {
**(config or {}),
"findings": all_findings,
"tool_name": "FuzzForge Android Static Analysis",
"tool_version": "1.0.0",
}
result = await reporter.execute(reporter_config, workspace)
# Le reporter renvoie typiquement {"sarif": {...}} dans result.dict()
return result.dict().get("sarif", {})
# ---------------------- FLOW ---------------------- #
@flow(name="android_static_analysis", log_prints=True)
async def main_flow(
target_path: str = os.getenv("FF_TARGET_PATH", "/workspace/android_test"),
volume_mode: str = "ro",
apk_path: str = "",
opengrep_config: Dict[str, Any] = {},
custom_rules_path: str = None,
reporter_config: Dict[str, Any] = {},
) -> Dict[str, Any]:
"""
Android static analysis workflow using OpenGrep and MobSF.
Args:
target_path: Path to decompiled source code (for OpenGrep analysis)
volume_mode: Volume mount mode (ro/rw)
apk_path: Path to APK file for MobSF analysis (relative to workspace or absolute)
opengrep_config: Configuration for OpenGrep module
custom_rules_path: Path to custom OpenGrep rules
reporter_config: Configuration for SARIF reporter
"""
print("📱 Starting Android Static Analysis Workflow")
print(f"Workspace: {target_path} (mode: {volume_mode})")
workspace = Path(target_path)
# Start MobSF server in background if APK analysis is needed
mobsf_process = None
if apk_path:
print("🚀 Starting MobSF server in background...")
try:
mobsf_process = subprocess.Popen(
["bash", "-c", "cd /app/mobsf && ./run.sh 127.0.0.1:8877"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
print("⏳ Waiting for MobSF to initialize (45 seconds)...")
time.sleep(45)
print("✅ MobSF should be ready now")
# Retrieve MobSF API key from secret file
print("🔑 Retrieving MobSF API key...")
try:
secret_file = Path("/root/.MobSF/secret")
if secret_file.exists():
secret = secret_file.read_text().strip()
if secret:
# API key is SHA256 hash of the secret file contents
import hashlib
api_key = hashlib.sha256(secret.encode()).hexdigest()
os.environ["MOBSF_API_KEY"] = api_key
print(f"✅ MobSF API key retrieved")
else:
print("⚠️ API key file is empty")
else:
print(f"⚠️ API key file not found at {secret_file}")
except Exception as e:
print(f"⚠️ Error retrieving API key: {e}")
except Exception as e:
print(f"⚠️ Failed to start MobSF: {e}")
mobsf_process = None
# Resolve APK path if provided
# Note: target_path gets mounted as /workspace/ in the execution container
# So all paths should be relative to /workspace/
apk_file_path = None
if apk_path:
apk_path_obj = Path(apk_path)
if apk_path_obj.is_absolute():
apk_file_path = str(apk_path_obj)
else:
# Relative paths are relative to /workspace/ (the mounted target directory)
apk_file_path = f"/workspace/{apk_path}"
print(f"APK path resolved to: {apk_file_path}")
print(f"Checking if APK exists in target: {(Path(target_path) / apk_path).exists()}")
# Set default Android-specific configuration if not provided
if not opengrep_config:
opengrep_config = {
"languages": ["java", "kotlin"], # Focus on Android languages
}
# Use custom Android rules if available, otherwise use custom_rules_path param
if custom_rules_path:
opengrep_config["custom_rules_path"] = custom_rules_path
elif "custom_rules_path" not in opengrep_config:
# Default to custom Android security rules
opengrep_config["custom_rules_path"] = "/app/custom_opengrep_rules"
try:
# --- Phase 1 : Jadx Decompilation ---
jadx_result = None
actual_workspace = workspace
if apk_file_path:
print(f"Phase 1: Jadx decompilation of APK: {apk_file_path}")
jadx_config = {
"apk_path": apk_file_path,
"output_dir": "jadx_output",
"overwrite": True,
"threads": 4,
}
jadx_result = await run_jadx_task(workspace, jadx_config)
if jadx_result.get("status") == "success":
# Use Jadx source output as workspace for OpenGrep
source_dir = jadx_result.get("summary", {}).get("source_dir")
if source_dir:
actual_workspace = Path(source_dir)
print(f"✅ Jadx decompiled {jadx_result.get('summary', {}).get('java_files', 0)} Java files")
print(f" OpenGrep will analyze: {source_dir}")
else:
print(f"⚠️ Jadx failed: {jadx_result.get('error', 'unknown error')}")
else:
print("Phase 1: Jadx decompilation skipped (no APK provided)")
# --- Phase 2 : OpenGrep ---
print("Phase 2: OpenGrep analysis on source code")
print(f"Using config: {opengrep_config}")
opengrep_result = await run_opengrep_task(actual_workspace, opengrep_config)
# --- Phase 3 : MobSF ---
mobsf_result = None
if apk_file_path:
print(f"Phase 3: MobSF analysis on APK: {apk_file_path}")
mobsf_config = {
"mobsf_url": "http://localhost:8877",
"file_path": apk_file_path,
"api_key": os.environ.get("MOBSF_API_KEY", "")
}
print(f"Using MobSF config (api_key={mobsf_config['api_key'][:10]}...): {mobsf_config}")
mobsf_result = await run_mobsf_task(workspace, mobsf_config)
print(f"MobSF result: {mobsf_result}")
else:
print(f"Phase 3: MobSF analysis skipped (apk_path='{apk_path}' empty)")
# --- Phase 4 : Rapport SARIF ---
print("Phase 4: SARIF report generation")
sarif_report = await generate_android_sarif_report(
opengrep_result, mobsf_result, reporter_config or {}, workspace
)
findings = sarif_report.get("runs", [{}])[0].get("results", []) if sarif_report else []
print(f"✅ Workflow complete with {len(findings)} findings")
return sarif_report
except Exception as e:
logger.error(f"Workflow failed: {e}")
print(f"❌ Workflow failed: {e}")
# Retourner un squelette SARIF minimal en cas d'échec
return {
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {"driver": {"name": "FuzzForge Android Static Analysis"}},
"results": [],
"invocations": [
{
"executionSuccessful": False,
"exitCode": 1,
"exitCodeDescription": str(e),
}
],
}
],
}
finally:
# Cleanup: Stop MobSF if it was started
if mobsf_process:
print("🛑 Stopping MobSF server...")
try:
mobsf_process.terminate()
mobsf_process.wait(timeout=5)
print("✅ MobSF stopped")
except Exception as e:
print(f"⚠️ Error stopping MobSF: {e}")
try:
mobsf_process.kill()
except:
pass
+17
View File
@@ -30,6 +30,7 @@ logger = logging.getLogger(__name__)
# Import each workflow individually to handle failures gracefully
security_assessment_flow = None
secret_detection_flow = None
android_static_analysis_flow = None
# Try to import each workflow individually
try:
@@ -42,6 +43,11 @@ try:
except ImportError as e:
logger.warning(f"Failed to import secret_detection_scan workflow: {e}")
try:
from .android_static_analysis.workflow import main_flow as android_static_analysis_flow
except ImportError as e:
logger.warning(f"Failed to import android_static_analysis workflow: {e}")
# Manual registry - developers add workflows here after creation
# Only include workflows that were successfully imported
@@ -70,6 +76,17 @@ if secret_detection_flow is not None:
"tags": ["secrets", "credentials", "detection", "trufflehog", "gitleaks", "comprehensive"]
}
if android_static_analysis_flow is not None:
WORKFLOW_REGISTRY["android_static_analysis"] = {
"flow": android_static_analysis_flow,
"module_path": "toolbox.workflows.android_static_analysis.workflow",
"function_name": "main_flow",
"description": "Perform static analysis on Android applications using OpenGrep",
"version": "1.0.0",
"author": "FuzzForge Team",
"tags": ["android", "static-analysis", "security", "opengrep", "semgrep"]
}
#
# To add a new workflow, follow this pattern:
#