package agents
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
// CoordinatorAgent routes findings to specialized agents
type CoordinatorAgent struct {
OllamaURL string
Model string
timeout time.Duration
// Fast keyword-based pre-classification
classifierRules map[string]AgentType
}
// NewCoordinatorAgent creates a new coordinator agent
func NewCoordinatorAgent(ollamaURL, fastModel string) *CoordinatorAgent {
ca := &CoordinatorAgent{
OllamaURL: ollamaURL,
Model: fastModel,
timeout: 30 * time.Second, // Increased for local LLM
classifierRules: make(map[string]AgentType),
}
// Initialize fast classification rules (keyword -> agent type)
ca.initClassifierRules()
return ca
}
// initClassifierRules sets up keyword-based fast classification
func (ca *CoordinatorAgent) initClassifierRules() {
// XSS indicators
xssKeywords := []string{
"script", "onerror", "onclick", "onload", "onmouseover", "onfocus",
"innerHTML", "document.write", "document.cookie", "eval(", "alert(",
"= 0.7 {
// High confidence keyword match - skip LLM
return agentType, score, fmt.Sprintf("Fast classification: found %s indicators", agentType)
}
// Step 2: LLM-based classification for ambiguous cases
if score < 0.5 {
llmType, llmConf, reason := ca.llmClassify(ctx, finding)
if llmConf > score {
return llmType, llmConf, reason
}
}
// Return best fast match or general
if score >= 0.3 {
return agentType, score, "Partial keyword match"
}
return AgentTypeGeneral, 0.5, "No specific classification - using general agent"
}
// fastClassify performs keyword-based classification
func (ca *CoordinatorAgent) fastClassify(finding Finding) (AgentType, float64) {
// Step 1: Type-based fast routing (highest priority)
switch strings.ToLower(finding.Type) {
case "javascript":
// JS findings go to secrets first (API keys, tokens), then XSS
if containsAny(finding.Context, []string{"api_key", "apikey", "secret", "token", "password", "akia", "sk_live", "pk_live", "ghp_"}) {
return AgentTypeSecrets, 0.9
}
return AgentTypeXSS, 0.8
case "http":
// HTTP responses go to headers agent
return AgentTypeHeaders, 0.8
case "technology":
// Technology findings go to crypto (for version/vuln analysis)
return AgentTypeCrypto, 0.8
case "api":
return AgentTypeAPI, 0.9
case "security_issue":
// Security issues need general analysis
return AgentTypeGeneral, 0.8
case "takeover":
// Takeover is auth-related
return AgentTypeAuth, 0.9
}
// Step 2: Keyword-based classification for untyped findings
content := strings.ToLower(finding.Context + " " + finding.URL + " " + finding.Type + " " + finding.Technology)
for k, v := range finding.Headers {
content += " " + strings.ToLower(k) + ":" + strings.ToLower(v)
}
// Count matches per agent type
scores := make(map[AgentType]int)
totalMatches := 0
for keyword, agentType := range ca.classifierRules {
if strings.Contains(content, strings.ToLower(keyword)) {
scores[agentType]++
totalMatches++
}
}
if totalMatches == 0 {
return AgentTypeGeneral, 0.5 // Default with moderate confidence
}
// Find agent with highest score
var bestAgent AgentType
var bestScore int
for agent, score := range scores {
if score > bestScore {
bestScore = score
bestAgent = agent
}
}
// Calculate confidence (more matches = higher confidence)
confidence := 0.5
if bestScore >= 5 {
confidence = 0.9
} else if bestScore >= 3 {
confidence = 0.75
} else if bestScore >= 2 {
confidence = 0.65
} else if bestScore >= 1 {
confidence = 0.55
}
return bestAgent, confidence
}
// llmClassify uses the LLM for complex classification
func (ca *CoordinatorAgent) llmClassify(ctx context.Context, finding Finding) (AgentType, float64, string) {
prompt := fmt.Sprintf(`Classify this security finding into exactly ONE category. Respond with ONLY the category name and confidence.
Finding Type: %s
URL: %s
Technology: %s
Content Sample: %s
Categories:
- xss (Cross-Site Scripting, DOM manipulation, script injection)
- sqli (SQL Injection, database queries, SQL errors)
- auth (Authentication, sessions, tokens, authorization, IDOR)
- api (REST/GraphQL APIs, CORS, rate limiting)
- crypto (TLS/SSL, encryption, certificates, hashing)
- secrets (API keys, credentials, passwords, tokens in code)
- headers (HTTP security headers, CSP, HSTS, cookies)
- general (none of the above)
Response format: CATEGORY:confidence
Example: sqli:85`,
finding.Type,
finding.URL,
finding.Technology,
truncateStr(finding.Context, 500))
response, err := ca.queryOllama(ctx, prompt)
if err != nil {
return AgentTypeGeneral, 0.5, "LLM classification failed"
}
// Parse response
response = strings.TrimSpace(strings.ToLower(response))
parts := strings.Split(response, ":")
if len(parts) >= 1 {
category := strings.TrimSpace(parts[0])
confidence := 0.6 // Default confidence
if len(parts) >= 2 {
var conf float64
fmt.Sscanf(parts[1], "%f", &conf)
if conf > 1 {
conf = conf / 100
}
if conf > 0 && conf <= 1 {
confidence = conf
}
}
agentType := parseAgentType(category)
return agentType, confidence, fmt.Sprintf("LLM classified as %s", agentType)
}
return AgentTypeGeneral, 0.5, "Could not parse LLM response"
}
// DetermineHandoffs checks if additional agents should analyze the finding
func (ca *CoordinatorAgent) DetermineHandoffs(finding Finding, primaryResult *AgentResult) []AgentType {
var handoffs []AgentType
// Define handoff rules
switch primaryResult.AgentType {
case AgentTypeAPI:
// API findings often have auth issues
if containsAny(finding.Context, []string{"401", "403", "unauthorized", "forbidden"}) {
handoffs = append(handoffs, AgentTypeAuth)
}
// CORS issues often relate to XSS
if containsAny(finding.Context, []string{"cors", "access-control"}) {
handoffs = append(handoffs, AgentTypeXSS)
}
case AgentTypeAuth:
// Auth pages may have XSS
if containsAny(finding.Context, []string{"