mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-07-01 13:35:31 +02:00
feat: Add Android static analysis workflow with Jadx, OpenGrep, and MobSF
Comprehensive Android security testing workflow converted from Prefect to Temporal architecture: Modules (3): - JadxDecompiler: APK to Java source code decompilation - OpenGrepAndroid: Static analysis with Android-specific security rules - MobSFScanner: Comprehensive mobile security framework integration Custom Rules (13): - clipboard-sensitive-data, hardcoded-secrets, insecure-data-storage - insecure-deeplink, insecure-logging, intent-redirection - sensitive_data_sharedPreferences, sqlite-injection - vulnerable-activity, vulnerable-content-provider, vulnerable-service - webview-javascript-enabled, webview-load-arbitrary-url Workflow: - 6-phase Temporal workflow: download → Jadx → OpenGrep → MobSF → SARIF → upload - 4 activities: decompile_with_jadx, scan_with_opengrep, scan_with_mobsf, generate_android_sarif - SARIF output combining findings from all security tools Docker Worker: - ARM64 Mac compatibility via amd64 platform emulation - Pre-installed: Android SDK, Jadx 1.4.7, OpenGrep 1.45.0, MobSF 3.9.7 - MobSF runs as background service with API key auto-generation - Added aiohttp for async HTTP communication Test APKs: - BeetleBug.apk and shopnest.apk for workflow validation
This commit is contained in:
@@ -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,200 @@
|
||||
"""
|
||||
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
|
||||
"""
|
||||
logger.info(f"Activity: scan_with_mobsf (workspace={workspace_path})")
|
||||
|
||||
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,261 @@
|
||||
"""
|
||||
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})")
|
||||
download_result = await workflow.execute_activity(
|
||||
"download_target",
|
||||
args=[target_id],
|
||||
start_to_close_timeout=timedelta(minutes=10),
|
||||
retry_policy=retry_policy,
|
||||
)
|
||||
workspace_path = download_result["workspace_path"]
|
||||
workflow.logger.info(f"✓ Target downloaded to: {workspace_path}")
|
||||
|
||||
# Determine APK path
|
||||
actual_apk_path = apk_path if apk_path else download_result.get("primary_file", "app.apk")
|
||||
|
||||
# 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
|
||||
),
|
||||
)
|
||||
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")
|
||||
|
||||
upload_result = await workflow.execute_activity(
|
||||
"upload_results",
|
||||
args=[target_id, sarif_report],
|
||||
start_to_close_timeout=timedelta(minutes=10),
|
||||
retry_policy=retry_policy,
|
||||
)
|
||||
|
||||
workflow.logger.info(f"✓ Results uploaded: {upload_result.get('result_url')}")
|
||||
|
||||
# Phase 6: Cleanup cache
|
||||
workflow.logger.info("Phase 6: Cleaning up cache")
|
||||
|
||||
await workflow.execute_activity(
|
||||
"cleanup_cache",
|
||||
args=[target_id],
|
||||
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": upload_result.get("result_url"),
|
||||
}
|
||||
|
||||
workflow.logger.info(
|
||||
f"✅ AndroidStaticAnalysisWorkflow completed successfully: {total_findings} findings"
|
||||
)
|
||||
|
||||
return {
|
||||
"sarif": sarif_report,
|
||||
"summary": summary,
|
||||
}
|
||||
Reference in New Issue
Block a user