package frequency
import (
"net"
"net/url"
"os"
"strings"
"sync"
"sync/atomic"
"github.com/bluele/gcache"
"github.com/projectdiscovery/gologger"
)
type Tracker struct {
frequencies gcache.Cache
paramOccurenceThreshold int
isDebug bool
}
const (
DefaultMaxTrackCount = 10000
DefaultParamOccurenceThreshold = 10
)
type cacheItem struct {
errors atomic.Int32
sync.Once
}
func New(maxTrackCount, paramOccurenceThreshold int) *Tracker {
gc := gcache.New(maxTrackCount).ARC().Build()
var isDebug bool
if os.Getenv("FREQ_DEBUG") != "" {
isDebug = true
}
return &Tracker{
isDebug: isDebug,
frequencies: gc,
paramOccurenceThreshold: paramOccurenceThreshold,
}
}
func (t *Tracker) Close() {
t.frequencies.Purge()
}
func (t *Tracker) MarkParameter(parameter, target, template string) {
normalizedTarget := normalizeTarget(target)
key := getFrequencyKey(parameter, normalizedTarget, template)
if t.isDebug {
gologger.Verbose().Msgf("[%s] Marking %s as found uninteresting", template, key)
}
existingCacheItem, err := t.frequencies.GetIFPresent(key)
if err != nil || existingCacheItem == nil {
newItem := &cacheItem{errors: atomic.Int32{}}
newItem.errors.Store(1)
_ = t.frequencies.Set(key, newItem)
return
}
existingCacheItemValue := existingCacheItem.(*cacheItem)
existingCacheItemValue.errors.Add(1)
_ = t.frequencies.Set(key, existingCacheItemValue)
}
func (t *Tracker) IsParameterFrequent(parameter, target, template string) bool {
normalizedTarget := normalizeTarget(target)
key := getFrequencyKey(parameter, normalizedTarget, template)
if t.isDebug {
gologger.Verbose().Msgf("[%s] Checking if %s is frequently found uninteresting", template, key)
}
existingCacheItem, err := t.frequencies.GetIFPresent(key)
if err != nil {
return false
}
existingCacheItemValue := existingCacheItem.(*cacheItem)
if existingCacheItemValue.errors.Load() >= int32(t.paramOccurenceThreshold) {
existingCacheItemValue.Do(func() {
gologger.Verbose().Msgf("[%s] Skipped %s from parameter for %s as found uninteresting %d times", template, parameter, target, existingCacheItemValue.errors.Load())
})
return true
}
return false
}
func (t *Tracker) UnmarkParameter(parameter, target, template string) {
normalizedTarget := normalizeTarget(target)
key := getFrequencyKey(parameter, normalizedTarget, template)
if t.isDebug {
gologger.Verbose().Msgf("[%s] Unmarking %s as frequently found uninteresting", template, key)
}
_ = t.frequencies.Remove(key)
}
func getFrequencyKey(parameter, target, template string) string {
var sb strings.Builder
sb.WriteString(target)
sb.WriteString(":")
sb.WriteString(template)
sb.WriteString(":")
sb.WriteString(parameter)
str := sb.String()
return str
}
func normalizeTarget(value string) string {
finalValue := value
if strings.HasPrefix(value, "http") {
if parsed, err := url.Parse(value); err == nil {
hostname := parsed.Host
finalPort := parsed.Port()
if finalPort == "" {
if parsed.Scheme == "https" {
finalPort = "443"
} else {
finalPort = "80"
}
hostname = net.JoinHostPort(parsed.Host, finalPort)
}
finalValue = hostname
}
}
return finalValue
}