Skip to content
Closed
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
12 changes: 11 additions & 1 deletion pkg/agents/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,20 @@ func AllFlags() []cli.Flag {

// ConfigureAll initializes all configured agents and returns a slice of ToolSets.
// Agents that are not configured will return nil and be skipped.
func ConfigureAll(ctx context.Context) ([]interfaces.ToolSet, error) {
//
// storageClient and storagePrefix are the warren-wide shared storage client
// (bucket already bound) and object-key prefix. They are injected into any
// factory implementing StorageAware before Configure is called. storageClient
// may be nil when storage is not configured; storage-aware agents must degrade
// gracefully in that case.
func ConfigureAll(ctx context.Context, storageClient interfaces.StorageClient, storagePrefix string) ([]interfaces.ToolSet, error) {
var toolSets []interfaces.ToolSet

for _, factory := range All {
if aware, ok := factory.(StorageAware); ok {
aware.SetStorage(storageClient, storagePrefix)
}

ts, err := factory.Configure(ctx)
if err != nil {
return nil, goerr.Wrap(err, "failed to configure agent")
Expand Down
9 changes: 9 additions & 0 deletions pkg/agents/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ type ToolSetFactory interface {
// Returns (nil, nil) if the agent is not configured.
Configure(ctx context.Context) (interfaces.ToolSet, error)
}

// StorageAware is an optional interface for factories that need the
// warren-wide storage client and prefix (e.g. for snapshotting large
// result sets to shared object storage). ConfigureAll injects these
// before calling Configure. Factories that do not need storage simply
// omit this interface.
type StorageAware interface {
SetStorage(client interfaces.StorageClient, prefix string)
}
9 changes: 9 additions & 0 deletions pkg/agents/falcon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ The agent exposes the following read-only tools to the LLM:

> **Note:** Event search uses the Next-Gen SIEM Search API, which runs queries asynchronously. The tool handles job creation and polling internally — results are returned once the search completes.

#### Pagination and result limits

`falcon_search_events` returns events in pages to avoid flooding the agent with large result sets:

- At most **100 events** are returned per call (`limit`, max 100). The response includes `total` (number of events in the result set), `offset`, `returned`, and `has_more`.
- A filter query returns at most **200 events by default**. To retrieve more (up to **20,000**), append `| tail(N)` to the query string. For the exact number of matching events, use an aggregation such as `| count()` rather than paging through raw events.
- The first response includes a `result_set_id`. To fetch later pages, call the tool again with that `result_set_id` and an increased `offset` — this serves pages from a stored snapshot **without re-running the query**, so pagination is stable and cheap.
- Snapshots are written to the configured Cloud Storage bucket (shared `--storage-bucket` / `--storage-prefix`) under `falcon/events/`. When storage is not configured, only the first page is available (no `result_set_id`). Snapshot lifetime is governed by the bucket's object lifecycle (TTL) policy.

## Troubleshooting

### Agent not appearing in available tools
Expand Down
31 changes: 28 additions & 3 deletions pkg/agents/falcon/export_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package falcon

import "context"
import (
"context"

"github.com/secmon-lab/warren/pkg/domain/interfaces"
)

// TokenProviderForTest wraps tokenProvider for testing purposes.
type TokenProviderForTest struct {
Expand Down Expand Up @@ -29,11 +33,21 @@ type InternalToolForTest struct {
tool *internalTool
}

// NewInternalToolForTest creates an internalTool wrapper for testing.
// NewInternalToolForTest creates an internalTool wrapper for testing without
// storage (event search returns the first page directly).
func NewInternalToolForTest(clientID, clientSecret, baseURL string) *InternalToolForTest {
tp := newTokenProvider(clientID, clientSecret, baseURL)
return &InternalToolForTest{
tool: newInternalTool(tp, baseURL),
tool: newInternalTool(tp, baseURL, nil, ""),
}
}

// NewInternalToolForTestWithStorage creates an internalTool wrapper backed by
// the given storage client, enabling result-set snapshotting and pagination.
func NewInternalToolForTestWithStorage(clientID, clientSecret, baseURL string, storage interfaces.StorageClient, prefix string) *InternalToolForTest {
tp := newTokenProvider(clientID, clientSecret, baseURL)
return &InternalToolForTest{
tool: newInternalTool(tp, baseURL, storage, prefix),
}
}

Expand All @@ -50,3 +64,14 @@ func (t *InternalToolForTest) SpecCount(ctx context.Context) (int, error) {
}
return len(specs), nil
}

// ParseLimit exposes parseLimit for testing.
func ParseLimit(args map[string]any) int { return parseLimit(args) }

// ParseOffset exposes parseOffset for testing.
func ParseOffset(args map[string]any) int { return parseOffset(args) }

// Paginate exposes paginate for testing.
func Paginate(events []any, offset, limit int) ([]any, int, bool) {
return paginate(events, offset, limit)
}
13 changes: 12 additions & 1 deletion pkg/agents/falcon/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ type Factory struct {
clientID string
clientSecret string
baseURL string

storageClient interfaces.StorageClient
storagePrefix string
}

// SetStorage implements agents.StorageAware. It receives the warren-wide
// shared storage client and prefix, used to snapshot large event result
// sets for stable pagination.
func (f *Factory) SetStorage(client interfaces.StorageClient, prefix string) {
f.storageClient = client
f.storagePrefix = prefix
}

// Flags implements agents.ToolSetFactory.
Expand Down Expand Up @@ -66,6 +77,6 @@ func (f *Factory) Configure(ctx context.Context) (interfaces.ToolSet, error) {
)

return &toolSet{
internal: newInternalTool(tp, baseURL),
internal: newInternalTool(tp, baseURL, f.storageClient, f.storagePrefix),
}, nil
}
9 changes: 9 additions & 0 deletions pkg/agents/falcon/prompt/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ CQL is used with `falcon_search_events` to query raw EDR telemetry data. CQL is
- PowerShell executions: `FileName="powershell.exe" AND #event_simpleName=ProcessRollup2 | tail(50)`
- Events by agent ID in last 24h: `aid=abc123` (use start="1d" parameter)

### Result Limits and Pagination

`falcon_search_events` caps how many events it returns so the context is not flooded:

- A filter (non-aggregate) query returns at most **200 events by default**. To retrieve more (up to **20,000**), append `| tail(N)` to the query string (e.g. `... | tail(5000)`). The tool does NOT add `tail` for you.
- When you only need the count of matching events, use an aggregation such as `| count()` instead of paging through raw events — it returns the exact total in a single call.
- Each call returns at most **100 events** (`limit`, max 100). The response includes `total`, `offset`, `returned`, `has_more`, and a `result_set_id`.
- To page through a larger result set, call again with the same `result_set_id` and an increased `offset`. This serves the next page from a stored snapshot without re-running the query, so paging is stable and cheap. Do not re-issue the original query just to get later pages.

## Standard Investigation Workflow

### 1. Understand the Request
Expand Down
Loading
Loading