mirror of https://github.com/daffainfo/nuclei.git
Added simplehttp-only clustering impl (wip)
parent
ab2bb0226f
commit
02822a17c0
|
@ -0,0 +1,49 @@
|
||||||
|
package clusterer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cluster clusters a list of templates into a lesser number if possible based
|
||||||
|
// on the similarity between the sent requests.
|
||||||
|
//
|
||||||
|
// If the attributes match, multiple requests can be clustered into a single
|
||||||
|
// request which saves time and network resources during execution.
|
||||||
|
func Cluster(list map[string]*templates.Template) [][]*templates.Template {
|
||||||
|
final := [][]*templates.Template{}
|
||||||
|
|
||||||
|
// Each protocol that can be clustered should be handled here.
|
||||||
|
for key, template := range list {
|
||||||
|
// We only cluster http requests as of now.
|
||||||
|
// Take care of requests that can't be clustered first.
|
||||||
|
if len(template.RequestsHTTP) == 0 {
|
||||||
|
delete(list, key)
|
||||||
|
final = append(final, []*templates.Template{template})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(list, key) // delete element first so it's not found later.
|
||||||
|
// Find any/all similar matching request that is identical to
|
||||||
|
// this one and cluster them together for http protocol only.
|
||||||
|
if len(template.RequestsHTTP) == 1 {
|
||||||
|
cluster := []*templates.Template{}
|
||||||
|
|
||||||
|
for otherKey, other := range list {
|
||||||
|
if len(other.RequestsHTTP) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if template.RequestsHTTP[0].CanCluster(other.RequestsHTTP[0]) {
|
||||||
|
delete(list, otherKey)
|
||||||
|
cluster = append(cluster, other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cluster) > 0 {
|
||||||
|
cluster = append(cluster, template)
|
||||||
|
final = append(final, cluster)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final = append(final, []*templates.Template{template})
|
||||||
|
}
|
||||||
|
return final
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package clusterer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/logrusorgru/aurora"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/catalogue"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolinit"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/templates"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTTPRequestsCluster(t *testing.T) {
|
||||||
|
catalogue := catalogue.New("/Users/ice3man/nuclei-templates")
|
||||||
|
templatesList, err := catalogue.GetTemplatePath("/Users/ice3man/nuclei-templates")
|
||||||
|
require.Nil(t, err, "could not get templates")
|
||||||
|
|
||||||
|
protocolinit.Init(&types.Options{})
|
||||||
|
list := make(map[string]*templates.Template)
|
||||||
|
for _, template := range templatesList {
|
||||||
|
executerOpts := &protocols.ExecuterOptions{
|
||||||
|
Output: &mockOutput{},
|
||||||
|
Options: &types.Options{},
|
||||||
|
Progress: nil,
|
||||||
|
Catalogue: catalogue,
|
||||||
|
RateLimiter: nil,
|
||||||
|
ProjectFile: nil,
|
||||||
|
}
|
||||||
|
t, err := templates.Parse(template, executerOpts)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := list[t.ID]; !ok {
|
||||||
|
list[t.ID] = t
|
||||||
|
} else {
|
||||||
|
// log.Fatalf("Duplicate template found: %v\n", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalClusterCount := 0
|
||||||
|
totalRequestsSentNew := 0
|
||||||
|
new := Cluster(list)
|
||||||
|
for i, cluster := range new {
|
||||||
|
if len(cluster) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("[%d] cluster created:\n", i)
|
||||||
|
for _, request := range cluster {
|
||||||
|
totalClusterCount++
|
||||||
|
fmt.Printf("\t%v\n", request.ID)
|
||||||
|
}
|
||||||
|
totalRequestsSentNew++
|
||||||
|
}
|
||||||
|
fmt.Printf("Reduced %d requests to %d via clustering\n", totalClusterCount, totalRequestsSentNew)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockOutput struct{}
|
||||||
|
|
||||||
|
// Close closes the output writer interface
|
||||||
|
func (m *mockOutput) Close() {}
|
||||||
|
|
||||||
|
// Colorizer returns the colorizer instance for writer
|
||||||
|
func (m *mockOutput) Colorizer() aurora.Aurora {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes the event to file and/or screen.
|
||||||
|
func (m *mockOutput) Write(*output.ResultEvent) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request writes a log the requests trace log
|
||||||
|
func (m *mockOutput) Request(templateID, url, requestType string, err error) {}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package compare
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// StringSlice compares two string slices for equality
|
||||||
|
func StringSlice(a, b []string) bool {
|
||||||
|
// If one is nil, the other must also be nil.
|
||||||
|
if (a == nil) != (b == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if !strings.EqualFold(a[i], b[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringMap compares two string maps for equality
|
||||||
|
func StringMap(a, b map[string]string) bool {
|
||||||
|
// If one is nil, the other must also be nil.
|
||||||
|
if (a == nil) != (b == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k, v := range a {
|
||||||
|
if w, ok := b[k]; !ok || !strings.EqualFold(v, w) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
|
@ -18,6 +18,11 @@ func NewExecuter(requests []protocols.Request, options *protocols.ExecuterOption
|
||||||
return &Executer{requests: requests, options: options}
|
return &Executer{requests: requests, options: options}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRequests returns the requests the rule will perform
|
||||||
|
func (e *Executer) GetRequests() []protocols.Request {
|
||||||
|
return e.requests
|
||||||
|
}
|
||||||
|
|
||||||
// Compile compiles the execution generators preparing any requests possible.
|
// Compile compiles the execution generators preparing any requests possible.
|
||||||
func (e *Executer) Compile() error {
|
func (e *Executer) Compile() error {
|
||||||
for _, request := range e.requests {
|
for _, request := range e.requests {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/compare"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CanCluster returns true if the request can be clustered.
|
||||||
|
//
|
||||||
|
// This used by the clustering engine to decide whether two requests
|
||||||
|
// are similar enough to be considered one and can be checked by
|
||||||
|
// just adding the matcher/extractors for the request and the correct IDs.
|
||||||
|
func (r *Request) CanCluster(other *Request) bool {
|
||||||
|
if len(r.Payloads) > 0 || len(r.Raw) > 0 || len(r.Body) > 0 || r.Unsafe {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.Method != other.Method ||
|
||||||
|
r.MaxRedirects != other.MaxRedirects ||
|
||||||
|
r.CookieReuse != other.CookieReuse ||
|
||||||
|
r.Redirects != other.Redirects {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !compare.StringSlice(r.Path, other.Path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !compare.StringMap(r.Headers, other.Headers) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
Loading…
Reference in New Issue