ja4plus 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ja4plus-0.2.0/LICENSE +28 -0
- ja4plus-0.2.0/MANIFEST.in +3 -0
- ja4plus-0.2.0/PKG-INFO +178 -0
- ja4plus-0.2.0/README.md +140 -0
- ja4plus-0.2.0/ja4plus/__init__.py +30 -0
- ja4plus-0.2.0/ja4plus/collector.py +194 -0
- ja4plus-0.2.0/ja4plus/fingerprinters/__init__.py +23 -0
- ja4plus-0.2.0/ja4plus/fingerprinters/base.py +44 -0
- ja4plus-0.2.0/ja4plus/fingerprinters/ja4.py +284 -0
- ja4plus-0.2.0/ja4plus/fingerprinters/ja4h.py +131 -0
- ja4plus-0.2.0/ja4plus/fingerprinters/ja4l.py +265 -0
- ja4plus-0.2.0/ja4plus/fingerprinters/ja4s.py +144 -0
- ja4plus-0.2.0/ja4plus/fingerprinters/ja4ssh.py +402 -0
- ja4plus-0.2.0/ja4plus/fingerprinters/ja4t.py +88 -0
- ja4plus-0.2.0/ja4plus/fingerprinters/ja4ts.py +90 -0
- ja4plus-0.2.0/ja4plus/fingerprinters/ja4x.py +300 -0
- ja4plus-0.2.0/ja4plus/utils/__init__.py +3 -0
- ja4plus-0.2.0/ja4plus/utils/http_utils.py +181 -0
- ja4plus-0.2.0/ja4plus/utils/ssh_utils.py +221 -0
- ja4plus-0.2.0/ja4plus/utils/tls_utils.py +387 -0
- ja4plus-0.2.0/ja4plus/utils/x509_utils.py +352 -0
- ja4plus-0.2.0/ja4plus.egg-info/PKG-INFO +178 -0
- ja4plus-0.2.0/ja4plus.egg-info/SOURCES.txt +45 -0
- ja4plus-0.2.0/ja4plus.egg-info/dependency_links.txt +1 -0
- ja4plus-0.2.0/ja4plus.egg-info/requires.txt +6 -0
- ja4plus-0.2.0/ja4plus.egg-info/top_level.txt +3 -0
- ja4plus-0.2.0/pyproject.toml +48 -0
- ja4plus-0.2.0/requirements.txt +7 -0
- ja4plus-0.2.0/setup.cfg +4 -0
- ja4plus-0.2.0/setup.py +52 -0
- ja4plus-0.2.0/tests/test_comprehensive.py +944 -0
- ja4plus-0.2.0/tests/test_edge_cases.py +450 -0
- ja4plus-0.2.0/tests/test_integration.py +166 -0
- ja4plus-0.2.0/tests/test_ja4.py +78 -0
- ja4plus-0.2.0/tests/test_ja4_deep.py +448 -0
- ja4plus-0.2.0/tests/test_ja4h.py +68 -0
- ja4plus-0.2.0/tests/test_ja4h_deep.py +318 -0
- ja4plus-0.2.0/tests/test_ja4l.py +107 -0
- ja4plus-0.2.0/tests/test_ja4s.py +158 -0
- ja4plus-0.2.0/tests/test_ja4ssh.py +180 -0
- ja4plus-0.2.0/tests/test_ja4ssh_deep.py +327 -0
- ja4plus-0.2.0/tests/test_ja4t.py +97 -0
- ja4plus-0.2.0/tests/test_ja4t_deep.py +367 -0
- ja4plus-0.2.0/tests/test_ja4ts.py +124 -0
- ja4plus-0.2.0/tests/test_ja4x.py +159 -0
- ja4plus-0.2.0/tests/test_ja4x_deep.py +326 -0
- ja4plus-0.2.0/tests/test_utils.py +505 -0
ja4plus-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, ja4plus contributors
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
ja4plus-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ja4plus
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: JA4+ network fingerprinting library for TLS, TCP, HTTP, SSH, and X.509 analysis
|
|
5
|
+
Home-page: https://github.com/Crank-Git/ja4plus
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/Crank-Git/ja4plus
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/Crank-Git/ja4plus/issues
|
|
9
|
+
Project-URL: Source Code, https://github.com/Crank-Git/ja4plus
|
|
10
|
+
Project-URL: Documentation, https://github.com/Crank-Git/ja4plus/tree/main/docs
|
|
11
|
+
Project-URL: JA4+ Specification, https://github.com/FoxIO-LLC/ja4
|
|
12
|
+
Keywords: ja4,ja4plus,fingerprinting,tls,tcp,http,ssh,x509,network,security,scapy
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: System :: Networking :: Monitoring
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Operating System :: OS Independent
|
|
27
|
+
Requires-Python: >=3.8
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: scapy>=2.4.0
|
|
31
|
+
Requires-Dist: cryptography>=3.4.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov>=3.0; extra == "dev"
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
|
|
39
|
+
# JA4+
|
|
40
|
+
|
|
41
|
+
A Python library for JA4+ network fingerprinting. Implements all eight JA4+ methods for identifying and classifying network traffic based on TLS, TCP, HTTP, SSH, and X.509 characteristics.
|
|
42
|
+
|
|
43
|
+
JA4+ is a set of network fingerprinting standards created by [FoxIO](https://foxio.io). This library is an independent Python implementation of the published specification. For the original spec, see the [FoxIO JA4+ repository](https://github.com/FoxIO-LLC/ja4).
|
|
44
|
+
|
|
45
|
+
## Supported Fingerprint Types
|
|
46
|
+
|
|
47
|
+
| Type | Protocol | Description |
|
|
48
|
+
|------|----------|-------------|
|
|
49
|
+
| JA4 | TLS | Client fingerprint from ClientHello messages |
|
|
50
|
+
| JA4S | TLS | Server fingerprint from ServerHello messages |
|
|
51
|
+
| JA4H | HTTP | Client fingerprint from request headers and cookies |
|
|
52
|
+
| JA4T | TCP | Client OS fingerprint from SYN packets |
|
|
53
|
+
| JA4TS | TCP | Server fingerprint from SYN-ACK packets |
|
|
54
|
+
| JA4L | TCP | Light distance and latency estimation |
|
|
55
|
+
| JA4X | X.509 | Certificate structure fingerprint from OID sequences |
|
|
56
|
+
| JA4SSH | SSH | Session type classification from traffic patterns |
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install ja4plus
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or install from source:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/Crank-Git/ja4plus.git
|
|
68
|
+
cd ja4plus
|
|
69
|
+
pip install -e .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from scapy.all import rdpcap
|
|
76
|
+
from ja4plus import JA4Fingerprinter
|
|
77
|
+
|
|
78
|
+
packets = rdpcap("capture.pcap")
|
|
79
|
+
|
|
80
|
+
fp = JA4Fingerprinter()
|
|
81
|
+
for packet in packets:
|
|
82
|
+
result = fp.process_packet(packet)
|
|
83
|
+
if result:
|
|
84
|
+
print(f"JA4: {result}")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
### Class-Based API
|
|
90
|
+
|
|
91
|
+
Each fingerprinter processes packets and collects results:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from ja4plus import JA4Fingerprinter, JA4SFingerprinter, JA4TFingerprinter
|
|
95
|
+
|
|
96
|
+
ja4 = JA4Fingerprinter()
|
|
97
|
+
ja4s = JA4SFingerprinter()
|
|
98
|
+
ja4t = JA4TFingerprinter()
|
|
99
|
+
|
|
100
|
+
for packet in packets:
|
|
101
|
+
ja4.process_packet(packet)
|
|
102
|
+
ja4s.process_packet(packet)
|
|
103
|
+
ja4t.process_packet(packet)
|
|
104
|
+
|
|
105
|
+
for entry in ja4.get_fingerprints():
|
|
106
|
+
print(entry["fingerprint"])
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Function-Based API
|
|
110
|
+
|
|
111
|
+
For one-shot fingerprinting of individual packets:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from ja4plus import generate_ja4, generate_ja4s, generate_ja4h
|
|
115
|
+
|
|
116
|
+
fingerprint = generate_ja4(packet)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### All Fingerprinters
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from ja4plus import (
|
|
123
|
+
JA4Fingerprinter, # TLS Client
|
|
124
|
+
JA4SFingerprinter, # TLS Server
|
|
125
|
+
JA4HFingerprinter, # HTTP
|
|
126
|
+
JA4TFingerprinter, # TCP Client (SYN)
|
|
127
|
+
JA4TSFingerprinter, # TCP Server (SYN-ACK)
|
|
128
|
+
JA4LFingerprinter, # Latency
|
|
129
|
+
JA4XFingerprinter, # X.509 Certificate
|
|
130
|
+
JA4SSHFingerprinter, # SSH
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
All fingerprinters share a common interface:
|
|
135
|
+
|
|
136
|
+
| Method | Description |
|
|
137
|
+
|--------|-------------|
|
|
138
|
+
| `process_packet(pkt)` | Process a packet, returns fingerprint string or `None` |
|
|
139
|
+
| `get_fingerprints()` | Returns list of all collected fingerprint dicts |
|
|
140
|
+
| `reset()` | Clears all collected state |
|
|
141
|
+
|
|
142
|
+
See [`docs/usage.md`](docs/usage.md) for detailed usage of each fingerprinter.
|
|
143
|
+
|
|
144
|
+
## Fingerprint Formats
|
|
145
|
+
|
|
146
|
+
| Type | Format | Example |
|
|
147
|
+
|------|--------|---------|
|
|
148
|
+
| JA4 | `{proto}{ver}{sni}{ciphcnt}{extcnt}{alpn}_{hash}_{hash}` | `t13d1516h2_8daaf6152771_e5627efa2ab1` |
|
|
149
|
+
| JA4S | `{proto}{ver}{extcnt}{alpn}_{cipher}_{hash}` | `t130200_1301_a56c5b993250` |
|
|
150
|
+
| JA4H | `{method}{ver}{cookie}{ref}{cnt}{lang}_{hash}_{hash}_{hash}` | `ge11cr0800_edb4461d7a83_4817af47a558_...` |
|
|
151
|
+
| JA4T | `{window}_{options}_{mss}_{wscale}` | `65535_2-4-8-1-3_1460_7` |
|
|
152
|
+
| JA4TS | `{window}_{options}_{mss}_{wscale}` | `14600_2-4-8-1-3_1460_0` |
|
|
153
|
+
| JA4L | `{latency_us}_{ttl}` | `2500_56` |
|
|
154
|
+
| JA4X | `{issuer_hash}_{subject_hash}_{ext_hash}` | `a37f49ba31e2_a37f49ba31e2_dd4f1a0ef8b2` |
|
|
155
|
+
| JA4SSH | `c{mode}s{mode}_c{pkts}s{pkts}_c{acks}s{acks}` | `c36s36_c51s80_c69s0` |
|
|
156
|
+
|
|
157
|
+
## Requirements
|
|
158
|
+
|
|
159
|
+
- Python 3.8+
|
|
160
|
+
- [scapy](https://scapy.net/) >= 2.4.0
|
|
161
|
+
- [cryptography](https://cryptography.io/) >= 3.4.0
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
git clone https://github.com/Crank-Git/ja4plus.git
|
|
167
|
+
cd ja4plus
|
|
168
|
+
pip install -e ".[dev]"
|
|
169
|
+
pytest tests/ -v
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
BSD 3-Clause License. See [LICENSE](LICENSE) for details.
|
|
175
|
+
|
|
176
|
+
## Acknowledgments
|
|
177
|
+
|
|
178
|
+
JA4+ was created by John Althouse at [FoxIO](https://foxio.io). This library is an independent implementation of the published specification. For the original spec and reference implementation, see [github.com/FoxIO-LLC/ja4](https://github.com/FoxIO-LLC/ja4).
|
ja4plus-0.2.0/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# JA4+
|
|
2
|
+
|
|
3
|
+
A Python library for JA4+ network fingerprinting. Implements all eight JA4+ methods for identifying and classifying network traffic based on TLS, TCP, HTTP, SSH, and X.509 characteristics.
|
|
4
|
+
|
|
5
|
+
JA4+ is a set of network fingerprinting standards created by [FoxIO](https://foxio.io). This library is an independent Python implementation of the published specification. For the original spec, see the [FoxIO JA4+ repository](https://github.com/FoxIO-LLC/ja4).
|
|
6
|
+
|
|
7
|
+
## Supported Fingerprint Types
|
|
8
|
+
|
|
9
|
+
| Type | Protocol | Description |
|
|
10
|
+
|------|----------|-------------|
|
|
11
|
+
| JA4 | TLS | Client fingerprint from ClientHello messages |
|
|
12
|
+
| JA4S | TLS | Server fingerprint from ServerHello messages |
|
|
13
|
+
| JA4H | HTTP | Client fingerprint from request headers and cookies |
|
|
14
|
+
| JA4T | TCP | Client OS fingerprint from SYN packets |
|
|
15
|
+
| JA4TS | TCP | Server fingerprint from SYN-ACK packets |
|
|
16
|
+
| JA4L | TCP | Light distance and latency estimation |
|
|
17
|
+
| JA4X | X.509 | Certificate structure fingerprint from OID sequences |
|
|
18
|
+
| JA4SSH | SSH | Session type classification from traffic patterns |
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install ja4plus
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or install from source:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/Crank-Git/ja4plus.git
|
|
30
|
+
cd ja4plus
|
|
31
|
+
pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from scapy.all import rdpcap
|
|
38
|
+
from ja4plus import JA4Fingerprinter
|
|
39
|
+
|
|
40
|
+
packets = rdpcap("capture.pcap")
|
|
41
|
+
|
|
42
|
+
fp = JA4Fingerprinter()
|
|
43
|
+
for packet in packets:
|
|
44
|
+
result = fp.process_packet(packet)
|
|
45
|
+
if result:
|
|
46
|
+
print(f"JA4: {result}")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Class-Based API
|
|
52
|
+
|
|
53
|
+
Each fingerprinter processes packets and collects results:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from ja4plus import JA4Fingerprinter, JA4SFingerprinter, JA4TFingerprinter
|
|
57
|
+
|
|
58
|
+
ja4 = JA4Fingerprinter()
|
|
59
|
+
ja4s = JA4SFingerprinter()
|
|
60
|
+
ja4t = JA4TFingerprinter()
|
|
61
|
+
|
|
62
|
+
for packet in packets:
|
|
63
|
+
ja4.process_packet(packet)
|
|
64
|
+
ja4s.process_packet(packet)
|
|
65
|
+
ja4t.process_packet(packet)
|
|
66
|
+
|
|
67
|
+
for entry in ja4.get_fingerprints():
|
|
68
|
+
print(entry["fingerprint"])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Function-Based API
|
|
72
|
+
|
|
73
|
+
For one-shot fingerprinting of individual packets:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from ja4plus import generate_ja4, generate_ja4s, generate_ja4h
|
|
77
|
+
|
|
78
|
+
fingerprint = generate_ja4(packet)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### All Fingerprinters
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from ja4plus import (
|
|
85
|
+
JA4Fingerprinter, # TLS Client
|
|
86
|
+
JA4SFingerprinter, # TLS Server
|
|
87
|
+
JA4HFingerprinter, # HTTP
|
|
88
|
+
JA4TFingerprinter, # TCP Client (SYN)
|
|
89
|
+
JA4TSFingerprinter, # TCP Server (SYN-ACK)
|
|
90
|
+
JA4LFingerprinter, # Latency
|
|
91
|
+
JA4XFingerprinter, # X.509 Certificate
|
|
92
|
+
JA4SSHFingerprinter, # SSH
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
All fingerprinters share a common interface:
|
|
97
|
+
|
|
98
|
+
| Method | Description |
|
|
99
|
+
|--------|-------------|
|
|
100
|
+
| `process_packet(pkt)` | Process a packet, returns fingerprint string or `None` |
|
|
101
|
+
| `get_fingerprints()` | Returns list of all collected fingerprint dicts |
|
|
102
|
+
| `reset()` | Clears all collected state |
|
|
103
|
+
|
|
104
|
+
See [`docs/usage.md`](docs/usage.md) for detailed usage of each fingerprinter.
|
|
105
|
+
|
|
106
|
+
## Fingerprint Formats
|
|
107
|
+
|
|
108
|
+
| Type | Format | Example |
|
|
109
|
+
|------|--------|---------|
|
|
110
|
+
| JA4 | `{proto}{ver}{sni}{ciphcnt}{extcnt}{alpn}_{hash}_{hash}` | `t13d1516h2_8daaf6152771_e5627efa2ab1` |
|
|
111
|
+
| JA4S | `{proto}{ver}{extcnt}{alpn}_{cipher}_{hash}` | `t130200_1301_a56c5b993250` |
|
|
112
|
+
| JA4H | `{method}{ver}{cookie}{ref}{cnt}{lang}_{hash}_{hash}_{hash}` | `ge11cr0800_edb4461d7a83_4817af47a558_...` |
|
|
113
|
+
| JA4T | `{window}_{options}_{mss}_{wscale}` | `65535_2-4-8-1-3_1460_7` |
|
|
114
|
+
| JA4TS | `{window}_{options}_{mss}_{wscale}` | `14600_2-4-8-1-3_1460_0` |
|
|
115
|
+
| JA4L | `{latency_us}_{ttl}` | `2500_56` |
|
|
116
|
+
| JA4X | `{issuer_hash}_{subject_hash}_{ext_hash}` | `a37f49ba31e2_a37f49ba31e2_dd4f1a0ef8b2` |
|
|
117
|
+
| JA4SSH | `c{mode}s{mode}_c{pkts}s{pkts}_c{acks}s{acks}` | `c36s36_c51s80_c69s0` |
|
|
118
|
+
|
|
119
|
+
## Requirements
|
|
120
|
+
|
|
121
|
+
- Python 3.8+
|
|
122
|
+
- [scapy](https://scapy.net/) >= 2.4.0
|
|
123
|
+
- [cryptography](https://cryptography.io/) >= 3.4.0
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone https://github.com/Crank-Git/ja4plus.git
|
|
129
|
+
cd ja4plus
|
|
130
|
+
pip install -e ".[dev]"
|
|
131
|
+
pytest tests/ -v
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
BSD 3-Clause License. See [LICENSE](LICENSE) for details.
|
|
137
|
+
|
|
138
|
+
## Acknowledgments
|
|
139
|
+
|
|
140
|
+
JA4+ was created by John Althouse at [FoxIO](https://foxio.io). This library is an independent implementation of the published specification. For the original spec and reference implementation, see [github.com/FoxIO-LLC/ja4](https://github.com/FoxIO-LLC/ja4).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JA4+ - Network Fingerprinting Library
|
|
3
|
+
|
|
4
|
+
A Python implementation of the JA4+ network fingerprinting methods
|
|
5
|
+
created by FoxIO LLC. Supports TLS, TCP, HTTP, SSH, and X.509
|
|
6
|
+
fingerprinting for network security monitoring and traffic analysis.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ja4plus.fingerprinters.ja4 import JA4Fingerprinter
|
|
10
|
+
from ja4plus.fingerprinters.ja4s import JA4SFingerprinter
|
|
11
|
+
from ja4plus.fingerprinters.ja4h import JA4HFingerprinter
|
|
12
|
+
from ja4plus.fingerprinters.ja4l import JA4LFingerprinter
|
|
13
|
+
from ja4plus.fingerprinters.ja4x import JA4XFingerprinter
|
|
14
|
+
from ja4plus.fingerprinters.ja4ssh import JA4SSHFingerprinter
|
|
15
|
+
from ja4plus.fingerprinters.ja4t import JA4TFingerprinter
|
|
16
|
+
from ja4plus.fingerprinters.ja4ts import JA4TSFingerprinter
|
|
17
|
+
|
|
18
|
+
# Function-based API
|
|
19
|
+
from ja4plus.fingerprinters.ja4 import generate_ja4
|
|
20
|
+
from ja4plus.fingerprinters.ja4s import generate_ja4s
|
|
21
|
+
from ja4plus.fingerprinters.ja4h import generate_ja4h
|
|
22
|
+
from ja4plus.fingerprinters.ja4l import generate_ja4l
|
|
23
|
+
from ja4plus.fingerprinters.ja4x import generate_ja4x
|
|
24
|
+
from ja4plus.fingerprinters.ja4ssh import generate_ja4ssh
|
|
25
|
+
from ja4plus.fingerprinters.ja4t import generate_ja4t
|
|
26
|
+
from ja4plus.fingerprinters.ja4ts import generate_ja4ts
|
|
27
|
+
|
|
28
|
+
__version__ = "0.2.0"
|
|
29
|
+
__author__ = "ja4plus contributors"
|
|
30
|
+
__license__ = "BSD-3-Clause"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
JA4+ collector for Elastic Agent integration.
|
|
4
|
+
Captures network traffic and outputs JA4+ fingerprints as JSON.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import signal
|
|
12
|
+
from scapy.all import sniff
|
|
13
|
+
|
|
14
|
+
from ja4plus.fingerprinters.ja4 import JA4Fingerprinter
|
|
15
|
+
from ja4plus.fingerprinters.ja4s import JA4SFingerprinter
|
|
16
|
+
from ja4plus.fingerprinters.ja4h import JA4HFingerprinter
|
|
17
|
+
from ja4plus.fingerprinters.ja4t import JA4TFingerprinter
|
|
18
|
+
from ja4plus.fingerprinters.ja4ts import JA4TSFingerprinter
|
|
19
|
+
from ja4plus.fingerprinters.ja4x import JA4XFingerprinter
|
|
20
|
+
from ja4plus.fingerprinters.ja4ssh import JA4SSHFingerprinter
|
|
21
|
+
|
|
22
|
+
# Global variables for signal handling
|
|
23
|
+
packet_count = 0
|
|
24
|
+
max_packets = 0
|
|
25
|
+
start_time = 0
|
|
26
|
+
timeout_seconds = 0
|
|
27
|
+
|
|
28
|
+
def signal_handler(signum, frame):
|
|
29
|
+
"""Handle termination signals gracefully"""
|
|
30
|
+
sys.stderr.write("Received signal, shutting down gracefully...\n")
|
|
31
|
+
sys.exit(0)
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
"""Main collector function"""
|
|
35
|
+
global packet_count, max_packets, start_time, timeout_seconds
|
|
36
|
+
|
|
37
|
+
parser = argparse.ArgumentParser(description="JA4+ Network Fingerprinting Collector")
|
|
38
|
+
parser.add_argument("--interface", "-i", default="any", help="Network interface to capture on")
|
|
39
|
+
parser.add_argument("--filter", "-f", default="tcp or udp", help="BPF filter expression")
|
|
40
|
+
parser.add_argument("--fingerprinters", default="ja4,ja4s,ja4h,ja4t,ja4ts,ja4x,ja4ssh",
|
|
41
|
+
help="Comma-separated list of fingerprinters to enable")
|
|
42
|
+
parser.add_argument("--max-packets", type=int, default=0,
|
|
43
|
+
help="Maximum packets to process (0 = unlimited)")
|
|
44
|
+
parser.add_argument("--timeout", type=float, default=0,
|
|
45
|
+
help="Timeout in seconds (0 = no timeout)")
|
|
46
|
+
parser.add_argument("--output", choices=["json", "text"], default="json",
|
|
47
|
+
help="Output format")
|
|
48
|
+
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
51
|
+
# Set up signal handlers
|
|
52
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
53
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
54
|
+
|
|
55
|
+
# Initialize global variables
|
|
56
|
+
max_packets = args.max_packets
|
|
57
|
+
timeout_seconds = args.timeout
|
|
58
|
+
start_time = time.time()
|
|
59
|
+
|
|
60
|
+
# Initialize enabled fingerprinters
|
|
61
|
+
available_fingerprinters = {
|
|
62
|
+
"ja4": JA4Fingerprinter(),
|
|
63
|
+
"ja4s": JA4SFingerprinter(),
|
|
64
|
+
"ja4h": JA4HFingerprinter(),
|
|
65
|
+
"ja4t": JA4TFingerprinter(),
|
|
66
|
+
"ja4ts": JA4TSFingerprinter(),
|
|
67
|
+
"ja4x": JA4XFingerprinter(),
|
|
68
|
+
"ja4ssh": JA4SSHFingerprinter()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
enabled_fps = args.fingerprinters.split(",")
|
|
72
|
+
fingerprinters = {}
|
|
73
|
+
for name in enabled_fps:
|
|
74
|
+
name = name.strip()
|
|
75
|
+
if name in available_fingerprinters:
|
|
76
|
+
fingerprinters[name] = available_fingerprinters[name]
|
|
77
|
+
else:
|
|
78
|
+
sys.stderr.write(f"Warning: Unknown fingerprinter '{name}'\n")
|
|
79
|
+
|
|
80
|
+
if not fingerprinters:
|
|
81
|
+
sys.stderr.write("Error: No valid fingerprinters enabled\n")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
def process_packet(packet):
|
|
85
|
+
"""Process packet with enabled fingerprinters and output results"""
|
|
86
|
+
global packet_count
|
|
87
|
+
|
|
88
|
+
# Check packet limit
|
|
89
|
+
if max_packets > 0 and packet_count >= max_packets:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Check timeout
|
|
93
|
+
if timeout_seconds > 0 and (time.time() - start_time) >= timeout_seconds:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
packet_count += 1
|
|
97
|
+
|
|
98
|
+
results = {
|
|
99
|
+
"@timestamp": int(time.time() * 1000), # Elasticsearch expects milliseconds
|
|
100
|
+
"ja4plus": {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Apply each enabled fingerprinter
|
|
104
|
+
for name, fp in fingerprinters.items():
|
|
105
|
+
try:
|
|
106
|
+
fingerprint = fp.process_packet(packet)
|
|
107
|
+
if fingerprint:
|
|
108
|
+
results["ja4plus"][name] = fingerprint
|
|
109
|
+
except Exception as e:
|
|
110
|
+
sys.stderr.write(f"Error in {name} fingerprinter: {e}\n")
|
|
111
|
+
|
|
112
|
+
# Only output if we have fingerprints
|
|
113
|
+
if results["ja4plus"]:
|
|
114
|
+
# Add network metadata - properly extract IP addresses
|
|
115
|
+
try:
|
|
116
|
+
from scapy.all import IP, IPv6
|
|
117
|
+
|
|
118
|
+
# Look for IP layer (IPv4 or IPv6)
|
|
119
|
+
if packet.haslayer(IP):
|
|
120
|
+
ip_layer = packet[IP]
|
|
121
|
+
results["source"] = {"ip": ip_layer.src}
|
|
122
|
+
results["destination"] = {"ip": ip_layer.dst}
|
|
123
|
+
elif packet.haslayer(IPv6):
|
|
124
|
+
ip_layer = packet[IPv6]
|
|
125
|
+
results["source"] = {"ip": ip_layer.src}
|
|
126
|
+
results["destination"] = {"ip": ip_layer.dst}
|
|
127
|
+
elif hasattr(packet, "src") and hasattr(packet, "dst"):
|
|
128
|
+
# Fallback to packet-level src/dst (might be MAC addresses)
|
|
129
|
+
# Only use if they look like IP addresses
|
|
130
|
+
src_str = str(packet.src)
|
|
131
|
+
dst_str = str(packet.dst)
|
|
132
|
+
if ":" not in src_str or len(src_str.split(":")) <= 4: # Not a MAC address
|
|
133
|
+
results["source"] = {"ip": src_str}
|
|
134
|
+
results["destination"] = {"ip": dst_str}
|
|
135
|
+
|
|
136
|
+
# Add port information if available
|
|
137
|
+
from scapy.all import TCP, UDP
|
|
138
|
+
if packet.haslayer(TCP):
|
|
139
|
+
tcp_layer = packet[TCP]
|
|
140
|
+
if "source" in results:
|
|
141
|
+
results["source"]["port"] = tcp_layer.sport
|
|
142
|
+
if "destination" in results:
|
|
143
|
+
results["destination"]["port"] = tcp_layer.dport
|
|
144
|
+
elif packet.haslayer(UDP):
|
|
145
|
+
udp_layer = packet[UDP]
|
|
146
|
+
if "source" in results:
|
|
147
|
+
results["source"]["port"] = udp_layer.sport
|
|
148
|
+
if "destination" in results:
|
|
149
|
+
results["destination"]["port"] = udp_layer.dport
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
sys.stderr.write(f"Error extracting network metadata: {e}\n")
|
|
153
|
+
|
|
154
|
+
# Add collection metadata
|
|
155
|
+
results["ja4plus"]["interface"] = args.interface
|
|
156
|
+
results["ja4plus"]["filter"] = args.filter
|
|
157
|
+
results["ja4plus"]["collection_timestamp"] = int(time.time() * 1000)
|
|
158
|
+
|
|
159
|
+
# Output based on format
|
|
160
|
+
if args.output == "json":
|
|
161
|
+
print(json.dumps(results))
|
|
162
|
+
else:
|
|
163
|
+
print(f"Packet {packet_count}: {results}")
|
|
164
|
+
|
|
165
|
+
sys.stdout.flush()
|
|
166
|
+
|
|
167
|
+
# Stop if we've reached the packet limit
|
|
168
|
+
if max_packets > 0 and packet_count >= max_packets:
|
|
169
|
+
sys.stderr.write(f"Reached packet limit of {max_packets}, stopping...\n")
|
|
170
|
+
sys.exit(0)
|
|
171
|
+
|
|
172
|
+
# Stop if we've reached the timeout
|
|
173
|
+
if timeout_seconds > 0 and (time.time() - start_time) >= timeout_seconds:
|
|
174
|
+
sys.stderr.write(f"Reached timeout of {timeout_seconds} seconds, stopping...\n")
|
|
175
|
+
sys.exit(0)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
sys.stderr.write(f"Starting JA4+ collection on interface '{args.interface}' with filter '{args.filter}'\n")
|
|
179
|
+
sys.stderr.write(f"Enabled fingerprinters: {', '.join(fingerprinters.keys())}\n")
|
|
180
|
+
|
|
181
|
+
# Start sniffing
|
|
182
|
+
sniff(prn=process_packet,
|
|
183
|
+
filter=args.filter,
|
|
184
|
+
iface=args.interface if args.interface != "any" else None,
|
|
185
|
+
store=0)
|
|
186
|
+
|
|
187
|
+
except KeyboardInterrupt:
|
|
188
|
+
sys.stderr.write("Interrupted by user\n")
|
|
189
|
+
except Exception as e:
|
|
190
|
+
sys.stderr.write(f"Error during packet capture: {e}\n")
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
main()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JA4+ fingerprinters for various protocols and traffic types.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .ja4 import JA4Fingerprinter
|
|
6
|
+
from .ja4s import JA4SFingerprinter
|
|
7
|
+
from .ja4h import JA4HFingerprinter
|
|
8
|
+
from .ja4l import JA4LFingerprinter
|
|
9
|
+
from .ja4x import JA4XFingerprinter
|
|
10
|
+
from .ja4ssh import JA4SSHFingerprinter
|
|
11
|
+
from .ja4t import JA4TFingerprinter
|
|
12
|
+
from .ja4ts import JA4TSFingerprinter
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'JA4Fingerprinter',
|
|
16
|
+
'JA4SFingerprinter',
|
|
17
|
+
'JA4HFingerprinter',
|
|
18
|
+
'JA4LFingerprinter',
|
|
19
|
+
'JA4XFingerprinter',
|
|
20
|
+
'JA4SSHFingerprinter',
|
|
21
|
+
'JA4TFingerprinter',
|
|
22
|
+
'JA4TSFingerprinter'
|
|
23
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base fingerprinter class for JA4+ fingerprinters.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class BaseFingerprinter:
|
|
6
|
+
"""Base class for all JA4+ fingerprinters."""
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
"""Initialize the fingerprinter."""
|
|
10
|
+
self.fingerprints = []
|
|
11
|
+
|
|
12
|
+
def process_packet(self, packet):
|
|
13
|
+
"""
|
|
14
|
+
Process a packet and extract fingerprint if applicable.
|
|
15
|
+
This method should be overridden by subclasses.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
packet: A network packet to analyze
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The extracted fingerprint if successful, None otherwise
|
|
22
|
+
"""
|
|
23
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
24
|
+
|
|
25
|
+
def add_fingerprint(self, fingerprint, packet):
|
|
26
|
+
"""
|
|
27
|
+
Add a fingerprint to the collection.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
fingerprint: The extracted fingerprint
|
|
31
|
+
packet: The packet that generated this fingerprint
|
|
32
|
+
"""
|
|
33
|
+
self.fingerprints.append({
|
|
34
|
+
'fingerprint': fingerprint,
|
|
35
|
+
'packet': packet
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
def get_fingerprints(self):
|
|
39
|
+
"""Return all collected fingerprints."""
|
|
40
|
+
return self.fingerprints
|
|
41
|
+
|
|
42
|
+
def reset(self):
|
|
43
|
+
"""Reset the fingerprinter state."""
|
|
44
|
+
self.fingerprints = []
|