Skip to main content
Callback functions allow you to process scan results in real-time as they are discovered by the Nuclei engine.

Overview

Callbacks are invoked for each vulnerability or finding detected during a scan. They provide a way to:
  • Process results in real-time
  • Store results in custom formats
  • Send alerts or notifications
  • Implement custom filtering logic
  • Aggregate or analyze findings

Callback signature

type ResultCallback func(event *output.ResultEvent)
The callback receives a single parameter: a pointer to output.ResultEvent containing all details about the finding.

ResultEvent structure

The ResultEvent contains comprehensive information about each finding:
type ResultEvent struct {
	// Template information
	TemplateID      string
	TemplatePath    string
	Info            Info

	// Target information
	Host            string
	Matched         string
	MatchedAt       string

	// Request/Response data
	Type            string
	Request         string
	Response        string
	ExtractedResults []string
	Metadata        map[string]interface{}

	// Timestamps
	Timestamp       time.Time

	// Additional fields
	IP              string
	CURLCommand     string
}

Key fields

TemplateID
string
Unique identifier of the template that matched
TemplatePath
string
File path of the template
Info
Info
Template metadata including name, severity, tags, description, etc.
Host
string
Target host where the finding was discovered
Matched
string
The exact matched content or pattern
MatchedAt
string
The location or URL where the match occurred
Type
string
Protocol type (http, dns, tcp, etc.)
Request
string
The raw request that was sent
Response
string
The raw response that was received
ExtractedResults
[]string
Data extracted by template extractors
Metadata
map[string]interface{}
Additional metadata from the template
Timestamp
time.Time
When the finding was discovered
IP
string
Resolved IP address of the target
CURLCommand
string
CURL command to reproduce the request

Info structure

The Info field contains template metadata:
type Info struct {
	Name        string
	Author      []string
	Severity    severity.Severity
	Description string
	Reference   []string
	Tags        []string
	Metadata    map[string]interface{}
}

Using callbacks

Single callback

Pass a single callback function to process all results:
import "github.com/projectdiscovery/nuclei/v3/pkg/output"

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	fmt.Printf("[%s] %s found on %s\n",
		event.Info.Severity,
		event.TemplateID,
		event.Host,
	)
})

Multiple callbacks

You can pass multiple callback functions:
callback1 := func(event *output.ResultEvent) {
	// Log to console
	fmt.Printf("Found: %s\n", event.TemplateID)
}

callback2 := func(event *output.ResultEvent) {
	// Save to database
	saveToDatabase(event)
}

err := ne.ExecuteWithCallback(callback1, callback2)

No callback (default output)

Passing nil uses the default JSON output to stdout:
err := ne.ExecuteWithCallback(nil)

Callback examples

Simple console output

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	fmt.Printf("[%s] %s\n", event.TemplateID, event.Host)
})

Detailed output with severity

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	fmt.Printf("[%s] [%s] %s found on %s\n",
		time.Now().Format("15:04:05"),
		event.Info.Severity,
		event.Info.Name,
		event.Host,
	)
})

Filter by severity

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	if event.Info.Severity == "critical" || event.Info.Severity == "high" {
		fmt.Printf("HIGH PRIORITY: %s on %s\n", event.TemplateID, event.Host)
		sendAlert(event)
	}
})

Collect all results

var results []*output.ResultEvent
var mu sync.Mutex

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	mu.Lock()
	defer mu.Unlock()
	results = append(results, event)
})

fmt.Printf("Total findings: %d\n", len(results))

Save to JSON file

import (
	"encoding/json"
	"os"
)

file, _ := os.Create("results.json")
defer file.Close()
encoder := json.NewEncoder(file)

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	encoder.Encode(event)
})

Send webhook notifications

import (
	"bytes"
	"encoding/json"
	"net/http"
)

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	if event.Info.Severity == "critical" {
		data, _ := json.Marshal(event)
		http.Post(
			"https://webhook.example.com/alerts",
			"application/json",
			bytes.NewBuffer(data),
		)
	}
})

