github.com/projectdiscovery/nuclei/v2@v2.9.15/pkg/templates/cluster.go (about)

     1  package templates
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	"github.com/projectdiscovery/gologger"
     9  	"github.com/projectdiscovery/nuclei/v2/pkg/model"
    10  	"github.com/projectdiscovery/nuclei/v2/pkg/operators"
    11  	"github.com/projectdiscovery/nuclei/v2/pkg/output"
    12  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
    13  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
    14  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/helpers/writer"
    15  	"github.com/projectdiscovery/nuclei/v2/pkg/templates/types"
    16  	cryptoutil "github.com/projectdiscovery/utils/crypto"
    17  	mapsutil "github.com/projectdiscovery/utils/maps"
    18  )
    19  
    20  // Cluster clusters a list of templates into a lesser number if possible based
    21  // on the similarity between the sent requests.
    22  //
    23  // If the attributes match, multiple requests can be clustered into a single
    24  // request which saves time and network resources during execution.
    25  //
    26  // The clusterer goes through all the templates, looking for templates with a single
    27  // HTTP/DNS/TLS request to an endpoint (multiple requests aren't clustered as of now).
    28  //
    29  // All the templates are iterated and any templates with request that is identical
    30  // to the first individual request is compared for equality.
    31  // The equality check is performed as described below -
    32  //
    33  // Cases where clustering is not performed (request is considered different)
    34  //   - If request contains payloads,raw,body,unsafe,req-condition,name attributes
    35  //   - If request methods,max-redirects,cookie-reuse,redirects are not equal
    36  //   - If request paths aren't identical.
    37  //   - If request headers aren't identical
    38  //   - Similarly for DNS, only identical DNS requests are clustered to a target.
    39  //   - Similarly for TLS, only identical TLS requests are clustered to a target.
    40  //
    41  // If multiple requests are identified as identical, they are appended to a slice.
    42  // Finally, the engine creates a single executer with a clusteredexecuter for all templates
    43  // in a cluster.
    44  func Cluster(list []*Template) [][]*Template {
    45  	final := [][]*Template{}
    46  	skip := mapsutil.NewSyncLockMap[string, struct{}]()
    47  
    48  	for _, template := range list {
    49  		key := template.Path
    50  
    51  		if skip.Has(key) {
    52  			continue
    53  		}
    54  
    55  		// We only cluster http, dns and ssl requests as of now.
    56  		// Take care of requests that can't be clustered first.
    57  		if len(template.RequestsHTTP) == 0 && len(template.RequestsDNS) == 0 && len(template.RequestsSSL) == 0 {
    58  			_ = skip.Set(key, struct{}{})
    59  			final = append(final, []*Template{template})
    60  			continue
    61  		}
    62  		_ = skip.Set(key, struct{}{})
    63  
    64  		var templateType types.ProtocolType
    65  		switch {
    66  		case len(template.RequestsDNS) == 1:
    67  			templateType = types.DNSProtocol
    68  		case len(template.RequestsHTTP) == 1:
    69  			templateType = types.HTTPProtocol
    70  		case len(template.RequestsSSL) == 1:
    71  			templateType = types.SSLProtocol
    72  		}
    73  
    74  		// Find any/all similar matching request that is identical to
    75  		// this one and cluster them together for http protocol only.
    76  		cluster := []*Template{}
    77  		for _, other := range list {
    78  			otherKey := other.Path
    79  
    80  			if skip.Has(otherKey) {
    81  				continue
    82  			}
    83  
    84  			switch templateType {
    85  			case types.DNSProtocol:
    86  				if len(other.RequestsDNS) != 1 {
    87  					continue
    88  				} else if template.RequestsDNS[0].CanCluster(other.RequestsDNS[0]) {
    89  					_ = skip.Set(otherKey, struct{}{})
    90  					cluster = append(cluster, other)
    91  				}
    92  			case types.HTTPProtocol:
    93  				if len(other.RequestsHTTP) != 1 {
    94  					continue
    95  				} else if template.RequestsHTTP[0].CanCluster(other.RequestsHTTP[0]) {
    96  					_ = skip.Set(otherKey, struct{}{})
    97  					cluster = append(cluster, other)
    98  				}
    99  			case types.SSLProtocol:
   100  				if len(other.RequestsSSL) != 1 {
   101  					continue
   102  				} else if template.RequestsSSL[0].CanCluster(other.RequestsSSL[0]) {
   103  					_ = skip.Set(otherKey, struct{}{})
   104  					cluster = append(cluster, other)
   105  				}
   106  			}
   107  		}
   108  		if len(cluster) > 0 {
   109  			cluster = append(cluster, template)
   110  			final = append(final, cluster)
   111  		} else {
   112  			final = append(final, []*Template{template})
   113  		}
   114  	}
   115  	return final
   116  }
   117  
   118  // ClusterID transforms clusterization into a mathematical hash repeatable across executions with the same templates
   119  func ClusterID(templates []*Template) string {
   120  	allIDS := make([]string, len(templates))
   121  	for tplIndex, tpl := range templates {
   122  		allIDS[tplIndex] = tpl.ID
   123  	}
   124  	sort.Strings(allIDS)
   125  	ids := strings.Join(allIDS, ",")
   126  	return cryptoutil.SHA256Sum(ids)
   127  }
   128  
   129  func ClusterTemplates(templatesList []*Template, options protocols.ExecutorOptions) ([]*Template, int) {
   130  	if options.Options.OfflineHTTP || options.Options.DisableClustering {
   131  		return templatesList, 0
   132  	}
   133  
   134  	var clusterCount int
   135  
   136  	finalTemplatesList := make([]*Template, 0, len(templatesList))
   137  	clusters := Cluster(templatesList)
   138  	for _, cluster := range clusters {
   139  		if len(cluster) > 1 {
   140  			executerOpts := options
   141  			clusterID := fmt.Sprintf("cluster-%s", ClusterID(cluster))
   142  
   143  			for _, req := range cluster[0].RequestsDNS {
   144  				req.Options().TemplateID = clusterID
   145  			}
   146  			for _, req := range cluster[0].RequestsHTTP {
   147  				req.Options().TemplateID = clusterID
   148  			}
   149  			for _, req := range cluster[0].RequestsSSL {
   150  				req.Options().TemplateID = clusterID
   151  			}
   152  			executerOpts.TemplateID = clusterID
   153  			finalTemplatesList = append(finalTemplatesList, &Template{
   154  				ID:            clusterID,
   155  				RequestsDNS:   cluster[0].RequestsDNS,
   156  				RequestsHTTP:  cluster[0].RequestsHTTP,
   157  				RequestsSSL:   cluster[0].RequestsSSL,
   158  				Executer:      NewClusterExecuter(cluster, &executerOpts),
   159  				TotalRequests: len(cluster[0].RequestsHTTP) + len(cluster[0].RequestsDNS),
   160  			})
   161  			clusterCount += len(cluster)
   162  		} else {
   163  			finalTemplatesList = append(finalTemplatesList, cluster...)
   164  		}
   165  	}
   166  	return finalTemplatesList, clusterCount
   167  }
   168  
   169  // ClusterExecuter executes a group of requests for a protocol for a clustered
   170  // request. It is different from normal executers since the original
   171  // operators are all combined and post processed after making the request.
   172  type ClusterExecuter struct {
   173  	requests     protocols.Request
   174  	operators    []*clusteredOperator
   175  	templateType types.ProtocolType
   176  	options      *protocols.ExecutorOptions
   177  }
   178  
   179  type clusteredOperator struct {
   180  	templateID   string
   181  	templatePath string
   182  	templateInfo model.Info
   183  	operator     *operators.Operators
   184  }
   185  
   186  var _ protocols.Executer = &ClusterExecuter{}
   187  
   188  // NewClusterExecuter creates a new request executer for list of requests
   189  func NewClusterExecuter(requests []*Template, options *protocols.ExecutorOptions) *ClusterExecuter {
   190  	executer := &ClusterExecuter{options: options}
   191  	if len(requests[0].RequestsDNS) == 1 {
   192  		executer.templateType = types.DNSProtocol
   193  		executer.requests = requests[0].RequestsDNS[0]
   194  	} else if len(requests[0].RequestsHTTP) == 1 {
   195  		executer.templateType = types.HTTPProtocol
   196  		executer.requests = requests[0].RequestsHTTP[0]
   197  	} else if len(requests[0].RequestsSSL) == 1 {
   198  		executer.templateType = types.SSLProtocol
   199  		executer.requests = requests[0].RequestsSSL[0]
   200  	}
   201  	appendOperator := func(req *Template, operator *operators.Operators) {
   202  		operator.TemplateID = req.ID
   203  		operator.ExcludeMatchers = options.ExcludeMatchers
   204  
   205  		executer.operators = append(executer.operators, &clusteredOperator{
   206  			operator:     operator,
   207  			templateID:   req.ID,
   208  			templateInfo: req.Info,
   209  			templatePath: req.Path,
   210  		})
   211  	}
   212  	for _, req := range requests {
   213  		if executer.templateType == types.DNSProtocol {
   214  			if req.RequestsDNS[0].CompiledOperators != nil {
   215  				appendOperator(req, req.RequestsDNS[0].CompiledOperators)
   216  			}
   217  		} else if executer.templateType == types.HTTPProtocol {
   218  			if req.RequestsHTTP[0].CompiledOperators != nil {
   219  				appendOperator(req, req.RequestsHTTP[0].CompiledOperators)
   220  			}
   221  		} else if executer.templateType == types.SSLProtocol {
   222  			if req.RequestsSSL[0].CompiledOperators != nil {
   223  				appendOperator(req, req.RequestsSSL[0].CompiledOperators)
   224  			}
   225  		}
   226  	}
   227  	return executer
   228  }
   229  
   230  // Compile compiles the execution generators preparing any requests possible.
   231  func (e *ClusterExecuter) Compile() error {
   232  	return e.requests.Compile(e.options)
   233  }
   234  
   235  // Requests returns the total number of requests the rule will perform
   236  func (e *ClusterExecuter) Requests() int {
   237  	var count int
   238  	count += e.requests.Requests()
   239  	return count
   240  }
   241  
   242  // Execute executes the protocol group and returns true or false if results were found.
   243  func (e *ClusterExecuter) Execute(input *contextargs.Context) (bool, error) {
   244  	var results bool
   245  
   246  	inputItem := input.Clone()
   247  	if e.options.InputHelper != nil && input.MetaInput.Input != "" {
   248  		if inputItem.MetaInput.Input = e.options.InputHelper.Transform(input.MetaInput.Input, e.templateType); input.MetaInput.Input == "" {
   249  			return false, nil
   250  		}
   251  	}
   252  	previous := make(map[string]interface{})
   253  	dynamicValues := make(map[string]interface{})
   254  	err := e.requests.ExecuteWithResults(inputItem, dynamicValues, previous, func(event *output.InternalWrappedEvent) {
   255  		for _, operator := range e.operators {
   256  			result, matched := operator.operator.Execute(event.InternalEvent, e.requests.Match, e.requests.Extract, e.options.Options.Debug || e.options.Options.DebugResponse)
   257  			event.InternalEvent["template-id"] = operator.templateID
   258  			event.InternalEvent["template-path"] = operator.templatePath
   259  			event.InternalEvent["template-info"] = operator.templateInfo
   260  
   261  			if result == nil && !matched && e.options.Options.MatcherStatus {
   262  				if err := e.options.Output.WriteFailure(event); err != nil {
   263  					gologger.Warning().Msgf("Could not write failure event to output: %s\n", err)
   264  				}
   265  				continue
   266  			}
   267  			if matched && result != nil {
   268  				event.OperatorsResult = result
   269  				event.Results = e.requests.MakeResultEvent(event)
   270  				results = true
   271  
   272  				_ = writer.WriteResult(event, e.options.Output, e.options.Progress, e.options.IssuesClient)
   273  			}
   274  		}
   275  	})
   276  	if err != nil && e.options.HostErrorsCache != nil {
   277  		e.options.HostErrorsCache.MarkFailed(input.MetaInput.Input, err)
   278  	}
   279  	return results, err
   280  }
   281  
   282  // ExecuteWithResults executes the protocol requests and returns results instead of writing them.
   283  func (e *ClusterExecuter) ExecuteWithResults(input *contextargs.Context, callback protocols.OutputEventCallback) error {
   284  	dynamicValues := make(map[string]interface{})
   285  
   286  	inputItem := input.Clone()
   287  	if e.options.InputHelper != nil && input.MetaInput.Input != "" {
   288  		if inputItem.MetaInput.Input = e.options.InputHelper.Transform(input.MetaInput.Input, e.templateType); input.MetaInput.Input == "" {
   289  			return nil
   290  		}
   291  	}
   292  	err := e.requests.ExecuteWithResults(inputItem, dynamicValues, nil, func(event *output.InternalWrappedEvent) {
   293  		for _, operator := range e.operators {
   294  			result, matched := operator.operator.Execute(event.InternalEvent, e.requests.Match, e.requests.Extract, e.options.Options.Debug || e.options.Options.DebugResponse)
   295  			if matched && result != nil {
   296  				event.OperatorsResult = result
   297  				event.InternalEvent["template-id"] = operator.templateID
   298  				event.InternalEvent["template-path"] = operator.templatePath
   299  				event.InternalEvent["template-info"] = operator.templateInfo
   300  				event.Results = e.requests.MakeResultEvent(event)
   301  				callback(event)
   302  			}
   303  		}
   304  	})
   305  	if err != nil && e.options.HostErrorsCache != nil {
   306  		e.options.HostErrorsCache.MarkFailed(input.MetaInput.Input, err)
   307  	}
   308  	return err
   309  }