summaryrefslogtreecommitdiff
path: root/analytics/ja4.go
blob: 56b885c29a7b0f6fb282342e2988eb8f2b5df1a7 (plain)
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
package analytics

import (
	"cmp"
	_ "embed"
	"log"
	"slices"
	"strings"

	"github.com/gocarina/gocsv"
)

//go:embed ja4plus-mapping.csv
var ja4MappingBytes []byte

type ja4MappingEntry struct {
	Application string `csv:"Application"`
	Library     string `csv:"Library"`
	Device      string `csv:"Device"`
	OS          string `csv:"OS"`
	Ja4         string `csv:"ja4"`
	Ja4s        string `csv:"ja4s"`
	Ja4h        string `csv:"ja4h"`
	Ja4x        string `csv:"ja4x"`
	Ja4t        string `csv:"ja4t"`
	Ja4tscan    string `csv:"ja4tscan"`
	Notes       string `csv:"Notes"`
}

type ClientType float32

const (
	ClientApp     ClientType = 0
	ClientBrowser ClientType = 0
	ClientProxy   ClientType = 0.5
	ClientCLI     ClientType = 1.5
	ClientOS      ClientType = 2
	ClientDevice  ClientType = 1.5
	ClientLibrary ClientType = 2
	ClientMalware ClientType = 3
	ClientUnknown ClientType = 2
)

func (e ja4MappingEntry) Name() (clientName string, clientType ClientType) {
	if app := e.Application; app != "" {
		browsers := []string{"Chromium Browser", "Mozilla Firefox", "Safari"}
		proxies := []string{"Lumma Client", "SoftEther VPN Client", "SoftEther VPN Server", "ngrok 3.8.0"}
		clis := []string{"Curl"}
		malwares := []string{
			"Cobalt Strike beacon",
			"Cobalt Strike Cat C2",
			"Cobalt Strike v4.9.1 beacon",
			"Darkgate Client",
			"IcedID",
			"IcedID Dropper",
			"Pikabot C2",
			"Qakbot C2",
			"Sliver Agent",
			"Sliver/Havoc C2 Server",
		}
		if slices.Contains(browsers, app) {
			return app, ClientBrowser
		}
		if slices.Contains(proxies, app) {
			return app, ClientProxy
		}
		if slices.Contains(clis, app) {
			return app, ClientCLI
		}
		if name := strings.TrimSpace(app); slices.Contains(malwares, name) {
			return name, ClientMalware
		}
		return app, ClientApp
	}

	if lib := e.Library; lib != "" {
		return lib, ClientLibrary
	}

	if dev := e.Device; dev != "" {
		return dev, ClientDevice
	}

	if os := e.OS; os != "" {
		return os, ClientOS
	}

	return cmp.Or(e.Application, e.Library, e.Device, e.OS), ClientUnknown
}

// LookupClient takes a JA4 fingerprint string and
// returns the corresponding HTTP client name string
func LookupClientByJA4(ja4 string) (string, ClientType) {
	var mappings []ja4MappingEntry
	err := gocsv.UnmarshalBytes(ja4MappingBytes, &mappings)
	if err != nil {
		log.Fatalln("error unmarshalling JA4 mapping")
	}

	for _, entry := range mappings {
		if entry.Ja4 == ja4 {
			return entry.Name()
		}
	}

	return "Unknown", ClientUnknown
}