Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/catalog/index/index.go
2843 views
1
package index
2
3
import (
4
"encoding/gob"
5
"maps"
6
"os"
7
"path/filepath"
8
"sync"
9
10
"github.com/maypok86/otter/v2"
11
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
12
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
13
folderutil "github.com/projectdiscovery/utils/folder"
14
)
15
16
const (
17
// IndexFileName is the name of the persistent cache file.
18
IndexFileName = "index.gob"
19
20
// IndexVersion is the schema version for cache invalidation on breaking
21
// changes.
22
IndexVersion = 1
23
24
// DefaultMaxSize is the default maximum number of templates to cache.
25
DefaultMaxSize = 50000
26
27
// DefaultMaxWeight is the default maximum weight of the cache.
28
DefaultMaxWeight = DefaultMaxSize * 800 // ~40MB assuming ~800B/entry
29
)
30
31
// Index represents a cache for template metadata.
32
type Index struct {
33
cache *otter.Cache[string, *Metadata]
34
cacheFile string
35
mu sync.RWMutex
36
version int
37
}
38
39
// cacheSnapshot represents the serialized cache structure.
40
type cacheSnapshot struct {
41
Version int `gob:"version"`
42
Data map[string]*Metadata `gob:"data"`
43
}
44
45
// NewIndex creates a new template metadata cache with the given options.
46
func NewIndex(cacheDir string) (*Index, error) {
47
if cacheDir == "" {
48
cacheDir = folderutil.AppCacheDirOrDefault(".nuclei-cache", config.BinaryName)
49
}
50
51
if err := os.MkdirAll(cacheDir, 0755); err != nil {
52
return nil, err
53
}
54
55
cacheFile := filepath.Join(cacheDir, IndexFileName)
56
57
// NOTE(dwisiswant0): Build cache with adaptive sizing based on memory cost.
58
opts := &otter.Options[string, *Metadata]{
59
MaximumWeight: uint64(DefaultMaxWeight),
60
Weigher: func(key string, value *Metadata) uint32 {
61
if value == nil {
62
return uint32(len(key))
63
}
64
65
weight := len(key)
66
weight += len(value.ID)
67
weight += len(value.FilePath)
68
weight += 24 // ModTime is time.Time (24B)
69
weight += len(value.Name)
70
weight += len(value.Severity)
71
weight += len(value.ProtocolType)
72
weight += len(value.TemplateVerifier)
73
74
for _, author := range value.Authors {
75
weight += len(author)
76
}
77
for _, tag := range value.Tags {
78
weight += len(tag)
79
}
80
81
return uint32(weight)
82
},
83
}
84
85
cache, err := otter.New(opts)
86
if err != nil {
87
return nil, err
88
}
89
90
c := &Index{
91
cache: cache,
92
cacheFile: cacheFile,
93
version: IndexVersion,
94
}
95
96
return c, nil
97
}
98
99
// NewDefaultIndex creates a index with default settings in the default cache
100
// directory.
101
func NewDefaultIndex() (*Index, error) {
102
return NewIndex("")
103
}
104
105
// Get retrieves metadata for a template path, validating freshness via mtime.
106
func (i *Index) Get(path string) (*Metadata, bool) {
107
i.mu.RLock()
108
defer i.mu.RUnlock()
109
110
metadata, found := i.cache.GetIfPresent(path)
111
if !found {
112
return nil, false
113
}
114
115
if !metadata.IsValid() {
116
go i.Delete(path)
117
118
return nil, false
119
}
120
121
return metadata, true
122
}
123
124
// Set stores metadata for a template path.
125
//
126
// The caller is responsible for ensuring the metadata is valid and contains
127
// the correct checksum before calling this method.
128
// Use [SetFromTemplate] for automatic extraction and checksum computation.
129
//
130
// Returns the metadata and whether it was successfully cached (false if evicted).
131
func (i *Index) Set(path string, metadata *Metadata) (*Metadata, bool) {
132
i.mu.Lock()
133
defer i.mu.Unlock()
134
135
return i.cache.Set(path, metadata)
136
}
137
138
// SetFromTemplate extracts metadata from a parsed template and stores it.
139
//
140
// Returns the metadata and whether it was successfully cached. The metadata is
141
// always returned (even on checksum failure) for immediate filtering use.
142
// Returns false if the metadata was not cached (e.g., set, evicted).
143
func (i *Index) SetFromTemplate(path string, tpl *templates.Template) (*Metadata, bool) {
144
metadata := NewMetadataFromTemplate(path, tpl)
145
146
info, err := os.Stat(path)
147
if err != nil {
148
return metadata, false
149
}
150
metadata.ModTime = info.ModTime()
151
152
if i.cache == nil {
153
return metadata, false
154
}
155
156
return i.Set(path, metadata)
157
}
158
159
// Has checks if metadata exists for a path without validation.
160
func (i *Index) Has(path string) bool {
161
i.mu.RLock()
162
defer i.mu.RUnlock()
163
164
_, found := i.cache.GetIfPresent(path)
165
166
return found
167
}
168
169
// Delete removes metadata for a path.
170
func (i *Index) Delete(path string) {
171
i.mu.Lock()
172
defer i.mu.Unlock()
173
174
i.cache.Invalidate(path)
175
}
176
177
// Size returns the number of cached entries.
178
func (i *Index) Size() int {
179
i.mu.RLock()
180
defer i.mu.RUnlock()
181
182
return i.cache.EstimatedSize()
183
}
184
185
// Clear removes all cached entries.
186
func (i *Index) Clear() {
187
i.mu.Lock()
188
defer i.mu.Unlock()
189
190
i.cache.InvalidateAll()
191
}
192
193
// Save persists the cache to disk using gob encoding.
194
func (i *Index) Save() error {
195
i.mu.RLock()
196
defer i.mu.RUnlock()
197
198
snapshot := &cacheSnapshot{
199
Version: i.version,
200
Data: make(map[string]*Metadata),
201
}
202
203
maps.Insert(snapshot.Data, i.cache.All())
204
205
// NOTE(dwisiswant0): write to temp for atomic op.
206
tmpFile := i.cacheFile + ".tmp"
207
file, err := os.Create(tmpFile)
208
if err != nil {
209
return err
210
}
211
212
encoder := gob.NewEncoder(file)
213
if err := encoder.Encode(snapshot); err != nil {
214
_ = file.Close()
215
_ = os.Remove(tmpFile)
216
217
return err
218
}
219
220
if err := file.Close(); err != nil {
221
_ = os.Remove(tmpFile)
222
223
return err
224
}
225
226
if err := os.Rename(tmpFile, i.cacheFile); err != nil {
227
_ = os.Remove(tmpFile)
228
229
return err
230
}
231
232
return nil
233
}
234
235
// Load loads the cache from disk using gob decoding.
236
func (i *Index) Load() error {
237
file, err := os.Open(i.cacheFile)
238
if err != nil {
239
if os.IsNotExist(err) {
240
return nil
241
}
242
243
return err
244
}
245
defer func() { _ = file.Close() }()
246
247
var snapshot cacheSnapshot
248
249
decoder := gob.NewDecoder(file)
250
if err := decoder.Decode(&snapshot); err != nil {
251
_ = file.Close()
252
_ = os.Remove(i.cacheFile)
253
254
return nil
255
}
256
257
if snapshot.Version != i.version {
258
_ = file.Close()
259
_ = os.Remove(i.cacheFile)
260
261
return nil
262
}
263
264
i.mu.Lock()
265
defer i.mu.Unlock()
266
267
for key, value := range snapshot.Data {
268
i.cache.Set(key, value)
269
}
270
271
return nil
272
}
273
274
// Filter returns all template paths that match the given filter criteria.
275
func (i *Index) Filter(filter *Filter) []string {
276
if filter == nil || filter.IsEmpty() {
277
return i.All()
278
}
279
280
i.mu.RLock()
281
defer i.mu.RUnlock()
282
283
var matched []string
284
for path, metadata := range i.cache.All() {
285
if filter.Matches(metadata) {
286
matched = append(matched, path)
287
}
288
}
289
290
return matched
291
}
292
293
// FilterFunc returns all template paths that match the given filter function.
294
func (i *Index) FilterFunc(fn FilterFunc) []string {
295
if fn == nil {
296
return i.All()
297
}
298
299
i.mu.RLock()
300
defer i.mu.RUnlock()
301
302
var matched []string
303
for path, metadata := range i.cache.All() {
304
if fn(metadata) {
305
matched = append(matched, path)
306
}
307
}
308
309
return matched
310
}
311
312
// All returns all template paths in the index.
313
func (i *Index) All() []string {
314
i.mu.RLock()
315
defer i.mu.RUnlock()
316
317
paths := make([]string, 0, i.cache.EstimatedSize())
318
for path := range i.cache.All() {
319
paths = append(paths, path)
320
}
321
322
return paths
323
}
324
325
// GetAll returns all metadata entries in the index.
326
func (i *Index) GetAll() map[string]*Metadata {
327
i.mu.RLock()
328
defer i.mu.RUnlock()
329
330
result := maps.Collect(i.cache.All())
331
332
return result
333
}
334
335
// Count returns the number of templates matching the filter.
336
func (i *Index) Count(filter *Filter) int {
337
if filter == nil || filter.IsEmpty() {
338
return i.Size()
339
}
340
341
i.mu.RLock()
342
defer i.mu.RUnlock()
343
344
count := 0
345
for _, metadata := range i.cache.All() {
346
if filter.Matches(metadata) {
347
count++
348
}
349
}
350
351
return count
352
}
353
354