Detector Quick Start Guide¶
Get your first Tracee detector running in 30 minutes. This hands-on tutorial assumes basic Go knowledge but no prior Tracee experience.
What You'll Build¶
A detector that identifies when processes access sensitive system files like /etc/shadow or /etc/sudoers, with:
- Automatic threat metadata enrichment
- Full process ancestry (parent โ grandparent โ ...)
- Container context (if applicable)
Prerequisites¶
# Clone Tracee
git clone https://github.com/aquasecurity/tracee
cd tracee
# Verify build environment
make env
# Required: Linux kernel 4.18+, Go 1.24+, Clang 12+
Step 1: Create Your Detector File¶
Create detectors/sensitive_file_access.go:
package detectors
import (
"context"
"github.com/aquasecurity/tracee/api/v1beta1"
"github.com/aquasecurity/tracee/api/v1beta1/detection"
)
// Auto-register the detector on startup
func init() {
register(&SensitiveFileAccess{})
}
// SensitiveFileAccess detects access to sensitive system files
type SensitiveFileAccess struct {
logger detection.Logger
}
// GetDefinition declares what this detector does and what it needs
func (d *SensitiveFileAccess) GetDefinition() detection.DetectorDefinition {
return detection.DetectorDefinition{
ID: "TRC-001", // Unique identifier
Requirements: detection.DetectorRequirements{
Events: []detection.EventRequirement{
{
Name: "security_file_open",
// Engine filters - only matching events reach OnEvent()
DataFilters: []string{
"pathname=/etc/shadow",
"pathname=/etc/sudoers",
},
},
},
},
// Define the output event this detector produces
ProducedEvent: v1beta1.EventDefinition{
Name: "sensitive_file_access",
Description: "Access to sensitive system files detected",
Version: &v1beta1.Version{Major: 1, Minor: 0, Patch: 0},
Fields: []*v1beta1.EventField{
{Name: "file_path", Type: "const char*"},
{Name: "executable", Type: "const char*"},
},
},
// Threat information template (auto-copied to outputs)
ThreatMetadata: &v1beta1.Threat{
Name: "Sensitive File Access",
Description: "A process attempted to access a sensitive system file",
Severity: v1beta1.Severity_MEDIUM,
},
// Tell engine to auto-enrich our outputs
AutoPopulate: detection.AutoPopulateFields{
Threat: true, // Copy ThreatMetadata above
DetectedFrom: true, // Link to triggering event
ProcessAncestry: true, // Add 5 levels of parent processes
},
}
}
// Init is called once at startup
func (d *SensitiveFileAccess) Init(params detection.DetectorParams) error {
d.logger = params.Logger
d.logger.Infow("SensitiveFileAccess detector initialized")
return nil
}
// OnEvent processes each matching event
func (d *SensitiveFileAccess) OnEvent(
ctx context.Context,
event *v1beta1.Event,
) ([]detection.DetectorOutput, error) {
// Type-safe data extraction
pathname, found := v1beta1.GetData[string](event, "pathname")
if !found {
return nil, nil // Skip if pathname missing
}
// Extract executable path (nil-safe protobuf getters)
executablePath := event.GetWorkload().GetProcess().GetExecutable().GetPath()
// Return output data - engine handles the rest (threat, ancestry, etc.)
return []detection.DetectorOutput{{
Data: []*v1beta1.EventValue{
v1beta1.NewStringValue("file_path", pathname),
v1beta1.NewStringValue("executable", executablePath),
},
}}, nil
}
Step 2: Build Tracee¶
# Build with your detector included
make tracee
# The detector is compiled in - no plugins or manual registration needed!
Step 3: Run Tracee¶
# Start Tracee with process tree and JSON output
sudo ./dist/tracee --stores process.source=both --output json | jq
Step 4: Trigger Your Detector¶
In a third terminal, access a sensitive file:
sudo cat /etc/shadow
Step 5: See the Detection¶
You should see output like:
{
"id": 5001,
"name": "sensitive_file_access",
"version": "1.0.0",
"timestamp": "2025-12-16T10:30:45.123Z",
"threat": {
"name": "Sensitive File Access",
"description": "A process attempted to access a sensitive system file",
"severity": "MEDIUM"
},
"data": {
"file_path": "/etc/shadow",
"executable": "/bin/cat"
},
"detected_from": {
"event_id": 257,
"event_name": "security_file_open"
},
"workload": {
"process": {
"entity_id": 12345,
"pid": 67890,
"executable": {"path": "/bin/cat"},
"ancestors": [
{"entity_id": 12344, "pid": 67889, "executable": {"path": "/bin/bash"}},
{"entity_id": 12343, "pid": 67888, "thread": {"name": "sshd"}},
{"entity_id": 1, "pid": 1, "thread": {"name": "systemd"}}
]
}
}
}
What Just Happened?¶
Let's break down the magic:
1. Auto-Registration¶
func init() {
register(&SensitiveFileAccess{})
}
Your detector registered itself automatically. No manual list maintenance!
2. Engine Filtering¶
DataFilters: []string{
"pathname=/etc/shadow",
"pathname=/etc/sudoers",
},
Tracee's engine filtered millions of security_file_open events. Only those matching your paths reached OnEvent(). This happens in the engine - super efficient!
3. Type-Safe Extraction¶
pathname, found := v1beta1.GetData[string](event, "pathname")
Generic type parameter ensures compile-time type safety. No runtime casting errors!
4. Auto-Enrichment¶
AutoPopulate: detection.AutoPopulateFields{
Threat: true,
DetectedFrom: true,
ProcessAncestry: true,
}
The engine automatically:
- โ
Copied
ThreatMetadatatooutput.Threat - โ
Set
output.DetectedFrompointing to the triggering event - โ Queried the process tree and populated 5 ancestor levels
- โ Preserved timestamp, workload, and policies from input event
You just returned simple data fields - the engine did the heavy lifting!
Common Patterns¶
Multiple Outputs¶
A single input can produce multiple detections:
func (d *MyDetector) OnEvent(ctx context.Context, event *v1beta1.Event) ([]detection.DetectorOutput, error) {
var outputs []detection.DetectorOutput
// Check condition 1
if isSuspicious {
outputs = append(outputs, detection.DetectorOutput{
Data: []*v1beta1.EventValue{
v1beta1.NewStringValue("reason", "suspicious pattern"),
},
})
}
// Check condition 2
if isMalicious {
outputs = append(outputs, detection.DetectorOutput{
Data: []*v1beta1.EventValue{
v1beta1.NewStringValue("reason", "known malware signature"),
},
})
}
return outputs, nil
}
Custom Threat Severity¶
Override the default threat metadata per detection:
return []detection.DetectorOutput{{
Data: []*v1beta1.EventValue{
v1beta1.NewStringValue("file", pathname),
},
Threat: &v1beta1.Threat{
Name: "Critical System File Access",
Severity: v1beta1.Severity_CRITICAL, // Override default MEDIUM
Description: "Root accessed critical system file",
},
}}, nil
Using DataStores¶
Access system state like process information:
func (d *MyDetector) OnEvent(ctx context.Context, event *v1beta1.Event) ([]detection.DetectorOutput, error) {
entityId := event.GetWorkload().GetProcess().GetEntityId()
// Query process store for process details
proc, err := d.dataStores.Processes().GetProcess(entityId)
if errors.Is(err, datastores.ErrNotFound) {
// Process not in tree yet
return nil, nil
}
if err != nil {
return nil, err
}
// Use process info in detection logic
if proc.GetInterpreter().GetPath() == "/bin/bash" {
return []detection.DetectorOutput{{
Data: []*v1beta1.EventValue{
v1beta1.NewStringValue("shell", "bash"),
},
}}, nil
}
return nil, nil
}
Filtering by Container¶
Only detect events from containers:
Requirements: detection.DetectorRequirements{
Events: []detection.EventRequirement{
{
Name: "security_file_open",
ScopeFilters: []string{"container=started"}, // Containers only
},
},
}
Or only from host:
ScopeFilters: []string{"not-container"}, // Host only
Debugging Tips¶
Enable Debug Logging¶
# Run with debug logs
sudo ./dist/tracee --logging debug --stores process.source=both
Check Detector Registration¶
# List detector events
sudo ./dist/tracee list | grep detectors
Verify Event Filtering¶
Check that your event name is correct:
# List available events
sudo ./dist/tracee list | grep security_file_open
Test Without Filters¶
Temporarily remove DataFilters to see all events:
Requirements: detection.DetectorRequirements{
Events: []detection.EventRequirement{
{
Name: "security_file_open",
// DataFilters: []string{...}, // Commented out for testing
},
},
}
Add logging in OnEvent():
func (d *SensitiveFileAccess) OnEvent(ctx context.Context, event *v1beta1.Event) ([]detection.DetectorOutput, error) {
d.logger.Debugw("Event received", "event", event.Name)
// ... rest of logic
}
Next Steps¶
Congratulations! You've written your first Tracee detector. Now explore:
๐ Deep Dive into APIs¶
- API Reference: Complete detector API documentation
- DataStore API: Query process trees, containers, DNS, and more
๐ Migration and Troubleshooting¶
- Migration Guide: Migrating from old signatures
- Troubleshooting: Common issues and solutions
๐ Study Real Examples¶
- Browse
detectors/directory for production implementations - See
api/v1beta1/detection/detector.gofor interface definitions
๐งช Write Tests¶
- Add unit tests in
detectors/sensitive_file_access_test.go - See Testing Guide for patterns
Troubleshooting¶
Detector Not Running¶
Problem: Your detector code changed but behavior didn't
Solution: Rebuild Tracee - detectors are compiled in, not dynamically loaded
make tracee
No Events Received¶
Problem: OnEvent() never called
Possible causes:
- Wrong event name - check
tracee list - Filters too restrictive - temporarily remove them
- Event not enabled - Tracee enables events based on requirements automatically
Process Ancestry Empty¶
Problem: workload.process.ancestors is null
Solution: Enable process tree:
sudo ./dist/tracee --stores process.source=both
Build Errors¶
Problem: Compilation fails
Common fixes:
# Update dependencies
make go-tidy
# Clean build
make clean
make tracee
Quick Reference¶
Detector Skeleton¶
package detectors
import (
"context"
"github.com/aquasecurity/tracee/api/v1beta1"
"github.com/aquasecurity/tracee/api/v1beta1/datastores"
"github.com/aquasecurity/tracee/api/v1beta1/detection"
)
func init() {
register(&MyDetector{})
}
type MyDetector struct {
logger detection.Logger
dataStores datastores.Registry
}
func (d *MyDetector) GetDefinition() detection.DetectorDefinition {
return detection.DetectorDefinition{
ID: "TRC-XXX",
Requirements: detection.DetectorRequirements{
Events: []detection.EventRequirement{{Name: "event_name"}},
},
ProducedEvent: v1beta1.EventDefinition{
Name: "my_detection",
Version: &v1beta1.Version{Major: 1},
},
AutoPopulate: detection.AutoPopulateFields{
Threat: true,
DetectedFrom: true,
ProcessAncestry: true,
},
}
}
func (d *MyDetector) Init(params detection.DetectorParams) error {
d.logger = params.Logger
d.dataStores = params.DataStores
return nil
}
func (d *MyDetector) OnEvent(ctx context.Context, event *v1beta1.Event) ([]detection.DetectorOutput, error) {
// Your detection logic
return []detection.DetectorOutput{{
Data: []*v1beta1.EventValue{},
}}, nil
}
Essential Commands¶
# Build
make tracee
# Run with process tree
sudo ./dist/tracee --stores process.source=both
# View detections in JSON
sudo ./dist/tracee --output json | jq
# List detector events
sudo ./dist/tracee list | grep detectors
# Debug mode
sudo ./dist/tracee --logging debug
Ready for more? Continue to the API Reference for comprehensive documentation, including migration guides and troubleshooting.