Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion tempo/agents/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from tempo.tools.registry import ToolRegistry
from tempo.prompts.builder import PromptBuilder
from tempo.infra.protocols import FindingsUpdater
from tempo.tools.network_capture import NetworkRequestCapture


@register_agent
Expand Down Expand Up @@ -108,6 +109,7 @@ def create_tools(
def create_browser_use_agent(
self,
container: "ContainerManager | None" = None,
network_capture: "NetworkRequestCapture | None" = None,
):
"""
Create a browser-use Agent with custom tempo-sec tools.
Expand All @@ -117,9 +119,10 @@ def create_browser_use_agent(

Args:
container: Container manager for shell/file operations
network_capture: Optional NetworkRequestCapture for network request visibility

Returns:
Tuple of (browser-use Agent instance, Browser instance, Tools instance)
Tuple of (browser-use Agent instance, Browser instance, Tools instance, NetworkRequestCapture or None)
"""
from browser_use import Agent, Browser
from browser_use.tools.service import Tools
Expand Down Expand Up @@ -148,6 +151,7 @@ def create_browser_use_agent(
imap_config=self.imap_config,
agent_email=self.agent_email,
tools_instance=shared_tools, # Pass shared instance
network_capture=network_capture, # Pass network capture for tool registration
)

# Select LLM based on configuration
Expand Down
25 changes: 25 additions & 0 deletions tempo/prompts/templates/browser_base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,31 @@ Review prior work to avoid duplication and build on existing knowledge.
- Use `file_write` to create test scripts, payloads, or save extracted data
- Use findings tools to document discoveries for the security assessment
- Use `read_findings` to check what's already been tested
- Use `get_network_requests` to inspect API calls made by the page (see below)

## Network Request Analysis

Use `get_network_requests` to inspect XHR/fetch requests made by the web application:

**When to use:**
- Analyzing API endpoints and request/response patterns
- Capturing authentication tokens (bearer tokens, API keys, session cookies)
- Understanding client-side API interactions on modern SPAs
- Finding hidden API endpoints not visible in the page source
- Bypassing WAF restrictions by replaying legitimate browser requests

**Parameters:**
- `filter_type`: Filter by "xhr", "fetch", or "all" (default: both xhr and fetch)
- `include_bodies`: Set to `true` to include request/response bodies (useful for API analysis)
- `max_requests`: Maximum requests to return (default: 30)

**Note:** If output is truncated, the full data is saved to a file. Use `file_read` on the saved path to access complete request history.

**Example workflow:**
1. Navigate to a page that makes API calls
2. Call `get_network_requests()` to see captured requests
3. If you need request/response bodies, call `get_network_requests(include_bodies=true)`
4. Use captured tokens in `shell` commands for direct API testing

## Common Testing Scenarios

Expand Down
34 changes: 33 additions & 1 deletion tempo/tools/browser_task_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ async def execute(
import uuid
import os
from tempo.agents.browser import BrowserAgent
from tempo.tools.network_capture import NetworkRequestCapture

# Fixed step limit for browser agent
max_steps = 50
Expand All @@ -102,6 +103,12 @@ async def execute(
"password": imap_password,
}

# Create network request capture for API traffic visibility
network_capture = NetworkRequestCapture(
output_dir="/workspace",
capture_response_bodies=True, # Queue bodies for on-demand fetch via get_network_requests(include_bodies=True)
)

# Create browser agent definition
browser_agent_def = BrowserAgent(
task=task,
Expand All @@ -114,11 +121,29 @@ async def execute(
use_browser_use_llm=use_browser_use_llm,
)

# Create browser-use agent with custom tools
# Create browser-use agent with custom tools (including network capture tool)
browser_use_agent, browser, combined_tools = browser_agent_def.create_browser_use_agent(
container=self._container_manager,
network_capture=network_capture,
)

# Attach network capture to browser session after it's available
# Note: The browser session exists on the agent, but CDP is set up during run()
# We'll attach after the browser session starts via a background hook
async def attach_network_capture_after_start():
"""Wait for browser session to have CDP client, then attach network capture."""
for _ in range(50): # Wait up to 5 seconds
if hasattr(browser_use_agent, 'browser_session'):
session = browser_use_agent.browser_session
if session and getattr(session, '_cdp_client_root', None):
await network_capture.attach(session)
return
await asyncio.sleep(0.1)

# Start the attachment in background (will complete during agent.run())
import asyncio
attach_task = asyncio.create_task(attach_network_capture_after_start())

# --- Visualization bridge for browser-use actions ---
# browser-use executes browser interactions via combined_tools.registry.execute_action(...)
# The Tempo visualization UI expects events via VisualizationCollector.on_step/on_tool_call_start/on_tool_call.
Expand Down Expand Up @@ -322,6 +347,13 @@ async def _execute_action_with_viz(*args, **kwargs):
)

finally:
# Clean up background attach task if still running
try:
if 'attach_task' in locals() and not attach_task.done():
attach_task.cancel()
except Exception:
pass

# Clean up browser resources
try:
await browser.close()
Expand Down
60 changes: 60 additions & 0 deletions tempo/tools/browser_use_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def _run_async(coro: Coroutine[None, None, T]) -> T:
if TYPE_CHECKING:
from tempo.infra.container import ContainerManager
from tempo.infra.protocols import FindingsUpdater
from tempo.tools.network_capture import NetworkRequestCapture


class BrowserUseToolsAdapter:
Expand All @@ -48,6 +49,7 @@ def __init__(
imap_config: dict | None = None,
agent_email: str | None = None,
tools_instance: Tools | None = None,
network_capture: "NetworkRequestCapture | None" = None,
):
"""
Initialize the tools adapter.
Expand All @@ -58,18 +60,21 @@ def __init__(
imap_config: IMAP configuration for email tools
agent_email: Email address for verification flows
tools_instance: Optional shared Tools instance (if None, creates new)
network_capture: Optional NetworkRequestCapture for network requests
"""
self.container = container_manager
self.findings = findings_updater
self.imap_config = imap_config or {}
self.agent_email = agent_email
self.tools = tools_instance if tools_instance is not None else Tools()
self.network_capture = network_capture

# Register all custom tools
self._register_findings_tools()
self._register_shell_tools()
self._register_filesystem_tools()
self._register_email_tools()
self._register_network_tools()

def _register_findings_tools(self):
"""Register findings management tools."""
Expand Down Expand Up @@ -377,6 +382,61 @@ def get_email_content(message_id: str) -> str:
return f"Error: {result.error}"
return result.output

def _register_network_tools(self):
"""Register network request capture tools."""
if not self.network_capture:
return

network_capture = self.network_capture

@self.tools.action(
"Get captured network requests (XHR/fetch) from the current page"
)
def get_network_requests(
filter_type: str = "",
include_bodies: bool = False,
max_requests: int = 30,
) -> str:
"""
Get captured XHR/fetch network requests from the current page.

Use this to analyze API calls made by the web application:
- View API endpoints and request/response patterns
- Capture authentication tokens and headers for security testing
- Understand client-side API interactions

Note: Requests are saved on page navigation. Use file_read on the
saved file path to access complete request history if output is truncated.

Args:
filter_type: Filter by type: "xhr", "fetch", "all" (default: xhr+fetch)
include_bodies: Include request/response bodies (use for API analysis)
max_requests: Maximum requests to return (default: 30)

Returns:
Formatted list of network requests with headers and optional bodies
"""
# Use async version if bodies requested (to fetch via CDP)
if include_bodies:
output, saved_file = _run_async(
network_capture.get_requests_async(
filter_type=filter_type,
include_bodies=include_bodies,
max_requests=max_requests,
)
)
else:
output, saved_file = network_capture.get_requests(
filter_type=filter_type,
include_bodies=include_bodies,
max_requests=max_requests,
)

if saved_file:
return f"{output}\n\nFull data saved to: {saved_file}"
return output

def get_tools(self) -> Tools:
"""Return the browser-use Tools instance."""
return self.tools

Loading
Loading