fix: make bounty module portable across platforms and LLM providers#3
fix: make bounty module portable across platforms and LLM providers#3
Conversation
Co-authored-by: romainsantoli-web <246739959+romainsantoli-web@users.noreply.github.com>
|
@copilot Fait un test et vérifie les changements |
35/35 tests passed, lint clean ✅ Breakdown of the 15 new tests added for the changes:
All pre-existing 20 tests continue to pass unchanged. |
|
@copilot fait le pr puis le main |
Je n'ai pas la possibilité de fusionner une PR ni de pousser directement sur La PR est prête : 35/35 tests passent et le lint est propre. Tu peux la merger toi-même en cliquant sur "Merge pull request" sur la page de la PR. |
There was a problem hiding this comment.
Pull request overview
This PR makes the bounty module portable across platforms and LLM providers by replacing three hard-coded macOS-specific assumptions: a Homebrew-pinned httpx binary path, Unix-only ffuf wordlist locations, and baked-in LLM model names with no override mechanism. All three are now resolved via environment variable overrides with sensible platform-aware fallback chains.
Changes:
scanner.py: Added_resolve_httpx_bin()and_resolve_ffuf_wordlist()functions that resolve tool paths via env vars → PATH lookup → platform-specific fallbacks, replacing hard-coded macOS paths. Movedssl/socketimports to module level and cleaned up unused imports.factory.py: Added_resolve_model()to support per-agent (FIRM_<NAME>_MODEL) and global (FIRM_DEFAULT_MODEL) env var overrides for LLM model names, wired into all 8 agent specs.- Test files: Added comprehensive unit tests for all three resolution functions and their integration with the scanning tools.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/firm/bounty/tools/scanner.py |
Replaced hard-coded httpx path and ffuf wordlist with portable resolution functions; moved ssl/socket to module-level imports; cleaned up unused imports. |
src/firm/bounty/factory.py |
Added _resolve_model() for env-var-based LLM model overrides; wired into all BOUNTY_AGENTS entries; updated module docstring. |
tests/test_bounty_tools.py |
Added TestResolveHttpxBin and TestResolveFfufWordlist test classes covering env var, PATH, and fallback resolution. |
tests/test_bounty_factory.py |
Added TestResolveModel test class covering per-agent, global, and default model resolution. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| BOUNTY_AGENTS: list[AgentSpec] = [ | ||
| AgentSpec( | ||
| name="hunt-director", | ||
| model="claude-sonnet-4-20250514", | ||
| model=_resolve_model("hunt-director", "claude-sonnet-4-20250514"), | ||
| initial_authority=0.90, | ||
| description="Campaign coordinator — plans phases, assigns targets, synthesises results.", | ||
| ), | ||
| AgentSpec( | ||
| name="recon-agent", | ||
| model="gpt-4.1", | ||
| model=_resolve_model("recon-agent", "gpt-4.1"), | ||
| initial_authority=0.70, | ||
| description="Reconnaissance specialist — subdomains, ports, tech stack, URL crawling.", | ||
| ), | ||
| AgentSpec( | ||
| name="web-hunter", | ||
| model="claude-sonnet-4-20250514", | ||
| model=_resolve_model("web-hunter", "claude-sonnet-4-20250514"), | ||
| initial_authority=0.65, | ||
| description="Web vulnerability hunter — SQLi, XSS, SSRF, IDOR on web apps.", | ||
| ), | ||
| AgentSpec( | ||
| name="api-hunter", | ||
| model="gpt-4o", | ||
| model=_resolve_model("api-hunter", "gpt-4o"), | ||
| initial_authority=0.65, | ||
| description="API vulnerability hunter — auth bypass, BOLA, rate limiting, GraphQL.", | ||
| ), | ||
| AgentSpec( | ||
| name="code-auditor", | ||
| model="o4-mini", | ||
| model=_resolve_model("code-auditor", "o4-mini"), | ||
| initial_authority=0.60, | ||
| description="Static code auditor — semgrep, pattern-based detection, dependency audit.", | ||
| ), | ||
| AgentSpec( | ||
| name="mobile-hunter", | ||
| model="claude-sonnet-4-20250514", | ||
| model=_resolve_model("mobile-hunter", "claude-sonnet-4-20250514"), | ||
| initial_authority=0.55, | ||
| description="Mobile app security — APK/IPA analysis, certificate pinning, local storage.", | ||
| ), | ||
| AgentSpec( | ||
| name="web3-hunter", | ||
| model="gpt-4.1", | ||
| model=_resolve_model("web3-hunter", "gpt-4.1"), | ||
| initial_authority=0.55, | ||
| description="Smart contract / blockchain security — reentrancy, flash loans, oracle manipulation.", | ||
| ), | ||
| AgentSpec( | ||
| name="report-writer", | ||
| model="gpt-4o", | ||
| model=_resolve_model("report-writer", "gpt-4o"), | ||
| initial_authority=0.40, | ||
| description="Report writer — crafts clear, detailed H1 reports with reproduction steps.", | ||
| ), |
There was a problem hiding this comment.
_resolve_model() is called at module import time for each agent in BOUNTY_AGENTS. This means env var overrides (FIRM_*_MODEL, FIRM_DEFAULT_MODEL) are "frozen" at first import. For the documented CLI workflow (env vars set before firm bounty ...), this works because cli.py uses lazy imports. However, programmatic callers who set env vars and then call create_bounty_firm() after the module has already been imported won't see the updated models. Consider deferring model resolution into create_bounty_firm() so it always reads current env vars, or document this limitation explicitly.
| monkeypatch.setenv("HTTPX_BIN", "/custom/httpx") | ||
| assert _resolve_httpx_bin() == "/custom/httpx" | ||
|
|
||
| def test_env_var_empty_falls_through(self, monkeypatch, tmp_path): |
There was a problem hiding this comment.
The tmp_path fixture parameter is declared but never used in this test. It can be removed.
| def test_env_var_empty_falls_through(self, monkeypatch, tmp_path): | |
| def test_env_var_empty_falls_through(self, monkeypatch): |
| _DEFAULT_FFUF_WORDLISTS: list[str] = [ | ||
| "/usr/share/seclists/Discovery/Web-Content/common.txt", # Debian/Ubuntu | ||
| "/usr/share/SecLists/Discovery/Web-Content/common.txt", # some distros | ||
| str(Path.home() / "SecLists/Discovery/Web-Content/common.txt"), # macOS/user install | ||
| "/opt/homebrew/share/seclists/Discovery/Web-Content/common.txt", # Homebrew | ||
| ] |
There was a problem hiding this comment.
Path.home() is now called at module import time (as part of _DEFAULT_FFUF_WORDLISTS), whereas previously the equivalent Path.home() call was inside the scan_ffuf function body and only executed when that tool was invoked. If Path.home() raises a RuntimeError (e.g., in a containerized environment with no HOME set), it will now prevent the entire scanner module from being imported, rather than just causing that one tool call to fail. Consider computing this path lazily inside _resolve_ffuf_wordlist() instead, or wrapping it with a try/except at module level.
The bounty module had three hard-coded, macOS-specific assumptions making it non-portable: a Homebrew-pinned
httpxbinary path, Unix-onlyffufwordlist locations, and baked-in LLM model names with no override mechanism.Changes
scanner.py— portable tool resolution_resolve_httpx_bin():HTTPX_BINenv var →shutil.which("httpx-toolkit")→shutil.which("httpx")→ Homebrew path as last resort_resolve_ffuf_wordlist(): explicit arg →FFUF_WORDLISTenv var → 4 platform-aware default paths (Debian/Ubuntu, Red Hat, macOS Homebrew,~/SecLists)import ssl/import socketto module levelfactory.py— configurable LLM models_resolve_model(agent_name, default_model)wired into all 8BOUNTY_AGENTSFIRM_<AGENT_NAME_UPPER>_MODEL(e.g.FIRM_HUNT_DIRECTOR_MODEL)FIRM_DEFAULT_MODEL# Run the full bounty firm on Linux with a custom model and wordlist FIRM_DEFAULT_MODEL=gpt-4o \ FFUF_WORDLIST=/opt/wordlists/common.txt \ HTTPX_BIN=/usr/local/bin/httpx \ firm bounty run --target example.com💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.