mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-06-09 08:13:54 +02:00
first commit
This commit is contained in:
@@ -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
|
||||
@@ -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:
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user