package stats
import (
_ "embed"
"fmt"
"sort"
"strconv"
"sync/atomic"
"github.com/logrusorgru/aurora"
"github.com/projectdiscovery/nuclei/v3/pkg/output/stats/waf"
mapsutil "github.com/projectdiscovery/utils/maps"
)
type Tracker struct {
statusCodes *mapsutil.SyncLockMap[string, *atomic.Int32]
errorCodes *mapsutil.SyncLockMap[string, *atomic.Int32]
wafDetected *mapsutil.SyncLockMap[string, *atomic.Int32]
wafDetector *waf.WafDetector
}
func NewTracker() *Tracker {
return &Tracker{
statusCodes: mapsutil.NewSyncLockMap[string, *atomic.Int32](),
errorCodes: mapsutil.NewSyncLockMap[string, *atomic.Int32](),
wafDetected: mapsutil.NewSyncLockMap[string, *atomic.Int32](),
wafDetector: waf.NewWafDetector(),
}
}
func (t *Tracker) TrackStatusCode(statusCode string) {
t.incrementCounter(t.statusCodes, statusCode)
}
func (t *Tracker) TrackErrorKind(errKind string) {
t.incrementCounter(t.errorCodes, errKind)
}
func (t *Tracker) TrackWAFDetected(httpResponse string) {
waf, ok := t.wafDetector.DetectWAF(httpResponse)
if !ok {
return
}
t.incrementCounter(t.wafDetected, waf)
}
func (t *Tracker) incrementCounter(m *mapsutil.SyncLockMap[string, *atomic.Int32], key string) {
if counter, ok := m.Get(key); ok {
counter.Add(1)
} else {
newCounter := new(atomic.Int32)
newCounter.Store(1)
_ = m.Set(key, newCounter)
}
}
type StatsOutput struct {
StatusCodeStats map[string]int `json:"status_code_stats"`
ErrorStats map[string]int `json:"error_stats"`
WAFStats map[string]int `json:"waf_stats"`
}
func (t *Tracker) GetStats() *StatsOutput {
stats := &StatsOutput{
StatusCodeStats: make(map[string]int),
ErrorStats: make(map[string]int),
WAFStats: make(map[string]int),
}
_ = t.errorCodes.Iterate(func(k string, v *atomic.Int32) error {
stats.ErrorStats[k] = int(v.Load())
return nil
})
_ = t.statusCodes.Iterate(func(k string, v *atomic.Int32) error {
stats.StatusCodeStats[k] = int(v.Load())
return nil
})
_ = t.wafDetected.Iterate(func(k string, v *atomic.Int32) error {
waf, ok := t.wafDetector.GetWAF(k)
if !ok {
return nil
}
stats.WAFStats[waf.Name] = int(v.Load())
return nil
})
return stats
}
func (t *Tracker) DisplayTopStats(noColor bool) {
stats := t.GetStats()
if len(stats.StatusCodeStats) > 0 {
fmt.Printf("\n%s\n", aurora.Bold(aurora.Blue("Top Status Codes:")))
topStatusCodes := getTopN(stats.StatusCodeStats, 6)
for _, item := range topStatusCodes {
if noColor {
fmt.Printf(" %s: %d\n", item.Key, item.Value)
} else {
color := getStatusCodeColor(item.Key)
fmt.Printf(" %s: %d\n", aurora.Colorize(item.Key, color), item.Value)
}
}
}
if len(stats.ErrorStats) > 0 {
fmt.Printf("\n%s\n", aurora.Bold(aurora.Red("Top Errors:")))
topErrors := getTopN(stats.ErrorStats, 5)
for _, item := range topErrors {
if noColor {
fmt.Printf(" %s: %d\n", item.Key, item.Value)
} else {
fmt.Printf(" %s: %d\n", aurora.Red(item.Key), item.Value)
}
}
}
if len(stats.WAFStats) > 0 {
fmt.Printf("\n%s\n", aurora.Bold(aurora.Yellow("WAF Detections:")))
for name, count := range stats.WAFStats {
if noColor {
fmt.Printf(" %s: %d\n", name, count)
} else {
fmt.Printf(" %s: %d\n", aurora.Yellow(name), count)
}
}
}
}
type kv struct {
Key string
Value int
}
func getTopN(m map[string]int, n int) []kv {
var items []kv
for k, v := range m {
items = append(items, kv{k, v})
}
sort.Slice(items, func(i, j int) bool {
return items[i].Value > items[j].Value
})
if len(items) > n {
items = items[:n]
}
return items
}
func getStatusCodeColor(statusCode string) aurora.Color {
code, _ := strconv.Atoi(statusCode)
switch {
case code >= 200 && code < 300:
return aurora.GreenFg
case code >= 300 && code < 400:
return aurora.BlueFg
case code >= 400 && code < 500:
return aurora.YellowFg
case code >= 500:
return aurora.RedFg
default:
return aurora.WhiteFg
}
}