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 }