Group by severity

resultsBySeverity := make(map[string]int)
var mu sync.Mutex

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	mu.Lock()
	defer mu.Unlock()
	sev := string(event.Info.Severity)
	resultsBySeverity[sev]++
})

for severity, count := range resultsBySeverity {
	fmt.Printf("%s: %d findings\n", severity, count)
}

Custom formatting

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	fmt.Printf(`
=== Finding ===
Template: %s (%s)
Severity: %s
Target: %s
Matched: %s
Time: %s
`,
		event.Info.Name,
		event.TemplateID,
		event.Info.Severity,
		event.Host,
		event.Matched,
		event.Timestamp.Format(time.RFC3339),
	)
})

Store in database

import "database/sql"

db, _ := sql.Open("postgres", connStr)

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	_, err := db.Exec(`
		INSERT INTO vulnerabilities (template_id, host, severity, timestamp)
		VALUES ($1, $2, $3, $4)
	`,
		event.TemplateID,
		event.Host,
		event.Info.Severity,
		event.Timestamp,
	)
	if err != nil {
		log.Printf("DB error: %v", err)
	}
})

Real-time dashboard updates

import "github.com/gorilla/websocket"

var wsConn *websocket.Conn // WebSocket connection to dashboard

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	data, _ := json.Marshal(map[string]interface{}{
		"type":     "finding",
		"template": event.TemplateID,
		"host":     event.Host,
		"severity": event.Info.Severity,
	})
	wsConn.WriteMessage(websocket.TextMessage, data)
})

Thread-safe callbacks

For ThreadSafeNucleiEngine, use GlobalResultCallback:
ne, _ := nuclei.NewThreadSafeNucleiEngine()

ne.GlobalResultCallback(func(event *output.ResultEvent) {
	// This callback will be used for all concurrent scans
	fmt.Printf("[%s] %s\n", event.TemplateID, event.Host)
})

// Run multiple scans
go ne.ExecuteNucleiWithOpts([]string{"target1.com"}, opts...)
go ne.ExecuteNucleiWithOpts([]string{"target2.com"}, opts...)
When using GlobalResultCallback with concurrent scans, ensure your callback is thread-safe. Use mutexes when accessing shared state.

Best practices

Keep callbacks fast - avoid blocking operations
Use goroutines for slow operations (database writes, API calls)
Add mutex locks when modifying shared state
Handle errors gracefully within callbacks
Log callback errors instead of panicking
Consider buffered channels for high-volume results

Common patterns

Buffered channel pattern

resultChan := make(chan *output.ResultEvent, 100)

// Consumer goroutine
go func() {
	for event := range resultChan {
		// Process results asynchronously
		processResult(event)
	}
}()

// Callback pushes to channel
err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	resultChan <- event
})

close(resultChan)

Error handling pattern

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	defer func() {
		if r := recover(); r != nil {
			log.Printf("Callback panic: %v", r)
		}
	}()
	
	// Your callback logic
	processEvent(event)
})

Rate limiting pattern

import "golang.org/x/time/rate"

limiter := rate.NewLimiter(10, 1) // 10 calls per second

err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	limiter.Wait(context.Background())
	sendToAPI(event)
})

Accessing extracted data

Templates can extract data using extractors:
err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	if len(event.ExtractedResults) > 0 {
		fmt.Printf("Extracted data from %s:\n", event.Host)
		for _, extracted := range event.ExtractedResults {
			fmt.Printf("  - %s\n", extracted)
		}
	}
})

Metadata access

Access custom metadata from templates:
err := ne.ExecuteWithCallback(func(event *output.ResultEvent) {
	if cve, ok := event.Metadata["cve-id"].(string); ok {
		fmt.Printf("CVE: %s\n", cve)
	}
	if cvss, ok := event.Metadata["cvss-score"].(float64); ok {
		fmt.Printf("CVSS: %.1f\n", cvss)
	}
})

Next steps

NucleiEngine API

Explore engine methods

Options API

Learn about all configuration options

Build docs developers (and LLMs) love