mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-05-20 00:54:40 +02:00
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
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
Python SAST Workflow Activities
|
||||
|
||||
Activities specific to the Python SAST workflow:
|
||||
- scan_dependencies_activity: Scan Python dependencies for CVEs using pip-audit
|
||||
- analyze_with_bandit_activity: Analyze Python code for security issues using Bandit
|
||||
- analyze_with_mypy_activity: Analyze Python code for type safety using Mypy
|
||||
- generate_python_sast_sarif_activity: Generate SARIF report from all findings
|
||||
"""
|
||||
|
||||
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="scan_dependencies")
|
||||
async def scan_dependencies_activity(workspace_path: str, config: dict) -> dict:
|
||||
"""
|
||||
Scan Python dependencies for known vulnerabilities using pip-audit.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to the workspace directory
|
||||
config: DependencyScanner configuration
|
||||
|
||||
Returns:
|
||||
Scanner results dictionary
|
||||
"""
|
||||
logger.info(f"Activity: scan_dependencies (workspace={workspace_path})")
|
||||
|
||||
try:
|
||||
from modules.scanner import DependencyScanner
|
||||
|
||||
workspace = Path(workspace_path)
|
||||
if not workspace.exists():
|
||||
raise FileNotFoundError(f"Workspace not found: {workspace_path}")
|
||||
|
||||
scanner = DependencyScanner()
|
||||
result = await scanner.execute(config, workspace)
|
||||
|
||||
logger.info(
|
||||
f"✓ Dependency scanning completed: "
|
||||
f"{result.summary.get('total_vulnerabilities', 0)} vulnerabilities found"
|
||||
)
|
||||
return result.dict()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Dependency scanning failed: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@activity.defn(name="analyze_with_bandit")
|
||||
async def analyze_with_bandit_activity(workspace_path: str, config: dict) -> dict:
|
||||
"""
|
||||
Analyze Python code for security issues using Bandit.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to the workspace directory
|
||||
config: BanditAnalyzer configuration
|
||||
|
||||
Returns:
|
||||
Analysis results dictionary
|
||||
"""
|
||||
logger.info(f"Activity: analyze_with_bandit (workspace={workspace_path})")
|
||||
|
||||
try:
|
||||
from modules.analyzer import BanditAnalyzer
|
||||
|
||||
workspace = Path(workspace_path)
|
||||
if not workspace.exists():
|
||||
raise FileNotFoundError(f"Workspace not found: {workspace_path}")
|
||||
|
||||
analyzer = BanditAnalyzer()
|
||||
result = await analyzer.execute(config, workspace)
|
||||
|
||||
logger.info(
|
||||
f"✓ Bandit analysis completed: "
|
||||
f"{result.summary.get('total_issues', 0)} security issues found"
|
||||
)
|
||||
return result.dict()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Bandit analysis failed: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@activity.defn(name="analyze_with_mypy")
|
||||
async def analyze_with_mypy_activity(workspace_path: str, config: dict) -> dict:
|
||||
"""
|
||||
Analyze Python code for type safety issues using Mypy.
|
||||
|
||||
Args:
|
||||
workspace_path: Path to the workspace directory
|
||||
config: MypyAnalyzer configuration
|
||||
|
||||
Returns:
|
||||
Analysis results dictionary
|
||||
"""
|
||||
logger.info(f"Activity: analyze_with_mypy (workspace={workspace_path})")
|
||||
|
||||
try:
|
||||
from modules.analyzer import MypyAnalyzer
|
||||
|
||||
workspace = Path(workspace_path)
|
||||
if not workspace.exists():
|
||||
raise FileNotFoundError(f"Workspace not found: {workspace_path}")
|
||||
|
||||
analyzer = MypyAnalyzer()
|
||||
result = await analyzer.execute(config, workspace)
|
||||
|
||||
logger.info(
|
||||
f"✓ Mypy analysis completed: "
|
||||
f"{result.summary.get('total_errors', 0)} type errors found"
|
||||
)
|
||||
return result.dict()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Mypy analysis failed: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@activity.defn(name="generate_python_sast_sarif")
|
||||
async def generate_python_sast_sarif_activity(
|
||||
dependency_results: dict,
|
||||
bandit_results: dict,
|
||||
mypy_results: dict,
|
||||
config: dict,
|
||||
workspace_path: str
|
||||
) -> dict:
|
||||
"""
|
||||
Generate SARIF report from all SAST analysis results.
|
||||
|
||||
Args:
|
||||
dependency_results: Results from dependency scanner
|
||||
bandit_results: Results from Bandit analyzer
|
||||
mypy_results: Results from Mypy analyzer
|
||||
config: Reporter configuration
|
||||
workspace_path: Path to the workspace
|
||||
|
||||
Returns:
|
||||
SARIF report dictionary
|
||||
"""
|
||||
logger.info("Activity: generate_python_sast_sarif")
|
||||
|
||||
try:
|
||||
from modules.reporter import SARIFReporter
|
||||
|
||||
workspace = Path(workspace_path)
|
||||
|
||||
# Combine findings from all modules
|
||||
all_findings = []
|
||||
|
||||
# Add dependency scanner findings
|
||||
dependency_findings = dependency_results.get("findings", [])
|
||||
all_findings.extend(dependency_findings)
|
||||
|
||||
# Add Bandit findings
|
||||
bandit_findings = bandit_results.get("findings", [])
|
||||
all_findings.extend(bandit_findings)
|
||||
|
||||
# Add Mypy findings
|
||||
mypy_findings = mypy_results.get("findings", [])
|
||||
all_findings.extend(mypy_findings)
|
||||
|
||||
# Prepare reporter config
|
||||
reporter_config = {
|
||||
**config,
|
||||
"findings": all_findings,
|
||||
"tool_name": "FuzzForge Python SAST",
|
||||
"tool_version": "1.0.0"
|
||||
}
|
||||
|
||||
reporter = SARIFReporter()
|
||||
result = await reporter.execute(reporter_config, workspace)
|
||||
|
||||
# Extract SARIF from result
|
||||
sarif = result.dict().get("sarif", {})
|
||||
|
||||
logger.info(f"✓ SARIF report generated with {len(all_findings)} findings")
|
||||
return sarif
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SARIF report generation failed: {e}", exc_info=True)
|
||||
raise
|
||||
@@ -0,0 +1,116 @@
|
||||
name: python_sast
|
||||
version: "1.0.0"
|
||||
vertical: python
|
||||
description: "Python Static Application Security Testing (SAST) workflow combining dependency scanning (pip-audit), security linting (Bandit), and type checking (Mypy)"
|
||||
author: "FuzzForge Team"
|
||||
tags:
|
||||
- "python"
|
||||
- "sast"
|
||||
- "security"
|
||||
- "type-checking"
|
||||
- "dependencies"
|
||||
- "bandit"
|
||||
- "mypy"
|
||||
- "pip-audit"
|
||||
- "sarif"
|
||||
|
||||
# Workspace isolation mode (system-level configuration)
|
||||
# Using "shared" mode for read-only SAST analysis (no file modifications)
|
||||
workspace_isolation: "shared"
|
||||
|
||||
default_parameters:
|
||||
dependency_config: {}
|
||||
bandit_config: {}
|
||||
mypy_config: {}
|
||||
reporter_config: {}
|
||||
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
dependency_config:
|
||||
type: object
|
||||
description: "Dependency scanner (pip-audit) configuration"
|
||||
properties:
|
||||
dependency_files:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: "List of dependency files to scan (auto-discovered if empty)"
|
||||
default: []
|
||||
ignore_vulns:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: "List of vulnerability IDs to ignore"
|
||||
default: []
|
||||
bandit_config:
|
||||
type: object
|
||||
description: "Bandit security analyzer configuration"
|
||||
properties:
|
||||
severity_level:
|
||||
type: string
|
||||
enum: ["low", "medium", "high"]
|
||||
description: "Minimum severity level to report"
|
||||
default: "low"
|
||||
confidence_level:
|
||||
type: string
|
||||
enum: ["low", "medium", "high"]
|
||||
description: "Minimum confidence level to report"
|
||||
default: "medium"
|
||||
exclude_tests:
|
||||
type: boolean
|
||||
description: "Exclude test files from analysis"
|
||||
default: true
|
||||
skip_ids:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: "List of Bandit test IDs to skip"
|
||||
default: []
|
||||
mypy_config:
|
||||
type: object
|
||||
description: "Mypy type checker configuration"
|
||||
properties:
|
||||
strict_mode:
|
||||
type: boolean
|
||||
description: "Enable strict type checking"
|
||||
default: false
|
||||
ignore_missing_imports:
|
||||
type: boolean
|
||||
description: "Ignore errors about missing imports"
|
||||
default: true
|
||||
follow_imports:
|
||||
type: string
|
||||
enum: ["normal", "silent", "skip", "error"]
|
||||
description: "How to handle imports"
|
||||
default: "silent"
|
||||
reporter_config:
|
||||
type: object
|
||||
description: "SARIF reporter configuration"
|
||||
properties:
|
||||
include_code_flows:
|
||||
type: boolean
|
||||
description: "Include code flow information"
|
||||
default: false
|
||||
|
||||
output_schema:
|
||||
type: object
|
||||
properties:
|
||||
sarif:
|
||||
type: object
|
||||
description: "SARIF-formatted SAST findings from all tools"
|
||||
summary:
|
||||
type: object
|
||||
description: "SAST execution summary"
|
||||
properties:
|
||||
total_findings:
|
||||
type: integer
|
||||
vulnerabilities:
|
||||
type: integer
|
||||
description: "CVEs found in dependencies"
|
||||
security_issues:
|
||||
type: integer
|
||||
description: "Security issues found by Bandit"
|
||||
type_errors:
|
||||
type: integer
|
||||
description: "Type errors found by Mypy"
|
||||
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
Python SAST Workflow - Temporal Version
|
||||
|
||||
Static Application Security Testing for Python projects using multiple tools.
|
||||
"""
|
||||
|
||||
# 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 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 PythonSastWorkflow:
|
||||
"""
|
||||
Python Static Application Security Testing workflow.
|
||||
|
||||
This workflow:
|
||||
1. Downloads target from MinIO
|
||||
2. Runs dependency scanning (pip-audit for CVEs)
|
||||
3. Runs security linting (Bandit for security issues)
|
||||
4. Runs type checking (Mypy for type safety)
|
||||
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,
|
||||
dependency_config: Optional[Dict[str, Any]] = None,
|
||||
bandit_config: Optional[Dict[str, Any]] = None,
|
||||
mypy_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 in MinIO
|
||||
dependency_config: Configuration for dependency scanner
|
||||
bandit_config: Configuration for Bandit analyzer
|
||||
mypy_config: Configuration for Mypy analyzer
|
||||
reporter_config: Configuration for SARIF reporter
|
||||
|
||||
Returns:
|
||||
Dictionary containing SARIF report and summary
|
||||
"""
|
||||
workflow_id = workflow.info().workflow_id
|
||||
|
||||
workflow.logger.info(
|
||||
f"Starting PythonSASTWorkflow "
|
||||
f"(workflow_id={workflow_id}, target_id={target_id})"
|
||||
)
|
||||
|
||||
# Default configurations
|
||||
if not dependency_config:
|
||||
dependency_config = {
|
||||
"dependency_files": [], # Auto-discover
|
||||
"ignore_vulns": []
|
||||
}
|
||||
|
||||
if not bandit_config:
|
||||
bandit_config = {
|
||||
"severity_level": "low",
|
||||
"confidence_level": "medium",
|
||||
"exclude_tests": True,
|
||||
"skip_ids": []
|
||||
}
|
||||
|
||||
if not mypy_config:
|
||||
mypy_config = {
|
||||
"strict_mode": False,
|
||||
"ignore_missing_imports": True,
|
||||
"follow_imports": "silent"
|
||||
}
|
||||
|
||||
if not reporter_config:
|
||||
reporter_config = {
|
||||
"include_code_flows": False
|
||||
}
|
||||
|
||||
results = {
|
||||
"workflow_id": workflow_id,
|
||||
"target_id": target_id,
|
||||
"status": "running",
|
||||
"steps": []
|
||||
}
|
||||
|
||||
try:
|
||||
# Get run ID for workspace isolation (using shared mode for read-only analysis)
|
||||
run_id = workflow.info().run_id
|
||||
|
||||
# Step 1: Download target from MinIO
|
||||
workflow.logger.info("Step 1: Downloading target from MinIO")
|
||||
target_path = await workflow.execute_activity(
|
||||
"get_target",
|
||||
args=[target_id, run_id, "shared"], # target_id, run_id, workspace_isolation
|
||||
start_to_close_timeout=timedelta(minutes=5),
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=1),
|
||||
maximum_interval=timedelta(seconds=30),
|
||||
maximum_attempts=3
|
||||
)
|
||||
)
|
||||
results["steps"].append({
|
||||
"step": "download_target",
|
||||
"status": "success",
|
||||
"target_path": target_path
|
||||
})
|
||||
workflow.logger.info(f"✓ Target downloaded to: {target_path}")
|
||||
|
||||
# Step 2: Dependency scanning (pip-audit)
|
||||
workflow.logger.info("Step 2: Scanning dependencies for vulnerabilities")
|
||||
dependency_results = await workflow.execute_activity(
|
||||
"scan_dependencies",
|
||||
args=[target_path, dependency_config],
|
||||
start_to_close_timeout=timedelta(minutes=10),
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=2),
|
||||
maximum_interval=timedelta(seconds=60),
|
||||
maximum_attempts=2
|
||||
)
|
||||
)
|
||||
results["steps"].append({
|
||||
"step": "dependency_scanning",
|
||||
"status": "success",
|
||||
"vulnerabilities": dependency_results.get("summary", {}).get("total_vulnerabilities", 0)
|
||||
})
|
||||
workflow.logger.info(
|
||||
f"✓ Dependency scanning completed: "
|
||||
f"{dependency_results.get('summary', {}).get('total_vulnerabilities', 0)} vulnerabilities"
|
||||
)
|
||||
|
||||
# Step 3: Security linting (Bandit)
|
||||
workflow.logger.info("Step 3: Analyzing security issues with Bandit")
|
||||
bandit_results = await workflow.execute_activity(
|
||||
"analyze_with_bandit",
|
||||
args=[target_path, bandit_config],
|
||||
start_to_close_timeout=timedelta(minutes=10),
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=2),
|
||||
maximum_interval=timedelta(seconds=60),
|
||||
maximum_attempts=2
|
||||
)
|
||||
)
|
||||
results["steps"].append({
|
||||
"step": "bandit_analysis",
|
||||
"status": "success",
|
||||
"issues": bandit_results.get("summary", {}).get("total_issues", 0)
|
||||
})
|
||||
workflow.logger.info(
|
||||
f"✓ Bandit analysis completed: "
|
||||
f"{bandit_results.get('summary', {}).get('total_issues', 0)} security issues"
|
||||
)
|
||||
|
||||
# Step 4: Type checking (Mypy)
|
||||
workflow.logger.info("Step 4: Type checking with Mypy")
|
||||
mypy_results = await workflow.execute_activity(
|
||||
"analyze_with_mypy",
|
||||
args=[target_path, mypy_config],
|
||||
start_to_close_timeout=timedelta(minutes=10),
|
||||
retry_policy=RetryPolicy(
|
||||
initial_interval=timedelta(seconds=2),
|
||||
maximum_interval=timedelta(seconds=60),
|
||||
maximum_attempts=2
|
||||
)
|
||||
)
|
||||
results["steps"].append({
|
||||
"step": "mypy_analysis",
|
||||
"status": "success",
|
||||
"type_errors": mypy_results.get("summary", {}).get("total_errors", 0)
|
||||
})
|
||||
workflow.logger.info(
|
||||
f"✓ Mypy analysis completed: "
|
||||
f"{mypy_results.get('summary', {}).get('total_errors', 0)} type errors"
|
||||
)
|
||||
|
||||
# Step 5: Generate SARIF report
|
||||
workflow.logger.info("Step 5: Generating SARIF report")
|
||||
sarif_report = await workflow.execute_activity(
|
||||
"generate_python_sast_sarif",
|
||||
args=[dependency_results, bandit_results, mypy_results, reporter_config, target_path],
|
||||
start_to_close_timeout=timedelta(minutes=5)
|
||||
)
|
||||
results["steps"].append({
|
||||
"step": "report_generation",
|
||||
"status": "success"
|
||||
})
|
||||
|
||||
# Count total findings in SARIF
|
||||
total_findings = 0
|
||||
if sarif_report and "runs" in sarif_report:
|
||||
total_findings = len(sarif_report["runs"][0].get("results", []))
|
||||
|
||||
workflow.logger.info(f"✓ SARIF report generated with {total_findings} findings")
|
||||
|
||||
# Step 6: Upload results to MinIO
|
||||
workflow.logger.info("Step 6: Uploading results")
|
||||
try:
|
||||
results_url = await workflow.execute_activity(
|
||||
"upload_results",
|
||||
args=[workflow_id, sarif_report, "sarif"],
|
||||
start_to_close_timeout=timedelta(minutes=2)
|
||||
)
|
||||
results["results_url"] = results_url
|
||||
workflow.logger.info(f"✓ Results uploaded to: {results_url}")
|
||||
except Exception as e:
|
||||
workflow.logger.warning(f"Failed to upload results: {e}")
|
||||
results["results_url"] = None
|
||||
|
||||
# Step 7: Cleanup cache
|
||||
workflow.logger.info("Step 7: Cleaning up cache")
|
||||
try:
|
||||
await workflow.execute_activity(
|
||||
"cleanup_cache",
|
||||
args=[target_path, "shared"], # target_path, workspace_isolation
|
||||
start_to_close_timeout=timedelta(minutes=1)
|
||||
)
|
||||
workflow.logger.info("✓ Cache cleaned up (skipped for shared mode)")
|
||||
except Exception as e:
|
||||
workflow.logger.warning(f"Cache cleanup failed: {e}")
|
||||
|
||||
# Mark workflow as successful
|
||||
results["status"] = "success"
|
||||
results["sarif"] = sarif_report
|
||||
results["summary"] = {
|
||||
"total_findings": total_findings,
|
||||
"vulnerabilities": dependency_results.get("summary", {}).get("total_vulnerabilities", 0),
|
||||
"security_issues": bandit_results.get("summary", {}).get("total_issues", 0),
|
||||
"type_errors": mypy_results.get("summary", {}).get("total_errors", 0)
|
||||
}
|
||||
workflow.logger.info(f"✓ Workflow completed successfully: {workflow_id}")
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
workflow.logger.error(f"Workflow failed: {e}")
|
||||
results["status"] = "error"
|
||||
results["error"] = str(e)
|
||||
results["steps"].append({
|
||||
"step": "error",
|
||||
"status": "failed",
|
||||
"error": str(e)
|
||||
})
|
||||
raise
|
||||
Reference in New Issue
Block a user