diff options
| author | eltrac <hi@eltr.ac> | 2026-11-04 23:26:12 +0800 |
|---|---|---|
| committer | eltrac <hi@eltr.ac> | 2026-11-04 23:26:12 +0800 |
| commit | aa8478f07217b5a44424b5c102220127f8585a1f (patch) | |
| tree | 562d6be0551c2a9c8ff1b457c130c1f896bebd12 /analytics/ja4.go | |
initial commit
project: go mod init
data, reader: add types and roughly implement Bunny.net reader
analytics: basic analyzer, experimental
Diffstat (limited to 'analytics/ja4.go')
| -rw-r--r-- | analytics/ja4.go | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/analytics/ja4.go b/analytics/ja4.go new file mode 100644 index 0000000..56b885c --- /dev/null +++ b/analytics/ja4.go @@ -0,0 +1,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
+}
|
