@anonympins/fingerprint 0.5.4 → 0.5.5

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.
@@ -9,6 +9,15 @@
9
9
 
10
10
  'use strict';
11
11
 
12
+ function secureRandom() {
13
+ if (typeof window !== 'undefined' && (window.crypto || window.msCrypto)) {
14
+ const array = new Uint32Array(1);
15
+ (window.crypto || window.msCrypto).getRandomValues(array);
16
+ return array[0] / 0x100000000;
17
+ }
18
+ return Math.random();
19
+ }
20
+
12
21
  function cyrb53(str, seed = 0) {
13
22
  const safeStr = (typeof str === 'string' ? str : String(str || '')).slice(0, 10000);
14
23
  let h1 = 0xdeadbeef ^ seed,
@@ -518,7 +527,7 @@ const ClientOptimizers = {
518
527
  for (let i = 0; i < iterations; i++) {
519
528
  const newSolution = neighbor(currentSolution);
520
529
  const newEnergy = evaluator(newSolution);
521
- if (newEnergy < currentEnergy || Math.random() < Math.exp((currentEnergy - newEnergy) / temperature)) {
530
+ if (newEnergy < currentEnergy || secureRandom() < Math.exp((currentEnergy - newEnergy) / temperature)) {
522
531
  currentSolution = newSolution;
523
532
  currentEnergy = newEnergy;
524
533
  }
@@ -537,10 +546,10 @@ const ClientOptimizers = {
537
546
  population.sort((a, b) => a.fitness - b.fitness);
538
547
  const newPopulation = [population[0]]; // Elitism
539
548
  while (newPopulation.length < popSize) {
540
- const p1 = population[Math.floor(Math.random() * (popSize / 2))];
541
- const p2 = population[Math.floor(Math.random() * (popSize / 2))];
549
+ const p1 = population[Math.floor(secureRandom() * (popSize / 2))];
550
+ const p2 = population[Math.floor(secureRandom() * (popSize / 2))];
542
551
  let offspring = crossover(p1.chromosome, p2.chromosome);
543
- if (Math.random() < 0.1) offspring = mutate(offspring);
552
+ if (secureRandom() < 0.1) offspring = mutate(offspring);
544
553
  newPopulation.push({ chromosome: offspring, fitness: fitness(offspring) });
545
554
  }
546
555
  population = newPopulation;
@@ -561,10 +570,10 @@ const ClientOptimizers = {
561
570
  for (let gen = 0; gen < generations; gen++) {
562
571
  const offspring = [];
563
572
  for (let i = 0; i < populationSize; i++) {
564
- const parent1 = population[Math.floor(Math.random() * population.length)];
565
- const parent2 = population[Math.floor(Math.random() * population.length)];
573
+ const parent1 = population[Math.floor(secureRandom() * population.length)];
574
+ const parent2 = population[Math.floor(secureRandom() * population.length)];
566
575
  let childIndividual = crossover(parent1.individual, parent2.individual);
567
- if (Math.random() < mutationRate) {
576
+ if (secureRandom() < mutationRate) {
568
577
  childIndividual = mutate(childIndividual);
569
578
  }
570
579
  const child = { individual: childIndividual };
@@ -632,9 +641,9 @@ const ClientOptimizers = {
632
641
  return [-platformRevenue, -advertiserValue, marketImbalance];
633
642
  };
634
643
 
635
- const createIndividual = () => 0.5 + Math.random() * 4.5;
644
+ const createIndividual = () => 0.5 + secureRandom() * 4.5;
636
645
  const crossover = (cpc1, cpc2) => (cpc1 + cpc2) / 2;
637
- const mutate = (cpc) => Math.max(0.1, cpc + (Math.random() - 0.5) * 0.5);
646
+ const mutate = (cpc) => Math.max(0.1, cpc + (secureRandom() - 0.5) * 0.5);
638
647
 
639
648
  const gaOptions = {
640
649
  generations: 50,
@@ -738,10 +747,10 @@ const ClientOptimizers = {
738
747
  };
739
748
 
740
749
  const createIndividual = () => [
741
- 100 + Math.random() * 4900,
742
- 1 + Math.random() * 9999,
743
- Math.random() * 0.5,
744
- Math.floor(Math.random() * 10)
750
+ 100 + secureRandom() * 4900,
751
+ 1 + secureRandom() * 9999,
752
+ secureRandom() * 0.5,
753
+ Math.floor(secureRandom() * 10)
745
754
  ];
746
755
 
747
756
  const crossover = (s1, s2) => [
@@ -755,7 +764,7 @@ const ClientOptimizers = {
755
764
  const newSolution = [...solution];
756
765
  const i = Math.floor(Math.random() * 4);
757
766
  const mutationFactors = [500, 1000, 0.1, 2];
758
- newSolution[i] += (Math.random() - 0.5) * mutationFactors[i];
767
+ newSolution[i] += (secureRandom() - 0.5) * mutationFactors[i];
759
768
  return newSolution;
760
769
  };
761
770
 
@@ -809,16 +818,16 @@ async function solveUsefulWorkTask(task) {
809
818
  };
810
819
  const neighbor = (facilities) => {
811
820
  const newFacilities = facilities.map((f) => ({ ...f }));
812
- const i = Math.floor(Math.random() * numFacilities);
813
- const moveX = (Math.random() - 0.5) * (bounds.maxX - bounds.minX) * 0.1;
814
- const moveY = (Math.random() - 0.5) * (bounds.maxY - bounds.minY) * 0.1;
821
+ const i = Math.floor(secureRandom() * numFacilities);
822
+ const moveX = (secureRandom() - 0.5) * (bounds.maxX - bounds.minX) * 0.1;
823
+ const moveY = (secureRandom() - 0.5) * (bounds.maxY - bounds.minY) * 0.1;
815
824
  newFacilities[i].x = Math.max(bounds.minX, Math.min(bounds.maxX, newFacilities[i].x + moveX));
816
825
  newFacilities[i].y = Math.max(bounds.minY, Math.min(bounds.maxY, newFacilities[i].y + moveY));
817
826
  return newFacilities;
818
827
  };
819
828
  const initialSolution = task.initialSolution || Array.from({ length: numFacilities }, () => ({
820
- x: bounds.minX + Math.random() * (bounds.maxX - bounds.minX),
821
- y: bounds.minY + Math.random() * (bounds.maxY - bounds.minY),
829
+ x: bounds.minX + secureRandom() * (bounds.maxX - bounds.minX),
830
+ y: bounds.minY + secureRandom() * (bounds.maxY - bounds.minY),
822
831
  }));
823
832
  return ClientOptimizers.simulatedAnnealing(initialSolution, evaluator, neighbor, task.iterations, task.payload.options.initialTemperature, task.payload.options.coolingRate);
824
833
  }
@@ -833,11 +842,11 @@ async function solveUsefulWorkTask(task) {
833
842
  };
834
843
  const neighbor = (path) => {
835
844
  const newPath = [...path];
836
- const [i, j] = [Math.floor(Math.random() * path.length), Math.floor(Math.random() * path.length)];
845
+ const [i, j] = [Math.floor(secureRandom() * path.length), Math.floor(secureRandom() * path.length)];
837
846
  [newPath[i], newPath[j]] = [newPath[j], newPath[i]];
838
847
  return newPath;
839
848
  };
840
- const initialSolution = task.initialSolution || Array.from({ length: cities.length }, (_, i) => i).sort(() => 0.5 - Math.random());
849
+ const initialSolution = task.initialSolution || Array.from({ length: cities.length }, (_, i) => i).sort(() => 0.5 - secureRandom());
841
850
 
842
851
  return ClientOptimizers.simulatedAnnealing(initialSolution, evaluator, neighbor, task.iterations, task.payload.options.initialTemperature, task.payload.options.coolingRate);
843
852
  }
@@ -853,9 +862,9 @@ async function solveUsefulWorkTask(task) {
853
862
  if (vol > maxVolatility) return 1000 + (vol - maxVolatility);
854
863
  return -ret;
855
864
  };
856
- const createIndividual = () => Array.from({ length: assets.length }, Math.random);
865
+ const createIndividual = () => Array.from({ length: assets.length }, secureRandom);
857
866
  const crossover = (p1, p2) => p1.map((w, i) => (w + p2[i]) / 2);
858
- const mutate = p => { const n = [...p], i = Math.floor(Math.random() * n.length); n[i] += (Math.random() - 0.5) * 0.2; return n.map(v => Math.max(0, v)); };
867
+ const mutate = p => { const n = [...p], i = Math.floor(secureRandom() * n.length); n[i] += (secureRandom() - 0.5) * 0.2; return n.map(v => Math.max(0, v)); };
859
868
 
860
869
  // Le client doit recréer la population si elle n'est pas fournie
861
870
  const initialPopulation = task.initialPopulation || Array.from({ length: task.payload.options.populationSize }, () => ({ chromosome: createIndividual(), fitness: 0 }));
@@ -907,8 +916,8 @@ export async function solveOptimizationTask(initialPopulation, generations) {
907
916
  const crossover = (p1, p2) => p1.map((w, i) => (w + p2[i]) / 2);
908
917
  const mutate = (p) => {
909
918
  const newP = [...p];
910
- const i = Math.floor(Math.random() * newP.length);
911
- newP[i] += (Math.random() - 0.5) * 0.2;
919
+ const i = Math.floor(secureRandom() * newP.length);
920
+ newP[i] += (secureRandom() - 0.5) * 0.2;
912
921
  return newP;
913
922
  };
914
923
 
@@ -920,10 +929,10 @@ export async function solveOptimizationTask(initialPopulation, generations) {
920
929
  const newPopulation = [...parents]; // Élitisme
921
930
 
922
931
  while (newPopulation.length < population.length) {
923
- const parent1 = parents[Math.floor(Math.random() * parents.length)];
924
- const parent2 = parents[Math.floor(Math.random() * parents.length)];
932
+ const parent1 = parents[Math.floor(secureRandom() * parents.length)];
933
+ const parent2 = parents[Math.floor(secureRandom() * parents.length)];
925
934
  let offspring = crossover(parent1.chromosome, parent2.chromosome);
926
- if (Math.random() < 0.1) offspring = mutate(offspring);
935
+ if (secureRandom() < 0.1) offspring = mutate(offspring);
927
936
  // La fitness sera recalculée côté serveur pour la vérification.
928
937
  newPopulation.push({ chromosome: offspring, fitness: -1 });
929
938
  }
@@ -0,0 +1,175 @@
1
+ import { describe, expect, it, beforeAll } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const fpJsPath = path.resolve(__dirname, '../../../public/fp.js');
9
+ const fpWasmPath = path.resolve(__dirname, '../../../public/fp.wasm');
10
+
11
+ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', () => {
12
+ let wasmModule = null;
13
+
14
+ beforeAll(async () => {
15
+ // Essai de compilation WASM à la volée s'il y a em++
16
+ try {
17
+ execSync('node src/js/build-client.js', { stdio: 'ignore' });
18
+ } catch (e) {
19
+ // Ignorer si Emscripten n'est pas présent dans l'env
20
+ }
21
+
22
+ // Instancier le WASM réel si compilé
23
+ if (fs.existsSync(fpJsPath) && fs.existsSync(fpWasmPath)) {
24
+ try {
25
+ const module = await import(fpJsPath);
26
+ const createFingerprintModule = module.default || module.createFingerprintModule;
27
+ if (typeof createFingerprintModule === 'function') {
28
+ wasmModule = await createFingerprintModule({
29
+ locateFile: () => fpWasmPath
30
+ });
31
+ }
32
+ } catch (e) {
33
+ try {
34
+ const require = (await import('module')).createRequire(import.meta.url);
35
+ const createFingerprintModule = require(fpJsPath);
36
+ wasmModule = await createFingerprintModule({
37
+ locateFile: () => fpWasmPath
38
+ });
39
+ } catch (err) {
40
+ console.warn('WASM could not be initialized in test:', err);
41
+ }
42
+ }
43
+ }
44
+ });
45
+
46
+ function jsHashSeedToFloat(seed) {
47
+ let hash = 0;
48
+ for (let i = 0; i < seed.length; i++) {
49
+ hash = (hash << 5) - hash + seed.charCodeAt(i);
50
+ hash |= 0;
51
+ }
52
+ return Math.abs(hash % 1000000) / 1000000;
53
+ }
54
+
55
+ function jsGenerateTrajectory(seed, iterations) {
56
+ const numericSeed = jsHashSeedToFloat(seed);
57
+ const r = 3.9999;
58
+ const rFloat = Math.fround(r);
59
+ const output = new Float32Array(64);
60
+ for (let idx = 0; idx < 64; idx++) {
61
+ let x = Math.fround(numericSeed + idx * 0.015);
62
+ for (let i = 0; i < iterations; i++) {
63
+ x = Math.fround(rFloat * x * Math.fround(1.0 - x));
64
+ }
65
+ output[idx] = x;
66
+ }
67
+ return output;
68
+ }
69
+
70
+ function runPhpTrajectory(seed, iterations) {
71
+ const phpCode = `<?php
72
+ error_reporting(E_ALL);
73
+ ini_set('display_errors', '1');
74
+
75
+ \$seed = ${JSON.stringify(seed)};
76
+ \$iterations = ${parseInt(iterations, 10)};
77
+
78
+ function fround(\$value) {
79
+ \$unpacked = unpack('f', pack('f', \$value));
80
+ return \$unpacked ? \$unpacked[1] : \$value;
81
+ }
82
+
83
+ function hashSeedToFloat(\$seed) {
84
+ \$hash = 0;
85
+ \$len = strlen(\$seed);
86
+ for (\$i = 0; \$i < \$len; \$i++) {
87
+ \$hash = ((\$hash << 5) - \$hash + ord(\$seed[\$i])) & 0xffffffff;
88
+ if (\$hash & 0x80000000) {
89
+ \$hash = \$hash - 0x100000000;
90
+ }
91
+ }
92
+ return abs(\$hash % 1000000) / 1000000;
93
+ }
94
+
95
+ \$numericSeed = hashSeedToFloat(\$seed);
96
+ \$r = 3.9999;
97
+ \$solutions = [];
98
+ for (\$idx = 0; \$idx < 64; \$idx++) {
99
+ \$x = fround(\$numericSeed + \$idx * 0.015);
100
+ \$rFloat = fround(\$r);
101
+ for (\$i = 0; \$i < \$iterations; \$i++) {
102
+ \$x = fround(\$rFloat * \$x * fround(1.0 - \$x));
103
+ }
104
+ \$solutions[] = \$x;
105
+ }
106
+
107
+ \$sanitized = [];
108
+ foreach (\$solutions as \$val) {
109
+ if (is_nan(\$val)) {
110
+ \$sanitized[] = "NaN";
111
+ } elseif (is_infinite(\$val)) {
112
+ \$sanitized[] = \$val > 0 ? "Infinity" : "-Infinity";
113
+ } else {
114
+ \$sanitized[] = \$val;
115
+ }
116
+ }
117
+
118
+ echo json_encode(\$sanitized);
119
+ `;
120
+
121
+ const tempFile = path.resolve(__dirname, 'temp_trajectory.php').replace(/\\/g, '/');
122
+ fs.writeFileSync(tempFile, phpCode);
123
+ const command = `php -d display_errors=1 "${tempFile}" 2>&1`;
124
+ try {
125
+ const output = execSync(command).toString();
126
+ const parsed = JSON.parse(output.trim());
127
+ return parsed.map(v => {
128
+ if (v === "NaN") return NaN;
129
+ if (v === "Infinity") return Infinity;
130
+ if (v === "-Infinity") return -Infinity;
131
+ return v;
132
+ });
133
+ } finally {
134
+ if (fs.existsSync(tempFile)) {
135
+ fs.unlinkSync(tempFile);
136
+ }
137
+ }
138
+ }
139
+
140
+ it('should verify bit-to-bit convergence across 1,000 different seeds', () => {
141
+ const iterations = 100;
142
+ for (let i = 0; i < 50; i++) {
143
+ const seed = `seed_parity_val_${i}`;
144
+ const jsTrajectory = jsGenerateTrajectory(seed, iterations);
145
+ const phpTrajectory = runPhpTrajectory(seed, iterations);
146
+
147
+ for (let idx = 0; idx < 64; idx++) {
148
+ const jsVal = Math.fround(jsTrajectory[idx]);
149
+ const phpVal = Math.fround(phpTrajectory[idx]);
150
+ expect(jsVal).toBe(phpVal);
151
+ }
152
+
153
+ if (wasmModule && typeof wasmModule._generate_gpu_pow_trajectory === 'function') {
154
+ const outputPtr = wasmModule._malloc(256);
155
+ const seedPtr = wasmModule._malloc(seed.length + 1);
156
+ for (let s = 0; s < seed.length; s++) {
157
+ wasmModule.HEAP8[seedPtr + s] = seed.charCodeAt(s);
158
+ }
159
+ wasmModule.HEAP8[seedPtr + seed.length] = 0;
160
+
161
+ wasmModule._generate_gpu_pow_trajectory(seedPtr, iterations, outputPtr);
162
+
163
+ const wasmOutput = new Float32Array(wasmModule.HEAPF32.buffer, outputPtr, 64);
164
+ for (let idx = 0; idx < 64; idx++) {
165
+ const jsVal = Math.fround(jsTrajectory[idx]);
166
+ const wasmVal = Math.fround(wasmOutput[idx]);
167
+ expect(jsVal).toBe(wasmVal);
168
+ }
169
+
170
+ wasmModule._free(outputPtr);
171
+ wasmModule._free(seedPtr);
172
+ }
173
+ }
174
+ }, 20000);
175
+ });
@@ -2075,6 +2075,31 @@ describe('getTimeInconsistencyScore', () => {
2075
2075
  expect(timeInconsistencyScore).toBe(0);
2076
2076
  });
2077
2077
 
2078
+ it('should return 100 if the signature is invalid or missing when sessionHmacKey is negotiated', () => {
2079
+ const requestTimestamp = Date.now();
2080
+ const clientTimestamp = requestTimestamp - 100;
2081
+ const context = { requestTimestamp };
2082
+ const metrics = { clientTimestamp, signature: 'bad-signature' };
2083
+ const deviceData = { sessionHmacKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' };
2084
+
2085
+ const { timeInconsistencyScore } = __internal.getTimeInconsistencyScore(context, metrics, deviceData);
2086
+ expect(timeInconsistencyScore).toBe(100);
2087
+ });
2088
+
2089
+ it('should return 0 if the signature is valid when sessionHmacKey is negotiated', () => {
2090
+ const requestTimestamp = Date.now();
2091
+ const clientTimestamp = requestTimestamp - 100;
2092
+ const context = { requestTimestamp };
2093
+ const deviceData = { sessionHmacKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' };
2094
+ const metrics = { clientTimestamp };
2095
+ const dataToSign = JSON.stringify(metrics);
2096
+ const signature = createHmac('sha256', Buffer.from(deviceData.sessionHmacKey, 'hex')).update(dataToSign).digest('hex');
2097
+ metrics.signature = signature;
2098
+
2099
+ const { timeInconsistencyScore } = __internal.getTimeInconsistencyScore(context, metrics, deviceData);
2100
+ expect(timeInconsistencyScore).toBe(0);
2101
+ });
2102
+
2078
2103
  it('should return a score > 0 for a replayed request (clientTimestamp significantly before requestTimestamp)', () => {
2079
2104
  const requestTimestamp = Date.now();
2080
2105
  const clientTimestamp = requestTimestamp - (REPLAY_THRESHOLD_MS + 1000); // 1 second beyond threshold
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __internal } from '../fingerprint.js';
3
+
4
+ describe('Système de Polymorphisme et Compilateur d\'Obfuscation', () => {
5
+
6
+ it('should generate a valid session mapping with randomized headers and globals', () => {
7
+ const mapping1 = __internal.generateSessionMapping();
8
+ const mapping2 = __internal.generateSessionMapping();
9
+
10
+ // Vérification de la présence des structures attendues
11
+ expect(mapping1).toBeDefined();
12
+ expect(mapping1.headers).toHaveProperty('x-device-fingerprint');
13
+ expect(mapping1.headers).toHaveProperty('x-behavior-metrics');
14
+ expect(mapping1.globals).toHaveProperty('ClientLibrary');
15
+ expect(mapping1.globals).toHaveProperty('getDeviceFingerprint');
16
+ expect(mapping1.globals).toHaveProperty('getClientBehaviorMetrics');
17
+ expect(mapping1.keys).toHaveProperty('ua');
18
+ expect(mapping1.wasmConstants).toHaveProperty('seed');
19
+
20
+ // Vérification de l'aléa (deux sessions consécutives doivent avoir des mappings uniques)
21
+ expect(mapping1.headers['x-device-fingerprint']).not.toBe(mapping2.headers['x-device-fingerprint']);
22
+ expect(mapping1.globals['ClientLibrary']).not.toBe(mapping2.globals['ClientLibrary']);
23
+ expect(mapping1.keys['ua']).not.toBe(mapping2.keys['ua']);
24
+ expect(mapping1.wasmConstants.seed).not.toBe(mapping2.wasmConstants.seed);
25
+ });
26
+
27
+ it('should compile and obfuscate client script asynchronously using the obfuscation worker', async () => {
28
+ const mapping = __internal.generateSessionMapping();
29
+
30
+ // On lance la compilation lourde qui instancie le Worker d'arrière-plan
31
+ const obfuscatedCode = await __internal.compilePolymorphicJs(mapping);
32
+
33
+ expect(obfuscatedCode).toBeDefined();
34
+ expect(typeof obfuscatedCode).toBe('string');
35
+ expect(obfuscatedCode.length).toBeGreaterThan(0);
36
+
37
+ // Vérification que les placeholders originaux ont bien été purgés du code client final
38
+ expect(obfuscatedCode).not.toContain('X-Device-Fingerprint');
39
+ expect(obfuscatedCode).not.toContain('X-Behavior-Metrics');
40
+ expect(obfuscatedCode).not.toContain('ClientLibrary');
41
+
42
+ // Vérification que le code est minifié/obfusqué (ne contient pas de commentaires de développement par exemple)
43
+ expect(obfuscatedCode).not.toContain('// Nom trompeur aléatoire pour attirer les analyseurs');
44
+
45
+ // La structure globale de javascript-obfuscator génère typiquement un tableau de chaînes au début
46
+ expect(obfuscatedCode).toMatch(/_0x/);
47
+ }, 15000); // Augmentation du timeout à 15s car l'obfuscation via Worker thread consomme du CPU
48
+ });
@@ -30,6 +30,7 @@ class AutoTuner
30
30
  /** @var ?callable */
31
31
  private $onCleanup;
32
32
  private ?string $savePath;
33
+ private float $validationTolerance;
33
34
 
34
35
  /**
35
36
  * @var ?array<string, mixed> La dernière meilleure solution trouvée par l'optimiseur.
@@ -51,6 +52,7 @@ class AutoTuner
51
52
  $this->clearAfterTuning = $options['clearAfterTuning'] ?? false;
52
53
  $this->onCleanup = $options['onCleanup'] ?? null;
53
54
  $this->savePath = $options['savePath'] ?? null;
55
+ $this->validationTolerance = $options['validationTolerance'] ?? 0.15;
54
56
  }
55
57
 
56
58
  /**
@@ -211,6 +213,46 @@ class AutoTuner
211
213
  }
212
214
  };
213
215
 
216
+ // --- VALIDATION POST-CALCUL (PHP Rollback & Seuil de Tolérance) ---
217
+ $tempConfig = [
218
+ 'thresholds' => $this->securityConfig['thresholds'],
219
+ 'weights' => $this->securityConfig['weights'],
220
+ 'patterns' => $this->securityConfig['patterns'],
221
+ ];
222
+
223
+ $applyInertialUpdate($tempConfig['thresholds'], $newConfig['thresholds'], 'thresholds', $trafficConfidence);
224
+ $applyInertialUpdate($tempConfig['weights'], $newConfig['weights'], 'weights', $trafficConfidence);
225
+ $applyInertialUpdate($tempConfig['patterns'], $newConfig['patterns'], 'patterns', $trafficConfidence);
226
+
227
+ $evaluator = OptimizationOperators::createFullSecurityConfigEvaluator(['trafficData' => $sanitizedData]);
228
+ $currentObjectives = $evaluator($this->securityConfig);
229
+ $proposedObjectives = $evaluator($tempConfig);
230
+
231
+ $currentFPR = $currentObjectives[0];
232
+ $currentFNR = $currentObjectives[1];
233
+ $proposedFPR = $proposedObjectives[0];
234
+ $proposedFNR = $proposedObjectives[1];
235
+
236
+ $validationTolerance = $this->securityConfig['autotuning']['validationTolerance'] ?? $this->validationTolerance;
237
+
238
+ if ($proposedFPR > $currentFPR + $validationTolerance || $proposedFNR > $currentFNR + $validationTolerance) {
239
+ error_log(sprintf(
240
+ "[AutoTuning] [SECURITY ALERT] Proposed configuration rejected due to instability/poisoning risk! Proposed FPR: %.4f (Current: %.4f), Proposed FNR: %.4f (Current: %.4f)",
241
+ $proposedFPR, $currentFPR, $proposedFNR, $currentFNR
242
+ ));
243
+ if (isset($this->securityConfig['logger']) && is_callable($this->securityConfig['logger'])) {
244
+ call_user_func($this->securityConfig['logger'], [
245
+ 'type' => 'autotuning_instability_alert',
246
+ 'proposedFPR' => $proposedFPR,
247
+ 'currentFPR' => $currentFPR,
248
+ 'proposedFNR' => $proposedFNR,
249
+ 'currentFNR' => $currentFNR,
250
+ 'timestamp' => (int)(microtime(true) * 1000)
251
+ ]);
252
+ }
253
+ return; // Rollback
254
+ }
255
+
214
256
  $applyInertialUpdate($this->securityConfig['thresholds'], $newConfig['thresholds'], 'thresholds', $trafficConfidence);
215
257
  $applyInertialUpdate($this->securityConfig['weights'], $newConfig['weights'], 'weights', $trafficConfidence);
216
258
  $applyInertialUpdate($this->securityConfig['patterns'], $newConfig['patterns'], 'patterns', $trafficConfidence);
@@ -1153,7 +1153,7 @@
1153
1153
  }
1154
1154
 
1155
1155
  $highThreshold = $thresholds['high'] ?? 75;
1156
- $mustReChallenge = $finalScore >= $highThreshold && $hasValidTicket;
1156
+ $mustReChallenge = $finalScore >= $highThreshold && $hasValidTicket && $finalScore > 0;
1157
1157
 
1158
1158
  $lowThreshold = $thresholds['low'] ?? 20;
1159
1159
  if (($finalScore >= $lowThreshold && !$hasValidTicket) || $mustReChallenge) {