Release v0.7.3 - Android workflows, LiteLLM integration, ARM64 support (#32)

* ci: add worker validation and Docker build checks

Add automated validation to prevent worker-related issues:

**Worker Validation Script:**
- New script: .github/scripts/validate-workers.sh
- Validates all workers in docker-compose.yml exist
- Checks required files: Dockerfile, requirements.txt, worker.py
- Verifies files are tracked by git (not gitignored)
- Detects gitignore issues that could hide workers

**CI Workflow Updates:**
- Added validate-workers job (runs on every PR)
- Added build-workers job (runs if workers/ modified)
- Uses Docker Buildx for caching
- Validates Docker images build successfully
- Updated test-summary to check validation results

**PR Template:**
- New pull request template with comprehensive checklist
- Specific section for worker-related changes
- Reminds contributors to validate worker files
- Includes documentation and changelog reminders

These checks would have caught the secrets worker gitignore issue.

Implements Phase 1 improvements from CI/CD quality assessment.

* fix: add dev branch to test workflow triggers

The test workflow was configured for 'develop' but the actual branch is named 'dev'.
This caused tests not to run on PRs to dev branch.

Now tests will run on:
- PRs to: main, master, dev, develop
- Pushes to: main, master, dev, develop, feature/**

* fix: properly detect worker file changes in CI

The previous condition used invalid GitHub context field.
Now uses git diff to properly detect changes to workers/ or docker-compose.yml.

Behavior:
- Job always runs the check step
- Detects if workers/ or docker-compose.yml modified
- Only builds Docker images if workers actually changed
- Shows clear skip message when no worker changes detected

* feat: Add Python SAST workflow with three security analysis tools

Implements Issue #5 - Python SAST workflow that combines:
- Dependency scanning (pip-audit) for CVE detection
- Security linting (Bandit) for vulnerability patterns
- Type checking (Mypy) for type safety issues

## Changes

**New Modules:**
- `DependencyScanner`: Scans Python dependencies for known CVEs using pip-audit
- `BanditAnalyzer`: Analyzes Python code for security issues using Bandit
- `MypyAnalyzer`: Checks Python code for type safety issues using Mypy

**New Workflow:**
- `python_sast`: Temporal workflow that orchestrates all three SAST tools
  - Runs tools in parallel for fast feedback (3-5 min vs hours for fuzzing)
  - Generates unified SARIF report with findings from all tools
  - Supports configurable severity/confidence thresholds

**Updates:**
- Added SAST dependencies to Python worker (bandit, pip-audit, mypy)
- Updated module __init__.py files to export new analyzers
- Added type_errors.py test file to vulnerable_app for Mypy validation

## Testing

Workflow tested successfully on vulnerable_app:
-  Bandit: Detected 9 security issues (command injection, unsafe functions)
-  Mypy: Detected 5 type errors
-  DependencyScanner: Ran successfully (no CVEs in test dependencies)
-  SARIF export: Generated valid SARIF with 14 total findings

* fix: Remove unused imports to pass linter

* fix: resolve live monitoring bug, remove deprecated parameters, and auto-start Python worker

- Fix live monitoring style error by calling _live_monitor() helper directly
- Remove default_parameters duplication from 10 workflow metadata files
- Remove deprecated volume_mode parameter from 26 files across CLI, SDK, backend, and docs
- Configure Python worker to start automatically with docker compose up
- Clean up constants, validation, completion, and example files

Fixes #
- Live monitoring now works correctly with --live flag
- Workflow metadata follows JSON Schema standard
- Cleaner codebase without deprecated volume_mode
- Python worker (most commonly used) starts by default

* fix: resolve linter errors and optimize CI worker builds

- Remove unused Literal import from backend findings model
- Remove unnecessary f-string prefixes in CLI findings command
- Optimize GitHub Actions to build only modified workers
  - Detect specific worker changes (python, secrets, rust, android, ossfuzz)
  - Build only changed workers instead of all 5
  - Build all workers if docker-compose.yml changes
  - Significantly reduces CI build time

* 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

* fix(android): correct activity names and MobSF API key generation

- Fix activity names in workflow.py (get_target, upload_results, cleanup_cache)
- Fix MobSF API key generation in Dockerfile startup script (cut delimiter)
- Update activity parameter signatures to match actual implementations
- Workflow now executes successfully with Jadx and OpenGrep

* feat: add platform-aware worker architecture with ARM64 support

Implement platform-specific Dockerfile selection and graceful tool degradation to support both x86_64 and ARM64 (Apple Silicon) platforms.

**Backend Changes:**
- Add system info API endpoint (/system/info) exposing host filesystem paths
- Add FUZZFORGE_HOST_ROOT environment variable to backend service
- Add graceful degradation in MobSF activity for ARM64 platforms

**CLI Changes:**
- Implement multi-strategy path resolution (backend API, .fuzzforge marker, env var)
- Add platform detection (linux/amd64 vs linux/arm64)
- Add worker metadata.yaml reading for platform capabilities
- Auto-select appropriate Dockerfile based on detected platform
- Pass platform-specific env vars to docker-compose

**Worker Changes:**
- Create workers/android/metadata.yaml defining platform capabilities
- Rename Dockerfile -> Dockerfile.amd64 (full toolchain with MobSF)
- Create Dockerfile.arm64 (excludes MobSF due to Rosetta 2 incompatibility)
- Update docker-compose.yml to use ${ANDROID_DOCKERFILE} variable

**Workflow Changes:**
- Handle MobSF "skipped" status gracefully in workflow
- Log clear warnings when tools are unavailable on platform

**Key Features:**
- Automatic platform detection and Dockerfile selection
- Graceful degradation when tools unavailable (MobSF on ARM64)
- Works from any directory (backend API provides paths)
- Manual override via environment variables
- Clear user feedback about platform and selected Dockerfile

**Benefits:**
- Android workflow now works on Apple Silicon Macs
- No code changes needed for other workflows
- Convention established for future platform-specific workers

Closes: MobSF Rosetta 2 incompatibility issue
Implements: Platform-aware worker architecture (Option B)

* fix: make MobSFScanner import conditional for ARM64 compatibility

- Add try-except block to conditionally import MobSFScanner in modules/android/__init__.py
- Allows Android worker to start on ARM64 without MobSF dependencies (aiohttp)
- MobSF activity gracefully skips on ARM64 with clear warning message
- Remove workflow path detection logic (not needed - workflows receive directories)

Platform-aware architecture fully functional on ARM64:
- CLI detects ARM64 and selects Dockerfile.arm64 automatically
- Worker builds and runs without MobSF on ARM64
- Jadx successfully decompiles APKs (4145 files from BeetleBug.apk)
- OpenGrep finds security vulnerabilities (8 issues found)
- MobSF gracefully skips with warning on ARM64
- Graceful degradation working as designed

Tested with:
  ff workflow run android_static_analysis test_projects/android_test/ \
    --wait --no-interactive apk_path=BeetleBug.apk decompile_apk=true

Results: 8 security findings (1 ERROR, 7 WARNINGS)

* docs: update CHANGELOG with Android workflow and ARM64 support

Added [Unreleased] section documenting:
- Android Static Analysis Workflow (Jadx, OpenGrep, MobSF)
- Platform-Aware Worker Architecture with ARM64 support
- Python SAST Workflow
- CI/CD improvements and worker validation
- CLI enhancements
- Bug fixes and technical changes

Fixed date typo: 2025-01-16 → 2025-10-16

* fix: resolve linter errors in Android modules

- Remove unused imports from mobsf_scanner.py (asyncio, hashlib, json, Optional)
- Remove unused variables from opengrep_android.py (start_col, end_col)
- Remove duplicate Path import from workflow.py

* ci: support multi-platform Dockerfiles in worker validation

Updated worker validation script to accept both:
- Single Dockerfile pattern (existing workers)
- Multi-platform Dockerfile pattern (Dockerfile.amd64, Dockerfile.arm64, etc.)

This enables platform-aware worker architectures like the Android worker
which uses different Dockerfiles for x86_64 and ARM64 platforms.

* Feature/litellm proxy (#27)

* feat: seed governance config and responses routing

* Add env-configurable timeout for proxy providers

* Integrate LiteLLM OTEL collector and update docs

* Make .env.litellm optional for LiteLLM proxy

* Add LiteLLM proxy integration with model-agnostic virtual keys

Changes:
- Bootstrap generates 3 virtual keys with individual budgets (CLI: $100, Task-Agent: $25, Cognee: $50)
- Task-agent loads config at runtime via entrypoint script to wait for bootstrap completion
- All keys are model-agnostic by default (no LITELLM_DEFAULT_MODELS restrictions)
- Bootstrap handles database/env mismatch after docker prune by deleting stale aliases
- CLI and Cognee configured to use LiteLLM proxy with virtual keys
- Added comprehensive documentation in volumes/env/README.md

Technical details:
- task-agent entrypoint waits for keys in .env file before starting uvicorn
- Bootstrap creates/updates TASK_AGENT_API_KEY, COGNEE_API_KEY, and OPENAI_API_KEY
- Removed hardcoded API keys from docker-compose.yml
- All services route through http://localhost:10999 proxy

* Fix CLI not loading virtual keys from global .env

Project .env files with empty OPENAI_API_KEY values were overriding
the global virtual keys. Updated _load_env_file_if_exists to only
override with non-empty values.

* Fix agent executor not passing API key to LiteLLM

The agent was initializing LiteLlm without api_key or api_base,
causing authentication errors when using the LiteLLM proxy. Now
reads from OPENAI_API_KEY/LLM_API_KEY and LLM_ENDPOINT environment
variables and passes them to LiteLlm constructor.

* Auto-populate project .env with virtual key from global config

When running 'ff init', the command now checks for a global
volumes/env/.env file and automatically uses the OPENAI_API_KEY
virtual key if found. This ensures projects work with LiteLLM
proxy out of the box without manual key configuration.

* docs: Update README with LiteLLM configuration instructions

Add note about LITELLM_GEMINI_API_KEY configuration and clarify that OPENAI_API_KEY default value should not be changed as it's used for the LLM proxy.

* Refactor workflow parameters to use JSON Schema defaults

Consolidates parameter defaults into JSON Schema format, removing the separate default_parameters field. Adds extract_defaults_from_json_schema() helper to extract defaults from the standard schema structure. Updates LiteLLM proxy config to use LITELLM_OPENAI_API_KEY environment variable.

* Remove .env.example from task_agent

* Fix MDX syntax error in llm-proxy.md

* fix: apply default parameters from metadata.yaml automatically

Fixed TemporalManager.run_workflow() to correctly apply default parameter
values from workflow metadata.yaml files when parameters are not provided
by the caller.

Previous behavior:
- When workflow_params was empty {}, the condition
  `if workflow_params and 'parameters' in metadata` would fail
- Parameters would not be extracted from schema, resulting in workflows
  receiving only target_id with no other parameters

New behavior:
- Removed the `workflow_params and` requirement from the condition
- Now explicitly checks for defaults in parameter spec
- Applies defaults from metadata.yaml automatically when param not provided
- Workflows receive all parameters with proper fallback:
  provided value > metadata default > None

This makes metadata.yaml the single source of truth for parameter defaults,
removing the need for workflows to implement defensive default handling.

Affected workflows:
- llm_secret_detection (was failing with KeyError)
- All other workflows now benefit from automatic default application

Co-authored-by: tduhamel42 <tduhamel@fuzzinglabs.com>

* fix: add default values to llm_analysis workflow parameters

Resolves validation error where agent_url was None when not explicitly provided. The TemporalManager applies defaults from metadata.yaml, not from module input schemas, so all parameters need defaults in the workflow metadata.

Changes:
- Add default agent_url, llm_model (gpt-5-mini), llm_provider (openai)
- Expand file_patterns to 45 comprehensive patterns covering code, configs, secrets, and Docker files
- Increase default limits: max_files (10), max_file_size (100KB), timeout (90s)

* refactor: replace .env.example with .env.template in documentation

- Remove volumes/env/.env.example file
- Update all documentation references to use .env.template instead
- Update bootstrap script error message
- Update .gitignore comment

* feat(cli): add worker management commands with improved progress feedback

Add comprehensive CLI commands for managing Temporal workers:
- ff worker list - List workers with status and uptime
- ff worker start <name> - Start specific worker with optional rebuild
- ff worker stop - Safely stop all workers without affecting core services

Improvements:
- Live progress display during worker startup with Rich Status spinner
- Real-time elapsed time counter and container state updates
- Health check status tracking (starting → unhealthy → healthy)
- Helpful contextual hints at 10s, 30s, 60s intervals
- Better timeout messages showing last known state

Worker management enhancements:
- Use 'docker compose' (space) instead of 'docker-compose' (hyphen)
- Stop workers individually with 'docker stop' to avoid stopping core services
- Platform detection and Dockerfile selection (ARM64/AMD64)

Documentation:
- Updated docker-setup.md with CLI commands as primary method
- Created comprehensive cli-reference.md with all commands and examples
- Added worker management best practices

* fix: MobSF scanner now properly parses files dict structure

MobSF returns 'files' as a dict (not list):
{"filename": "line_numbers"}

The parser was treating it as a list, causing zero findings
to be extracted. Now properly iterates over the dict and
creates one finding per affected file with correct line numbers
and metadata (CWE, OWASP, MASVS, CVSS).

Fixed in both code_analysis and behaviour sections.

* chore: bump version to 0.7.3

* docs: fix broken documentation links in cli-reference

* chore: add worker startup documentation and cleanup .gitignore

- Add workflow-to-worker mapping tables across documentation
- Update troubleshooting guide with worker requirements section
- Enhance getting started guide with worker examples
- Add quick reference to docker setup guide
- Add WEEK_SUMMARY*.md pattern to .gitignore

* docs: update CHANGELOG with missing versions and recent changes

- Add Unreleased section for post-v0.7.3 documentation updates
- Add v0.7.2 entry with bug fixes and worker improvements
- Document that v0.7.1 was re-tagged as v0.7.2
- Fix v0.6.0 date to "Undocumented" (no tag exists)
- Add version comparison links for easier navigation

* chore: bump all package versions to 0.7.3 for consistency

* Update GitHub link to fuzzforge_ai

---------

Co-authored-by: Songbird99 <150154823+Songbird99@users.noreply.github.com>
Co-authored-by: Songbird <Songbirdx99@gmail.com>
This commit is contained in:
tduhamel42
2025-11-06 11:07:50 +01:00
committed by GitHub
parent f6cdb1ae2e
commit 943bc9a114
112 changed files with 8358 additions and 371 deletions
@@ -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,
}