-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathwappalyzer.go
More file actions
127 lines (119 loc) · 3.49 KB
/
Copy pathwappalyzer.go
File metadata and controls
127 lines (119 loc) · 3.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package dsl
import (
"bufio"
"errors"
"fmt"
"net/textproto"
"sort"
"strings"
"sync"
wappalyzer "github.com/projectdiscovery/wappalyzergo"
)
// Lazy singleton: wappalyzergo loads its embedded fingerprint database on
// construction and is safe for concurrent reads thereafter. Initializing
// on first use keeps the cost off the import path for callers that never
// touch the wappalyzer helper.
var (
wappalyzerOnce sync.Once
wappalyzerInstance *wappalyzer.Wappalyze
wappalyzerInitErr error
)
func getWappalyzer() (*wappalyzer.Wappalyze, error) {
wappalyzerOnce.Do(func() {
wappalyzerInstance, wappalyzerInitErr = wappalyzer.New()
})
return wappalyzerInstance, wappalyzerInitErr
}
// parseHeadersForWappalyzer turns a raw HTTP header block into the
// map[string][]string shape wappalyzergo wants. It accepts the textual
// form template authors typically have on hand (the contents of nuclei's
// {{header}} variable). Header names are kept as written - wappalyzergo
// lowercases them internally.
func parseHeadersForWappalyzer(raw string) (map[string][]string, error) {
raw = strings.TrimLeft(raw, "\r\n")
if raw == "" {
return map[string][]string{}, nil
}
if !strings.HasSuffix(raw, "\r\n\r\n") {
if !strings.HasSuffix(raw, "\r\n") {
raw += "\r\n"
}
raw += "\r\n"
}
tp := textproto.NewReader(bufio.NewReader(strings.NewReader(raw)))
mh, err := tp.ReadMIMEHeader()
if err != nil {
return nil, fmt.Errorf("invalid header block: %w", err)
}
return mh, nil
}
func toHeadersAndBody(args []interface{}) (map[string][]string, []byte, error) {
if len(args) != 2 {
return nil, nil, ErrInvalidDslFunction
}
var headerRaw string
switch v := args[0].(type) {
case string:
headerRaw = v
case []byte:
headerRaw = string(v)
case nil:
default:
return nil, nil, errors.New("first argument (headers) must be a string")
}
var body []byte
switch v := args[1].(type) {
case string:
body = []byte(v)
case []byte:
body = v
case nil:
default:
return nil, nil, errors.New("second argument (body) must be a string or bytes")
}
headers, err := parseHeadersForWappalyzer(headerRaw)
if err != nil {
return nil, nil, err
}
return headers, body, nil
}
// fingerprintsAsSortedSlice returns the technology names in a stable
// alphabetical order so that callers (and the DSL result cache) see a
// deterministic value for identical inputs.
func fingerprintsAsSortedSlice(m map[string]struct{}) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
// registerWappalyzerFunction installs the wappalyzer(headers, body)
// helper. It is called from dsl.go's init so the function is present
// before DefaultHelperFunctions is materialized.
//
// wappalyzer(headers, body) []string
//
// Runs wappalyzergo's fingerprint engine against the provided
// response headers and body and returns the detected technology
// names. headers may be the raw HTTP header block as a string (the
// most common form in nuclei templates) or a pre-built byte slice
// with the same content.
func registerWappalyzerFunction() {
MustAddFunction(NewWithSingleSignature(
"wappalyzer",
"(headers, body string) []string",
true,
func(args ...interface{}) (interface{}, error) {
headers, body, err := toHeadersAndBody(args)
if err != nil {
return nil, err
}
w, err := getWappalyzer()
if err != nil {
return nil, fmt.Errorf("wappalyzer init: %w", err)
}
return fingerprintsAsSortedSlice(w.Fingerprint(headers, body)), nil
},
))
}