@anonympins/fingerprint 0.3.3 → 0.3.4
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.
- package/CHANGELOG.md +192 -171
- package/README.md +1080 -1076
- package/composer.json +3 -2
- package/package.json +9 -6
- package/phpunit.xml +2 -1
- package/src/js/fingerprint.js +3733 -3448
- package/src/php/Config/SecurityProfiles.php +17 -7
- package/src/php/FingerprintBuilder.php +185 -184
- package/src/php/FingerprintClient.php +19 -5
- package/src/php/FingerprintEngine.php +863 -850
- package/src/php/RequestContext.php +4 -0
- package/src/php/Store/StoreManager.php +10 -0
- package/src/php/Tests/FingerprintEngineTest.php +299 -218
- package/src/php/Tests/IpReputationTest.php +157 -0
- package/src/php/Utils/BigInt.php +77 -34
- package/src/php/Utils/RequestUtils.php +313 -24
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
+
|
|
7
|
+
use PHPUnit\Framework\TestCase;
|
|
8
|
+
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
9
|
+
use Anonympins\Fingerprint\Store\StoreManager;
|
|
10
|
+
use Anonympins\Fingerprint\Store\IStore;
|
|
11
|
+
use Anonympins\Fingerprint\RequestContext;
|
|
12
|
+
|
|
13
|
+
class IpReputationTest extends TestCase
|
|
14
|
+
{
|
|
15
|
+
private $store;
|
|
16
|
+
|
|
17
|
+
protected function setUp(): void
|
|
18
|
+
{
|
|
19
|
+
parent::setUp();
|
|
20
|
+
|
|
21
|
+
// Un mock anonyme simple et en mémoire du Store pour isoler les tests
|
|
22
|
+
$this->store = new class implements IStore {
|
|
23
|
+
private array $data = [];
|
|
24
|
+
public function get(string $key) { return $this->data[$key] ?? null; }
|
|
25
|
+
public function set(string $key, $value, ?int $ttl = null): void { $this->data[$key] = $value; }
|
|
26
|
+
public function has(string $key): bool { return isset($this->data[$key]); }
|
|
27
|
+
public function delete(string $key): void { unset($this->data[$key]); }
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
StoreManager::setStore($this->store);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public function testGetScoreReturnsZeroForUnknownIp(): void
|
|
34
|
+
{
|
|
35
|
+
$score = RequestUtils::getIpReputationScore('8.8.8.8');
|
|
36
|
+
$this->assertEquals(0.0, $score);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public function testUpdateScoreChangesValueCorrectly(): void
|
|
40
|
+
{
|
|
41
|
+
$ip = '8.8.4.4';
|
|
42
|
+
RequestUtils::updateIpReputationScore($ip, 45.0);
|
|
43
|
+
$this->assertEquals(45.0, RequestUtils::getIpReputationScore($ip));
|
|
44
|
+
|
|
45
|
+
RequestUtils::updateIpReputationScore($ip, -15.0);
|
|
46
|
+
$this->assertEquals(30.0, RequestUtils::getIpReputationScore($ip));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public function testBoundsAreClampedBetween0And100(): void
|
|
50
|
+
{
|
|
51
|
+
$ip = '1.1.1.1';
|
|
52
|
+
RequestUtils::updateIpReputationScore($ip, 120.0);
|
|
53
|
+
$this->assertEquals(100.0, RequestUtils::getIpReputationScore($ip));
|
|
54
|
+
|
|
55
|
+
RequestUtils::updateIpReputationScore($ip, -150.0);
|
|
56
|
+
$this->assertEquals(0.0, RequestUtils::getIpReputationScore($ip));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public function testTimeDecayCalculatesLossOfTwoPointsPerHour(): void
|
|
60
|
+
{
|
|
61
|
+
$ip = '2.2.2.2';
|
|
62
|
+
$now = time();
|
|
63
|
+
|
|
64
|
+
// Simulation d'un score de 80 vieux de 4 heures (4 * 2 = 8 points de perte)
|
|
65
|
+
$this->store->set("ip-reputation:{$ip}", [
|
|
66
|
+
'score' => 80.0,
|
|
67
|
+
'lastUpdate' => $now - 14400
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
$score = RequestUtils::getIpReputationScore($ip);
|
|
71
|
+
$this->assertEquals(72.0, $score);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public function testIpReputationScoreIsIntegratedIntoFinalScore(): void
|
|
75
|
+
{
|
|
76
|
+
$ip = '1.2.3.4';
|
|
77
|
+
RequestUtils::updateIpReputationScore($ip, 60.0);
|
|
78
|
+
|
|
79
|
+
$weights = [
|
|
80
|
+
'ipReputationScore' => 0.5,
|
|
81
|
+
'historyScore' => 0.0,
|
|
82
|
+
'rotationScore' => 0.0,
|
|
83
|
+
'headerAnomalyScore' => 0.0,
|
|
84
|
+
'requestPatternScore' => 0.0,
|
|
85
|
+
'inconsistencyScore' => 0.0,
|
|
86
|
+
'honeypotScore' => 0.0,
|
|
87
|
+
'behaviorScore' => 0.0,
|
|
88
|
+
'botScore' => 0.0,
|
|
89
|
+
'crossLayerInconsistencyScore' => 0.0,
|
|
90
|
+
'tlsSpoofingScore' => 0.0,
|
|
91
|
+
'timeInconsistencyScore' => 0.0,
|
|
92
|
+
'clickVarianceScore' => 0.0,
|
|
93
|
+
'clientHintsInconsistencyScore' => 0.0,
|
|
94
|
+
'subnetScore' => 0.0,
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
$ipRepScore = RequestUtils::getIpReputationScore($ip);
|
|
98
|
+
$this->assertEquals(60.0, $ipRepScore);
|
|
99
|
+
$score = $ipRepScore * $weights['ipReputationScore'];
|
|
100
|
+
$this->assertEquals(30.0, $score);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public function testGetIpSubnetIpv4AndIpv6(): void
|
|
104
|
+
{
|
|
105
|
+
$ipv4Subnet = RequestUtils::getIpSubnet('192.168.1.50', 24, 48);
|
|
106
|
+
$this->assertEquals('192.168.1.0/24', $ipv4Subnet);
|
|
107
|
+
|
|
108
|
+
$ipv6Subnet = RequestUtils::getIpSubnet('2001:db8:abcd:0012::1', 24, 48);
|
|
109
|
+
$this->assertEquals('2001:db8:abcd::/48', $ipv6Subnet);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public function testGetClientHintsInconsistencyScoreMismatch(): void
|
|
113
|
+
{
|
|
114
|
+
$context = $this->createMock(RequestContext::class);
|
|
115
|
+
$context->method('getHeader')->willReturnCallback(function($name) {
|
|
116
|
+
if ($name === 'user-agent') {
|
|
117
|
+
return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/117.0';
|
|
118
|
+
}
|
|
119
|
+
if ($name === 'sec-ch-ua') {
|
|
120
|
+
return '"Google Chrome";v="117"';
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
$score = RequestUtils::getClientHintsInconsistencyScore($context);
|
|
126
|
+
$this->assertEquals(90.0, $score['clientHintsInconsistencyScore']);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public function testGetSubnetScoreCalculations(): void
|
|
130
|
+
{
|
|
131
|
+
$context = $this->createMock(RequestContext::class);
|
|
132
|
+
$context->clientIp = '192.168.1.50';
|
|
133
|
+
|
|
134
|
+
$score = RequestUtils::getSubnetScore($context, 'device-1');
|
|
135
|
+
$this->assertEquals(0.0, $score['subnetScore']);
|
|
136
|
+
|
|
137
|
+
for ($i = 1; $i <= 12; $i++) {
|
|
138
|
+
RequestUtils::updateSubnetMetrics($context, "device-{$i}", 30.0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$scoreWithHistory = RequestUtils::getSubnetScore($context, 'device-1');
|
|
142
|
+
$this->assertGreaterThan(0.0, $scoreWithHistory['subnetScore']);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public function testTlsSpoofingScoreWithJa4(): void
|
|
146
|
+
{
|
|
147
|
+
$context = $this->createMock(RequestContext::class);
|
|
148
|
+
$context->ja4 = 't13d1517h2_8daaf61527d5';
|
|
149
|
+
$context->ja3 = null;
|
|
150
|
+
$context->method('getHeader')->willReturnCallback(function($name) {
|
|
151
|
+
return $name === 'user-agent' ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/117.0' : null;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
$score = RequestUtils::getTlsSpoofingScore($context);
|
|
155
|
+
$this->assertEquals(90.0, $score['tlsSpoofingScore']);
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/php/Utils/BigInt.php
CHANGED
|
@@ -4,59 +4,88 @@ declare(strict_types=1);
|
|
|
4
4
|
|
|
5
5
|
namespace Anonympins\Fingerprint\Utils;
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Classe d'émulation pour les opérations sur de grands entiers (BigInt),
|
|
9
|
-
* similaire au BigInt de JavaScript. Utilise l'extension GMP.
|
|
10
|
-
* @internal
|
|
11
|
-
*/
|
|
12
7
|
class BigInt
|
|
13
8
|
{
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
private static bool $useGmp;
|
|
10
|
+
/** @var \GMP|string */
|
|
11
|
+
private $value;
|
|
16
12
|
|
|
17
13
|
/**
|
|
18
|
-
* @param \GMP|string|int $number
|
|
14
|
+
* @param \GMP|string|int $number Le nombre initial.
|
|
19
15
|
*/
|
|
20
16
|
public function __construct($number)
|
|
21
17
|
{
|
|
22
|
-
if (
|
|
23
|
-
|
|
18
|
+
if (!isset(self::$useGmp)) {
|
|
19
|
+
self::$useGmp = extension_loaded('gmp');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (self::$useGmp) {
|
|
23
|
+
if ($number instanceof \GMP) {
|
|
24
|
+
$this->value = $number;
|
|
25
|
+
} else {
|
|
26
|
+
$this->value = gmp_init($number);
|
|
27
|
+
}
|
|
24
28
|
} else {
|
|
25
|
-
|
|
29
|
+
if ($number instanceof BigInt) {
|
|
30
|
+
$this->value = $number->value;
|
|
31
|
+
} else {
|
|
32
|
+
$this->value = (string)$number;
|
|
33
|
+
}
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/**
|
|
30
38
|
* Crée une instance à partir d'une chaîne hexadécimale.
|
|
31
39
|
*/
|
|
32
|
-
public static function fromHex(string $hex):
|
|
40
|
+
public static function fromHex(string $hex): BigInt
|
|
33
41
|
{
|
|
34
|
-
|
|
42
|
+
if (self::$useGmp) {
|
|
43
|
+
return new self(gmp_init($hex, 16));
|
|
44
|
+
} else {
|
|
45
|
+
$dec = '0';
|
|
46
|
+
$len = strlen($hex);
|
|
47
|
+
for ($i = 0; $i < $len; $i++) {
|
|
48
|
+
$dec = bcadd(bcmul($dec, '16'), (string)hexdec($hex[$i]));
|
|
49
|
+
}
|
|
50
|
+
return new self($dec);
|
|
51
|
+
}
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
/**
|
|
38
55
|
* Compare cette instance avec une autre.
|
|
39
56
|
* @return int < 0 si this < other, 0 si this == other, > 0 si this > other.
|
|
40
57
|
*/
|
|
41
|
-
public function compareTo(
|
|
58
|
+
public function compareTo(BigInt $other): int
|
|
42
59
|
{
|
|
43
|
-
|
|
60
|
+
if (self::$useGmp) {
|
|
61
|
+
return gmp_cmp($this->value, $other->value);
|
|
62
|
+
} else {
|
|
63
|
+
return bccomp($this->value, $other->value);
|
|
64
|
+
}
|
|
44
65
|
}
|
|
45
66
|
|
|
46
67
|
/**
|
|
47
68
|
* Effectue un décalage de bits vers la gauche (<<).
|
|
48
69
|
*/
|
|
49
|
-
public function shiftLeft(int $bits):
|
|
70
|
+
public function shiftLeft(int $bits): BigInt
|
|
50
71
|
{
|
|
51
|
-
|
|
72
|
+
if (self::$useGmp) {
|
|
73
|
+
return new self(gmp_mul($this->value, gmp_pow("2", $bits)));
|
|
74
|
+
} else {
|
|
75
|
+
return new self(bcmul($this->value, bcpow("2", (string)$bits)));
|
|
76
|
+
}
|
|
52
77
|
}
|
|
53
78
|
|
|
54
79
|
/**
|
|
55
80
|
* Effectue un décalage de bits vers la droite (>>).
|
|
56
81
|
*/
|
|
57
|
-
public function shiftRight(int $bits):
|
|
82
|
+
public function shiftRight(int $bits): BigInt
|
|
58
83
|
{
|
|
59
|
-
|
|
84
|
+
if (self::$useGmp) {
|
|
85
|
+
return new self(gmp_div_q($this->value, gmp_pow("2", $bits)));
|
|
86
|
+
} else {
|
|
87
|
+
return new self(bcdiv($this->value, bcpow("2", (string)$bits)));
|
|
88
|
+
}
|
|
60
89
|
}
|
|
61
90
|
|
|
62
91
|
/**
|
|
@@ -64,7 +93,11 @@ class BigInt
|
|
|
64
93
|
*/
|
|
65
94
|
public function __toString(): string
|
|
66
95
|
{
|
|
67
|
-
|
|
96
|
+
if (self::$useGmp) {
|
|
97
|
+
return gmp_strval($this->value);
|
|
98
|
+
} else {
|
|
99
|
+
return $this->value;
|
|
100
|
+
}
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
/**
|
|
@@ -72,31 +105,41 @@ class BigInt
|
|
|
72
105
|
*/
|
|
73
106
|
public function toHex(): string
|
|
74
107
|
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
if (self::$useGmp) {
|
|
109
|
+
return gmp_strval($this->value, 16);
|
|
110
|
+
} else {
|
|
111
|
+
$hex = '';
|
|
112
|
+
$dec = $this->value;
|
|
113
|
+
while (bccomp($dec, '0') > 0) {
|
|
114
|
+
$rem = bcmod($dec, '16');
|
|
115
|
+
$hex = dechex((int)$rem) . $hex;
|
|
116
|
+
$dec = bcdiv($dec, '16');
|
|
117
|
+
}
|
|
118
|
+
return $hex ?: '0';
|
|
119
|
+
}
|
|
85
120
|
}
|
|
86
121
|
|
|
87
122
|
/**
|
|
88
123
|
* Crée une instance à partir d'une puissance de 2.
|
|
89
124
|
*/
|
|
90
|
-
public static function pow(int $base, int $exp):
|
|
125
|
+
public static function pow(int $base, int $exp): BigInt
|
|
91
126
|
{
|
|
92
|
-
|
|
127
|
+
if (self::$useGgmp) {
|
|
128
|
+
return new self(gmp_pow((string)$base, $exp));
|
|
129
|
+
} else {
|
|
130
|
+
return new self(bcpow((string)$base, (string)$exp));
|
|
131
|
+
}
|
|
93
132
|
}
|
|
94
133
|
|
|
95
134
|
/**
|
|
96
135
|
* Soustrait un autre BigInt.
|
|
97
136
|
*/
|
|
98
|
-
public function sub(
|
|
137
|
+
public function sub(BigInt $other): BigInt
|
|
99
138
|
{
|
|
100
|
-
|
|
139
|
+
if (self::$useGmp) {
|
|
140
|
+
return new self(gmp_sub($this->value, $other->value));
|
|
141
|
+
} else {
|
|
142
|
+
return new self(bcsub($this->value, $other->value));
|
|
143
|
+
}
|
|
101
144
|
}
|
|
102
145
|
}
|