From 9374fd3aee4086716bb547e473cee42db572f4e8 Mon Sep 17 00:00:00 2001 From: AFredefon Date: Tue, 7 Apr 2026 01:50:21 +0200 Subject: [PATCH] feat: implement workflow suggestions pipeline --- .../src/fuzzforge_common/hub/models.py | 14 + .../src/fuzzforge_common/hub/registry.py | 31 + fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py | 28 +- hub-config.json | 567 +++++++++++++++++- 4 files changed, 638 insertions(+), 2 deletions(-) diff --git a/fuzzforge-common/src/fuzzforge_common/hub/models.py b/fuzzforge-common/src/fuzzforge_common/hub/models.py index f0df8bd..e583e83 100644 --- a/fuzzforge-common/src/fuzzforge_common/hub/models.py +++ b/fuzzforge-common/src/fuzzforge_common/hub/models.py @@ -294,3 +294,17 @@ class HubConfig(BaseModel): default=True, description="Cache discovered tools", ) + + #: Workflow hints indexed by "after:" keys. + #: Loaded inline or merged from workflow_hints_file. + workflow_hints: dict[str, Any] = Field( + default_factory=dict, + description="Workflow hints indexed by 'after:'", + ) + + #: Optional path to an external workflow-hints.json file. + #: Relative paths are resolved relative to the hub-config.json location. + workflow_hints_file: str | None = Field( + default=None, + description="Path to an external workflow-hints.json to load and merge", + ) diff --git a/fuzzforge-common/src/fuzzforge_common/hub/registry.py b/fuzzforge-common/src/fuzzforge_common/hub/registry.py index ccbdd48..d93e6c6 100644 --- a/fuzzforge-common/src/fuzzforge_common/hub/registry.py +++ b/fuzzforge-common/src/fuzzforge_common/hub/registry.py @@ -87,6 +87,28 @@ class HubRegistry: config=server_config, ) + # Load and merge external workflow hints file if specified. + if self._config.workflow_hints_file: + hints_path = Path(self._config.workflow_hints_file) + if not hints_path.is_absolute(): + hints_path = config_path.parent / hints_path + if hints_path.exists(): + try: + with hints_path.open() as hf: + hints_data = json.load(hf) + self._config.workflow_hints.update(hints_data.get("hints", {})) + logger.info( + "Loaded workflow hints", + path=str(hints_path), + hints=len(self._config.workflow_hints), + ) + except Exception as hints_err: + logger.warning( + "Failed to load workflow hints file", + path=str(hints_path), + error=str(hints_err), + ) + logger.info( "Loaded hub configuration", path=str(config_path), @@ -218,6 +240,15 @@ class HubRegistry: server.discovery_error = None server.tools = tools + def get_workflow_hint(self, tool_name: str) -> dict | None: + """Get the workflow hint for a tool by name. + + :param tool_name: Tool name (e.g. ``binwalk_extract``). + :returns: Hint dict for the ``after:`` key, or None. + + """ + return self._config.workflow_hints.get(f"after:{tool_name}") or None + def get_all_tools(self) -> list: """Get all discovered tools from all servers. diff --git a/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py b/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py index dbdf491..6f7a59e 100644 --- a/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py +++ b/fuzzforge-mcp/src/fuzzforge_mcp/tools/hub.py @@ -291,7 +291,33 @@ async def execute_hub_tool( except Exception: # noqa: BLE001, S110 - never fail the tool call due to recording issues pass - return result.to_dict() + # Scan for new artifacts produced by the tool in /app/output. + response = result.to_dict() + try: + storage = get_storage() + project_path = get_project_path() + new_artifacts = storage.scan_artifacts( + project_path=project_path, + server_name=result.server_name, + tool_name=result.tool_name, + ) + if new_artifacts: + response["artifacts"] = [ + {"path": a["path"], "type": a["type"], "size": a["size"]} + for a in new_artifacts + ] + except Exception: # noqa: BLE001, S110 - never fail the tool call due to artifact scanning + pass + + # Append workflow suggestions based on hints configured for this tool. + try: + hint = executor.registry.get_workflow_hint(result.tool_name) + if hint: + response["suggested_next_steps"] = hint + except Exception: # noqa: BLE001, S110 - never fail the tool call due to hint lookup + pass + + return response except Exception as e: if isinstance(e, ToolError): diff --git a/hub-config.json b/hub-config.json index 1bc90ec..c99e0d8 100644 --- a/hub-config.json +++ b/hub-config.json @@ -1 +1,566 @@ -{"servers": []} +{ + "servers": [ + { + "name": "bloodhound-mcp", + "description": "bloodhound-mcp \u2014 active-directory", + "type": "docker", + "image": "bloodhound-mcp:latest", + "category": "active-directory", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "binwalk-mcp", + "description": "binwalk-mcp \u2014 binary-analysis", + "type": "docker", + "image": "binwalk-mcp:latest", + "category": "binary-analysis", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "capa-mcp", + "description": "capa-mcp \u2014 binary-analysis", + "type": "docker", + "image": "capa-mcp:latest", + "category": "binary-analysis", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "ghidra-mcp", + "description": "ghidra-mcp \u2014 binary-analysis", + "type": "docker", + "image": "ghidra-mcp:latest", + "category": "binary-analysis", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "ida-mcp", + "description": "ida-mcp \u2014 binary-analysis", + "type": "docker", + "image": "ida-mcp:latest", + "category": "binary-analysis", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "radare2-mcp", + "description": "radare2-mcp \u2014 binary-analysis", + "type": "docker", + "image": "radare2-mcp:latest", + "category": "binary-analysis", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "yara-mcp", + "description": "yara-mcp \u2014 binary-analysis", + "type": "docker", + "image": "yara-mcp:latest", + "category": "binary-analysis", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "daml-viewer-mcp", + "description": "daml-viewer-mcp \u2014 blockchain", + "type": "docker", + "image": "daml-viewer-mcp:latest", + "category": "blockchain", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "medusa-mcp", + "description": "medusa-mcp \u2014 blockchain", + "type": "docker", + "image": "medusa-mcp:latest", + "category": "blockchain", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "solazy-mcp", + "description": "solazy-mcp \u2014 blockchain", + "type": "docker", + "image": "solazy-mcp:latest", + "category": "blockchain", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "prowler-mcp", + "description": "prowler-mcp \u2014 cloud-security", + "type": "docker", + "image": "prowler-mcp:latest", + "category": "cloud-security", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "roadrecon-mcp", + "description": "roadrecon-mcp \u2014 cloud-security", + "type": "docker", + "image": "roadrecon-mcp:latest", + "category": "cloud-security", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "trivy-mcp", + "description": "trivy-mcp \u2014 cloud-security", + "type": "docker", + "image": "trivy-mcp:latest", + "category": "cloud-security", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "semgrep-mcp", + "description": "semgrep-mcp \u2014 code-security", + "type": "docker", + "image": "semgrep-mcp:latest", + "category": "code-security", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "searchsploit-mcp", + "description": "searchsploit-mcp \u2014 exploitation", + "type": "docker", + "image": "searchsploit-mcp:latest", + "category": "exploitation", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "boofuzz-mcp", + "description": "boofuzz-mcp \u2014 fuzzing", + "type": "docker", + "image": "boofuzz-mcp:latest", + "category": "fuzzing", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "dharma-mcp", + "description": "dharma-mcp \u2014 fuzzing", + "type": "docker", + "image": "dharma-mcp:latest", + "category": "fuzzing", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "dnstwist-mcp", + "description": "dnstwist-mcp \u2014 osint", + "type": "docker", + "image": "dnstwist-mcp:latest", + "category": "osint", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "maigret-mcp", + "description": "maigret-mcp \u2014 osint", + "type": "docker", + "image": "maigret-mcp:latest", + "category": "osint", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "hashcat-mcp", + "description": "hashcat-mcp \u2014 password-cracking", + "type": "docker", + "image": "hashcat-mcp:latest", + "category": "password-cracking", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "externalattacker-mcp", + "description": "externalattacker-mcp \u2014 reconnaissance", + "type": "docker", + "image": "externalattacker-mcp:latest", + "category": "reconnaissance", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "masscan-mcp", + "description": "masscan-mcp \u2014 reconnaissance", + "type": "docker", + "image": "masscan-mcp:latest", + "category": "reconnaissance", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "networksdb-mcp", + "description": "networksdb-mcp \u2014 reconnaissance", + "type": "docker", + "image": "networksdb-mcp:latest", + "category": "reconnaissance", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "nmap-mcp", + "description": "nmap-mcp \u2014 reconnaissance", + "type": "docker", + "image": "nmap-mcp:latest", + "category": "reconnaissance", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "pd-tools-mcp", + "description": "pd-tools-mcp \u2014 reconnaissance", + "type": "docker", + "image": "pd-tools-mcp:latest", + "category": "reconnaissance", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "shodan-mcp", + "description": "shodan-mcp \u2014 reconnaissance", + "type": "docker", + "image": "shodan-mcp:latest", + "category": "reconnaissance", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "whatweb-mcp", + "description": "whatweb-mcp \u2014 reconnaissance", + "type": "docker", + "image": "whatweb-mcp:latest", + "category": "reconnaissance", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "zoomeye-mcp", + "description": "zoomeye-mcp \u2014 reconnaissance", + "type": "docker", + "image": "zoomeye-mcp:latest", + "category": "reconnaissance", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "gitleaks-mcp", + "description": "gitleaks-mcp \u2014 secrets", + "type": "docker", + "image": "gitleaks-mcp:latest", + "category": "secrets", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "otx-mcp", + "description": "otx-mcp \u2014 threat-intel", + "type": "docker", + "image": "otx-mcp:latest", + "category": "threat-intel", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "virustotal-mcp", + "description": "virustotal-mcp \u2014 threat-intel", + "type": "docker", + "image": "virustotal-mcp:latest", + "category": "threat-intel", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "burp-mcp", + "description": "burp-mcp \u2014 web-security", + "type": "docker", + "image": "burp-mcp:latest", + "category": "web-security", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "ffuf-mcp", + "description": "ffuf-mcp \u2014 web-security", + "type": "docker", + "image": "ffuf-mcp:latest", + "category": "web-security", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "nikto-mcp", + "description": "nikto-mcp \u2014 web-security", + "type": "docker", + "image": "nikto-mcp:latest", + "category": "web-security", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "nuclei-mcp", + "description": "nuclei-mcp \u2014 web-security", + "type": "docker", + "image": "nuclei-mcp:latest", + "category": "web-security", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "sqlmap-mcp", + "description": "sqlmap-mcp \u2014 web-security", + "type": "docker", + "image": "sqlmap-mcp:latest", + "category": "web-security", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "waybackurls-mcp", + "description": "waybackurls-mcp \u2014 web-security", + "type": "docker", + "image": "waybackurls-mcp:latest", + "category": "web-security", + "capabilities": [ + "NET_RAW" + ], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "go-analyzer-mcp", + "description": "Go static analysis: fuzzable entry points, existing Fuzz* targets, unsafe/cgo usage, CVE scanning via govulncheck", + "type": "docker", + "image": "go-analyzer-mcp:latest", + "category": "code-security", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "go-harness-tester-mcp", + "description": "Test Go fuzz harness quality: compilation, seed execution, fuzzing trial, quality scoring 0-100", + "type": "docker", + "image": "go-harness-tester-mcp:latest", + "category": "code-security", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "go-fuzzer-mcp", + "description": "Run Go native fuzzing (go test -fuzz) with blocking and continuous modes, crash collection, session management", + "type": "docker", + "image": "go-fuzzer-mcp:latest", + "category": "code-security", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + }, + { + "name": "go-crash-analyzer-mcp", + "description": "Analyze Go fuzzing crashes: reproduce, classify (nil-deref, OOR, panic, race, etc.), deduplicate by stack signature", + "type": "docker", + "image": "go-crash-analyzer-mcp:latest", + "category": "code-security", + "capabilities": [], + "volumes": [ + "/home/afredefon/.fuzzforge/hub/workspace:/data" + ], + "enabled": true, + "source_hub": "mcp-security-hub" + } + ], + "workflow_hints_file": "mcp-security-hub/workflow-hints.json" +} \ No newline at end of file