@anonympins/fingerprint 0.6.5 → 0.6.7
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 +34 -0
- package/README.md +14 -1
- package/package.json +4 -1
- package/public/fp.js +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +266 -57
- package/src/js/gpu_pow.solver.js +3 -2
- package/src/js/pow.solver.inline.js +128 -2
- package/src/js/pow.solver.js +85 -0
- package/src/js/problem-manager.js +26 -4
- package/src/js/tests/cross_parity.test.js +2 -2
- package/src/js/tests/fingerprint.engine.test.js +25 -0
- package/src/js/tests/fingerprint.test.js +13 -4
- package/src/js/tests/ip-reputation.test.js +1 -1
- package/src/js/tests/quicFingerprint.test.js +21 -10
- package/src/js/upow-model-task.js +114 -0
- package/src/php/Challenge/ChallengeUtils.php +2 -2
- package/src/php/Config/SecurityProfiles.php +6 -6
- package/src/php/FingerprintEngine.php +1880 -1879
- package/src/php/Optimization/FunctionRegistry.php +1 -1
- package/src/php/Optimization/OptimizationOperators.php +13 -0
- package/src/php/ProblemManager.php +105 -0
- package/src/php/Tests/ChallengeUtilsTest.php +3 -2
- package/src/php/Tests/IpReputationTest.php +1 -1
- package/src/php/Tests/ProblemManagerTest.php +1 -1
- package/src/php/Tests/QuicFingerprintTest.php +9 -9
- package/src/php/Utils/RequestUtils.php +111 -46
|
@@ -53,6 +53,44 @@ function generateBlock(seed, blockIndex, blockSize = 1024) {
|
|
|
53
53
|
return block;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
async function loadTfjs() {
|
|
57
|
+
if (typeof tf !== 'undefined') return tf;
|
|
58
|
+
if (typeof window !== 'undefined') {
|
|
59
|
+
if (window.tf) return window.tf;
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const script = document.createElement('script');
|
|
62
|
+
script.src = 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs/dist/tf.min.js';
|
|
63
|
+
script.onload = () => resolve(window.tf);
|
|
64
|
+
script.onerror = () => reject(new Error('Failed to load TensorFlow.js'));
|
|
65
|
+
document.head.appendChild(script);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return await import('@tensorflow/tfjs');
|
|
70
|
+
} catch (e) {
|
|
71
|
+
throw new Error("TensorFlow.js is not available.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function loadOnnxRuntime() {
|
|
76
|
+
if (typeof ort !== 'undefined') return ort;
|
|
77
|
+
if (typeof window !== 'undefined') {
|
|
78
|
+
if (window.ort) return window.ort;
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const script = document.createElement('script');
|
|
81
|
+
script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
|
|
82
|
+
script.onload = () => resolve(window.ort);
|
|
83
|
+
script.onerror = () => reject(new Error('Failed to load ONNX Runtime Web'));
|
|
84
|
+
document.head.appendChild(script);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return await import('onnxruntime-node');
|
|
89
|
+
} catch (e) {
|
|
90
|
+
throw new Error("ONNX Runtime is not available.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
56
94
|
async function initializeSpace(seed, sizeMb) {
|
|
57
95
|
const db = await openDb();
|
|
58
96
|
const transaction = db.transaction("blocks", "readwrite");
|
|
@@ -491,8 +529,8 @@ async function solveUsefulWorkTask(task) {
|
|
|
491
529
|
const i = Math.floor(secureRandom() * numFacilities);
|
|
492
530
|
const moveX = (secureRandom() - 0.5) * (bounds.maxX - bounds.minX) * 0.1;
|
|
493
531
|
const moveY = (secureRandom() - 0.5) * (bounds.maxY - bounds.minY) * 0.1;
|
|
494
|
-
|
|
495
|
-
|
|
532
|
+
newFacilities[i].x = Math.max(bounds.minX, Math.min(bounds.maxX, newFacilities[i].x + moveX));
|
|
533
|
+
newFacilities[i].y = Math.max(bounds.minY, Math.min(bounds.maxY, newFacilities[i].y + moveY));
|
|
496
534
|
return newFacilities;
|
|
497
535
|
};
|
|
498
536
|
const initialSolution = task.initialSolution || Array.from({ length: numFacilities }, () => ({
|
|
@@ -557,6 +595,94 @@ async function solveUsefulWorkTask(task) {
|
|
|
557
595
|
return solverFunction(task.payload, { generations: task.generations, initialFront: task.initialFront });
|
|
558
596
|
}
|
|
559
597
|
|
|
598
|
+
case 'pytorch_onnx_learning': {
|
|
599
|
+
const { weights, payload, modelPath } = task;
|
|
600
|
+
const inputs = payload.inputs || [];
|
|
601
|
+
const labels = payload.labels || [];
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const ort = await loadOnnxRuntime();
|
|
605
|
+
const session = await ort.InferenceSession.create(modelPath);
|
|
606
|
+
const inputName = session.inputNames[0];
|
|
607
|
+
const outputName = session.outputNames[0];
|
|
608
|
+
|
|
609
|
+
const dims = [inputs.length, inputs[0].length];
|
|
610
|
+
const inputTensor = new ort.Tensor('float32', Float32Array.from(inputs.flat()), dims);
|
|
611
|
+
const results = await session.run({ [inputName]: inputTensor });
|
|
612
|
+
const outputTensor = results[outputName];
|
|
613
|
+
const predictions = Array.from(outputTensor.data);
|
|
614
|
+
|
|
615
|
+
const gradients = new Array(weights.length).fill(0);
|
|
616
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
617
|
+
const x = inputs[i];
|
|
618
|
+
const y = labels[i];
|
|
619
|
+
const pred = predictions[i];
|
|
620
|
+
const error = pred - y;
|
|
621
|
+
for (let j = 0; j < weights.length; j++) {
|
|
622
|
+
gradients[j] += error * x[j] / inputs.length;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return { gradients, predictions };
|
|
626
|
+
} catch (err) {
|
|
627
|
+
console.warn('[Useful Work Solver] Real ONNX loading failed, using fallback simulation:', err);
|
|
628
|
+
const gradients = new Array(weights.length).fill(0);
|
|
629
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
630
|
+
const x = inputs[i];
|
|
631
|
+
const y = labels[i];
|
|
632
|
+
let pred = 0;
|
|
633
|
+
for (let j = 0; j < weights.length; j++) {
|
|
634
|
+
pred += x[j] * weights[j];
|
|
635
|
+
}
|
|
636
|
+
const error = pred - y;
|
|
637
|
+
for (let j = 0; j < weights.length; j++) {
|
|
638
|
+
gradients[j] += error * x[j] / inputs.length;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return { gradients };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
case 'tfjs_learning': {
|
|
646
|
+
const { weights, payload, modelPath } = task;
|
|
647
|
+
const inputs = payload.inputs || [];
|
|
648
|
+
const labels = payload.labels || [];
|
|
649
|
+
try {
|
|
650
|
+
const tf = await loadTfjs();
|
|
651
|
+
const model = await tf.loadLayersModel(modelPath);
|
|
652
|
+
|
|
653
|
+
const xs = tf.tensor2d(inputs);
|
|
654
|
+
const ys = tf.tensor2d(labels);
|
|
655
|
+
|
|
656
|
+
const trainableVars = model.trainableWeights.map(w => w.read());
|
|
657
|
+
const lossFn = () => {
|
|
658
|
+
const preds = model.predict(xs);
|
|
659
|
+
return tf.losses.meanSquaredError(ys, preds);
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const gFn = tf.grad(lossFn);
|
|
663
|
+
const grads = gFn(trainableVars);
|
|
664
|
+
|
|
665
|
+
const flatGradients = [];
|
|
666
|
+
const arr = await grads.array();
|
|
667
|
+
flatGradients.push(...arr.flat());
|
|
668
|
+
|
|
669
|
+
tf.dispose([xs, ys, ...grads]);
|
|
670
|
+
return { gradients: flatGradients };
|
|
671
|
+
} catch (err) {
|
|
672
|
+
console.warn('[Useful Work Solver] Real TFJS loading failed, using fallback simulation:', err);
|
|
673
|
+
const gradients = new Array(weights.length).fill(0);
|
|
674
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
675
|
+
const x = inputs[i];
|
|
676
|
+
const y = labels[i][0] !== undefined ? labels[i][0] : labels[i];
|
|
677
|
+
let pred = 0;
|
|
678
|
+
for (let j = 0; j < weights.length; j++) pred += x[j] * weights[j];
|
|
679
|
+
const error = pred - y;
|
|
680
|
+
for (let j = 0; j < weights.length; j++) gradients[j] += error * x[j] / inputs.length;
|
|
681
|
+
}
|
|
682
|
+
return { gradients };
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
560
686
|
default:
|
|
561
687
|
throw new Error(`Unknown useful work type: ${task.type}`);
|
|
562
688
|
}
|
package/src/js/pow.solver.js
CHANGED
|
@@ -32,6 +32,25 @@ function cyrb53(str, seed = 0) {
|
|
|
32
32
|
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
async function loadTfjs() {
|
|
36
|
+
if (typeof tf !== 'undefined') return tf;
|
|
37
|
+
if (typeof window !== 'undefined') {
|
|
38
|
+
if (window.tf) return window.tf;
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const script = document.createElement('script');
|
|
41
|
+
script.src = 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs/dist/tf.min.js';
|
|
42
|
+
script.onload = () => resolve(window.tf);
|
|
43
|
+
script.onerror = () => reject(new Error('Failed to load TensorFlow.js'));
|
|
44
|
+
document.head.appendChild(script);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return await import('@tensorflow/tfjs');
|
|
49
|
+
} catch (e) {
|
|
50
|
+
throw new Error("TensorFlow.js is not available.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
function openDb() {
|
|
36
55
|
return new Promise((resolve, reject) => {
|
|
37
56
|
const request = indexedDB.open("pospace-db", 1);
|
|
@@ -898,6 +917,72 @@ async function solveUsefulWorkTask(task) {
|
|
|
898
917
|
return solverFunction(task.payload, { generations: task.generations, initialFront: task.initialFront });
|
|
899
918
|
}
|
|
900
919
|
|
|
920
|
+
case 'pytorch_onnx_learning': {
|
|
921
|
+
// Dans un environnement de production réel avec ONNX Runtime Web, on chargerait le modèle .onnx
|
|
922
|
+
// et on exécuterait un calcul de gradient (backward pass).
|
|
923
|
+
// Ici, nous simulons le calcul de gradient mathématique équivalent pour la régression/classification.
|
|
924
|
+
const { weights, payload } = task;
|
|
925
|
+
const inputs = payload.inputs || [];
|
|
926
|
+
const labels = payload.labels || [];
|
|
927
|
+
const gradients = new Array(weights.length).fill(0);
|
|
928
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
929
|
+
const x = inputs[i];
|
|
930
|
+
const y = labels[i];
|
|
931
|
+
let pred = 0;
|
|
932
|
+
for (let j = 0; j < weights.length; j++) {
|
|
933
|
+
pred += x[j] * weights[j];
|
|
934
|
+
}
|
|
935
|
+
const error = pred - y;
|
|
936
|
+
for (let j = 0; j < weights.length; j++) {
|
|
937
|
+
gradients[j] += error * x[j] / inputs.length;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return { gradients };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
case 'tfjs_learning': {
|
|
944
|
+
const { weights, payload, modelPath } = task;
|
|
945
|
+
const inputs = payload.inputs || [];
|
|
946
|
+
const labels = payload.labels || [];
|
|
947
|
+
try {
|
|
948
|
+
const tf = await loadTfjs();
|
|
949
|
+
// Chargement non-bloquant du modèle au format JSON
|
|
950
|
+
const model = await tf.loadLayersModel(modelPath);
|
|
951
|
+
|
|
952
|
+
const xs = tf.tensor2d(inputs);
|
|
953
|
+
const ys = tf.tensor2d(labels);
|
|
954
|
+
|
|
955
|
+
const trainableVars = model.trainableWeights.map(w => w.read());
|
|
956
|
+
const lossFn = () => {
|
|
957
|
+
const preds = model.predict(xs);
|
|
958
|
+
return tf.losses.meanSquaredError(ys, preds);
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
// Dérivation automatique pour le calcul exact des gradients
|
|
962
|
+
const gFn = tf.grad(lossFn);
|
|
963
|
+
const grads = gFn(trainableVars);
|
|
964
|
+
|
|
965
|
+
const flatGradients = [];
|
|
966
|
+
const arr = await grads.array();
|
|
967
|
+
flatGradients.push(...arr.flat());
|
|
968
|
+
|
|
969
|
+
tf.dispose([xs, ys, grads]);
|
|
970
|
+
return { gradients: flatGradients };
|
|
971
|
+
} catch (err) {
|
|
972
|
+
console.warn('[Useful Work Solver] Real TFJS loading failed, using fallback simulation:', err);
|
|
973
|
+
const gradients = new Array(weights.length).fill(0);
|
|
974
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
975
|
+
const x = inputs[i];
|
|
976
|
+
const y = labels[i][0] !== undefined ? labels[i][0] : labels[i];
|
|
977
|
+
let pred = 0;
|
|
978
|
+
for (let j = 0; j < weights.length; j++) pred += x[j] * weights[j];
|
|
979
|
+
const error = pred - y;
|
|
980
|
+
for (let j = 0; j < weights.length; j++) gradients[j] += error * x[j] / inputs.length;
|
|
981
|
+
}
|
|
982
|
+
return { gradients };
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
901
986
|
default:
|
|
902
987
|
throw new Error(`Unknown useful work type: ${task.type}`);
|
|
903
988
|
}
|
|
@@ -318,6 +318,12 @@ class ProblemManager {
|
|
|
318
318
|
task.initialFront = problem.state.paretoFront;
|
|
319
319
|
task.solverName = problem.workUnit.solverName; // Le nom du solveur à utiliser (ex: 'cpc.solve')
|
|
320
320
|
break;
|
|
321
|
+
|
|
322
|
+
case 'tfjs_learning':
|
|
323
|
+
task.modelPath = problem.workUnit.modelPath;
|
|
324
|
+
task.payload = problem.payload;
|
|
325
|
+
task.weights = problem.state.weights || [];
|
|
326
|
+
break;
|
|
321
327
|
}
|
|
322
328
|
|
|
323
329
|
return { problemId: problem.id, task };
|
|
@@ -379,9 +385,16 @@ class ProblemManager {
|
|
|
379
385
|
case 'genetic_algorithm_generations':
|
|
380
386
|
// VÉRIFICATION PAR ÉCHANTILLONNAGE pour équilibrer sécurité et performance.
|
|
381
387
|
const fitnessFunction = FunctionRegistry['portfolio.calculateMetrics']; // Ou une fonction plus générique
|
|
382
|
-
if (!
|
|
383
|
-
console.error(`[ProblemManager]
|
|
384
|
-
return;
|
|
388
|
+
if (!solutionData || !Array.isArray(solutionData.population)) {
|
|
389
|
+
console.error(`[ProblemManager] Propriété 'population' manquante ou invalide dans solutionData pour ${problemId}.`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (solutionData.population.length === 0) {
|
|
393
|
+
return; // Retour gracieux si la population est vide (ex: fallback de redirection)
|
|
394
|
+
}
|
|
395
|
+
if (!fitnessFunction) {
|
|
396
|
+
console.error(`[ProblemManager] Impossible de vérifier la population pour ${problemId} (fonction de fitness manquante).`);
|
|
397
|
+
return;
|
|
385
398
|
}
|
|
386
399
|
|
|
387
400
|
if (solutionData.population.length > 150) {
|
|
@@ -408,7 +421,16 @@ class ProblemManager {
|
|
|
408
421
|
if (!individual || !individual.chromosome) {
|
|
409
422
|
throw new Error("Structure d'individu ou chromosome invalide dans la population.");
|
|
410
423
|
}
|
|
411
|
-
|
|
424
|
+
const recalculated = fitnessFunction(individual.chromosome, problem.payload);
|
|
425
|
+
|
|
426
|
+
if (individual.fitness !== undefined && individual.fitness !== -1) {
|
|
427
|
+
if (Math.abs(individual.fitness - recalculated) > 1e-4) {
|
|
428
|
+
console.error(`[ProblemManager] Triche détectée pour ${problemId}! Fitness déclaré: ${individual.fitness}, recalculé: ${recalculated}`);
|
|
429
|
+
return; // Rejeter immédiatement toute la population
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
individual.fitness = recalculated; // Forcer la valeur exacte recalculée
|
|
433
|
+
totalRecalculatedFitness += recalculated;
|
|
412
434
|
}
|
|
413
435
|
|
|
414
436
|
problem.state.population = solutionData.population; // On accepte la population
|
|
@@ -58,7 +58,7 @@ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', ()
|
|
|
58
58
|
const rFloat = Math.fround(r);
|
|
59
59
|
const output = new Float32Array(64);
|
|
60
60
|
for (let idx = 0; idx < 64; idx++) {
|
|
61
|
-
let x = Math.fround(numericSeed + idx * 0.015);
|
|
61
|
+
let x = Math.fround((numericSeed + idx * 0.015) % 1);
|
|
62
62
|
for (let i = 0; i < iterations; i++) {
|
|
63
63
|
x = Math.fround(rFloat * x * Math.fround(1.0 - x));
|
|
64
64
|
}
|
|
@@ -96,7 +96,7 @@ describe('Cross-Parity Chaos PoW Floating Point Parity (JS vs WASM vs PHP)', ()
|
|
|
96
96
|
\$r = 3.9999;
|
|
97
97
|
\$solutions = [];
|
|
98
98
|
for (\$idx = 0; \$idx < 64; \$idx++) {
|
|
99
|
-
\$x = fround(\$numericSeed + \$idx * 0.015);
|
|
99
|
+
\$x = fround(fmod(\$numericSeed + \$idx * 0.015, 1.0));
|
|
100
100
|
\$rFloat = fround(\$r);
|
|
101
101
|
for (\$i = 0; \$i < \$iterations; \$i++) {
|
|
102
102
|
\$x = fround(\$rFloat * \$x * fround(1.0 - \$x));
|
|
@@ -130,6 +130,31 @@ describe('FingerprintEngine Challenge Validation', () => {
|
|
|
130
130
|
expect(await inMemoryStore.has(`secret:${nonce}`)).toBe(false);
|
|
131
131
|
}, 20000);
|
|
132
132
|
|
|
133
|
+
it('should generate a combined challenge page containing the correct prefix for the memory seed', async () => {
|
|
134
|
+
const originalRequestContext = {
|
|
135
|
+
clientIp: '127.0.0.1',
|
|
136
|
+
path: '/sensitive-data',
|
|
137
|
+
cookies: {},
|
|
138
|
+
query: {},
|
|
139
|
+
headers: {
|
|
140
|
+
'user-agent': 'A-Legit-Browser/1.0',
|
|
141
|
+
'x-device-fingerprint': 'fingerprint-A'
|
|
142
|
+
},
|
|
143
|
+
isStatic: false,
|
|
144
|
+
rawReq: { headers: { accept: 'text/html' } }
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Force a high score to trigger a challenge
|
|
148
|
+
vi.spyOn(engine, 'calculateFinalScore').mockReturnValueOnce(20);
|
|
149
|
+
|
|
150
|
+
const challengeDecision = await engine.processRequest(originalRequestContext);
|
|
151
|
+
|
|
152
|
+
expect(challengeDecision.action).toBe('challenge');
|
|
153
|
+
const body = challengeDecision.body;
|
|
154
|
+
// Verify that the memory seed is constructed with the correct leading colon
|
|
155
|
+
expect(body).toContain('const memSeed = ":" + nonce + ":" + clientSecret;');
|
|
156
|
+
});
|
|
157
|
+
|
|
133
158
|
it('should reject a challenge solution with a mismatched fingerprint', async () => {
|
|
134
159
|
// --- 1. First request: Challenge is issued to "Machine A" ---
|
|
135
160
|
const originalRequestContext = {
|
|
@@ -2414,7 +2414,7 @@ describe('Subnet Scoring (Node.js)', () => {
|
|
|
2414
2414
|
deviceIds: deviceIds, // count = 20. score += (20-10)*5 = 50
|
|
2415
2415
|
});
|
|
2416
2416
|
({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
|
|
2417
|
-
|
|
2417
|
+
expect(subnetScore).toBe(94.4);
|
|
2418
2418
|
});
|
|
2419
2419
|
});
|
|
2420
2420
|
|
|
@@ -2487,7 +2487,7 @@ describe('FingerprintEngine.processRequest - updateSubnetMetrics call logic', ()
|
|
|
2487
2487
|
beforeEach(() => {
|
|
2488
2488
|
inMemoryStore._map.clear();
|
|
2489
2489
|
configureStore(inMemoryStore);
|
|
2490
|
-
|
|
2490
|
+
// Add a default mock for getTlsFingerprint to stabilize these tests.
|
|
2491
2491
|
securityConfig = {
|
|
2492
2492
|
weights: { historyScore: 1.0 },
|
|
2493
2493
|
thresholds: { low: 20, medium: 45, high: 75, block: 95 }, // Default blockThreshold
|
|
@@ -2508,8 +2508,8 @@ describe('FingerprintEngine.processRequest - updateSubnetMetrics call logic', ()
|
|
|
2508
2508
|
calculateFinalScoreSpy.mockRestore();
|
|
2509
2509
|
});
|
|
2510
2510
|
|
|
2511
|
-
it('should call updateSubnetMetrics if finalScore is between
|
|
2512
|
-
calculateFinalScoreSpy.mockReturnValue(50); // Score between
|
|
2511
|
+
it('should call updateSubnetMetrics if finalScore is between mediumThreshold and blockThreshold', async () => {
|
|
2512
|
+
calculateFinalScoreSpy.mockReturnValue(50); // Score between 45 and 95
|
|
2513
2513
|
const requestContext = { clientIp: '192.168.1.1', cookies: {}, query: {}, headers: {} };
|
|
2514
2514
|
|
|
2515
2515
|
await engine.processRequest(requestContext);
|
|
@@ -2518,6 +2518,15 @@ describe('FingerprintEngine.processRequest - updateSubnetMetrics call logic', ()
|
|
|
2518
2518
|
expect(updateSubnetMetricsSpy).toHaveBeenCalledWith(requestContext, expect.any(String), 50);
|
|
2519
2519
|
});
|
|
2520
2520
|
|
|
2521
|
+
it('should NOT call updateSubnetMetrics if finalScore is below mediumThreshold', async () => {
|
|
2522
|
+
calculateFinalScoreSpy.mockReturnValue(30); // Score below 45
|
|
2523
|
+
const requestContext = { clientIp: '192.168.1.1', cookies: {}, query: {}, headers: {} };
|
|
2524
|
+
|
|
2525
|
+
await engine.processRequest(requestContext);
|
|
2526
|
+
|
|
2527
|
+
expect(updateSubnetMetricsSpy).not.toHaveBeenCalled();
|
|
2528
|
+
});
|
|
2529
|
+
|
|
2521
2530
|
it('should NOT call updateSubnetMetrics if finalScore is equal to or above blockThreshold', async () => {
|
|
2522
2531
|
calculateFinalScoreSpy.mockReturnValue(95); // Score equals blockThreshold
|
|
2523
2532
|
const requestContext = { clientIp: '192.168.1.1', cookies: {}, query: {}, headers: {} };
|
|
@@ -134,7 +134,7 @@ describe('IP Reputation Local System (Node.js)', () => {
|
|
|
134
134
|
let score = await __internal.getSubnetScore(mockContext);
|
|
135
135
|
expect(score.subnetScore).toBe(0);
|
|
136
136
|
for (let i = 1; i <= 12; i++) {
|
|
137
|
-
await __internal.updateSubnetMetrics(mockContext, `dev-${i}`,
|
|
137
|
+
await __internal.updateSubnetMetrics(mockContext, `dev-${i}`, 50);
|
|
138
138
|
}
|
|
139
139
|
score = await __internal.getSubnetScore(mockContext);
|
|
140
140
|
expect(score.subnetScore).toBeGreaterThan(0);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { __internal } from '../fingerprint.js';
|
|
3
3
|
|
|
4
|
-
const {
|
|
4
|
+
const { getProtocolAnomalyScore } = __internal;
|
|
5
5
|
|
|
6
|
-
describe('
|
|
7
|
-
it('should return 0.0 if no QUIC fingerprint is present', () => {
|
|
6
|
+
describe('Protocol Anomaly Flow Profiling', () => {
|
|
7
|
+
it('should return 0.0 if no QUIC or HTTP/2 fingerprint is present', () => {
|
|
8
8
|
const context = { headers: {} };
|
|
9
|
-
const score =
|
|
10
|
-
expect(score.
|
|
9
|
+
const score = getProtocolAnomalyScore(context);
|
|
10
|
+
expect(score.protocolAnomalyScore).toBe(0.0);
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
it('should detect Chrome spoofed with non-default stream/priority values', () => {
|
|
13
|
+
it('should detect Chrome spoofed with non-default stream/priority values (QUIC)', () => {
|
|
14
14
|
// Chrome attend un initial_max_data (1) >= 1MB, initial_max_streams_bidi (4) == 100, et extensible Priority (contient u=)
|
|
15
15
|
const context = {
|
|
16
16
|
headers: {
|
|
@@ -18,8 +18,8 @@ describe('QUIC / HTTP/3 Flow Profiling', () => {
|
|
|
18
18
|
'x-quic-fp': '1;1=65536,4=50;i=0' // max_data trop bas, max_streams != 100, pas de priorité u=
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
|
-
const score =
|
|
22
|
-
expect(score.
|
|
21
|
+
const score = getProtocolAnomalyScore(context);
|
|
22
|
+
expect(score.protocolAnomalyScore).toBe(100.0);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
it('should return 0.0 for a legitimate Chrome QUIC flow profile', () => {
|
|
@@ -29,7 +29,18 @@ describe('QUIC / HTTP/3 Flow Profiling', () => {
|
|
|
29
29
|
'x-quic-fp': '1;1=1572864,4=100;u=2,i'
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
|
-
const score =
|
|
33
|
-
expect(score.
|
|
32
|
+
const score = getProtocolAnomalyScore(context);
|
|
33
|
+
expect(score.protocolAnomalyScore).toBe(0.0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should detect Chromium HTTP/2 header order anomalies', () => {
|
|
37
|
+
const context = {
|
|
38
|
+
headers: {
|
|
39
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
40
|
+
'x-http2-fingerprint': '1|65535|0|invalid_order'
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const score = getProtocolAnomalyScore(context);
|
|
44
|
+
expect(score.protocolAnomalyScore).toBe(100.0);
|
|
34
45
|
});
|
|
35
46
|
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Classe de base abstraite pour définir des tâches uPoW basées sur l'entraînement
|
|
5
|
+
* ou l'évaluation de modèles IA (PyTorch exportés au format ONNX).
|
|
6
|
+
*/
|
|
7
|
+
export class UpowModelTask {
|
|
8
|
+
constructor(problemId, modelPath, config = {}) {
|
|
9
|
+
if (new.target === UpowModelTask) {
|
|
10
|
+
throw new TypeError("Cannot construct UpowModelTask instances directly");
|
|
11
|
+
}
|
|
12
|
+
this.problemId = problemId;
|
|
13
|
+
this.modelPath = modelPath;
|
|
14
|
+
this.config = {
|
|
15
|
+
batchSize: 32,
|
|
16
|
+
learningRate: 0.01,
|
|
17
|
+
validationTolerance: 0.15,
|
|
18
|
+
...config
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Récupère l'identifiant du problème.
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
getProblemId() {
|
|
27
|
+
return this.problemId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Récupère le chemin réseau du modèle ONNX.
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
getModelAssetPath() {
|
|
35
|
+
return this.modelPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Génère la charge utile de travail pour le client.
|
|
40
|
+
* Envoie l'état actuel des poids du modèle et un lot de données (mini-batch) à traiter.
|
|
41
|
+
* @param {number} suspicionFactor
|
|
42
|
+
* @returns {object} Données à envoyer au client
|
|
43
|
+
*/
|
|
44
|
+
dispatchTask(suspicionFactor) {
|
|
45
|
+
throw new Error("Method 'dispatchTask(suspicionFactor)' must be implemented.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Vérifie la validité de la solution retournée par le client.
|
|
50
|
+
* Implémente des gardes-fous contre l'empoisonnement (ex: vérification des gradients).
|
|
51
|
+
* @param {object} taskContext
|
|
52
|
+
* @param {object} solution
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
verifySolution(taskContext, solution) {
|
|
56
|
+
throw new Error("Method 'verifySolution(taskContext, solution)' must be implemented.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Intègre les gradients ou résultats validés dans le modèle global.
|
|
61
|
+
* Par exemple : algorithme FedAvg (Federated Averaging).
|
|
62
|
+
* @param {object} solution
|
|
63
|
+
*/
|
|
64
|
+
integrateSolution(solution) {
|
|
65
|
+
throw new Error("Method 'integrateSolution(solution)' must be implemented.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Exemple d'implémentation d'une tâche d'entraînement (Classifieur de requêtes suspectes)
|
|
71
|
+
*/
|
|
72
|
+
export class RequestClassifierTask extends UpowModelTask {
|
|
73
|
+
constructor(modelPath, config) {
|
|
74
|
+
super("request_classifier_nn", modelPath, config);
|
|
75
|
+
// Poids initiaux du modèle (Réseau de neurones linéaire simple)
|
|
76
|
+
this.weights = [0.1, -0.2, 0.8, 0.5];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
dispatchTask(suspicionFactor) {
|
|
80
|
+
// Génération de données d'entraînement anonymisées pour le client
|
|
81
|
+
const inputs = [
|
|
82
|
+
[1.0, 0.5, 0.0, 1.2],
|
|
83
|
+
[0.0, 1.0, -0.5, 0.8]
|
|
84
|
+
];
|
|
85
|
+
return {
|
|
86
|
+
weights: [...this.weights],
|
|
87
|
+
inputs: inputs,
|
|
88
|
+
learningRate: this.config.learningRate,
|
|
89
|
+
batchSize: this.config.batchSize
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
verifySolution(taskContext, solution) {
|
|
94
|
+
const gradients = solution?.gradients;
|
|
95
|
+
if (!Array.isArray(gradients) || gradients.length !== this.weights.length) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
// Anti-poisoning : s'assurer que les gradients ne contiennent pas de valeurs aberrantes (clipping)
|
|
99
|
+
const maxGradientNorm = 10.0;
|
|
100
|
+
for (const grad of gradients) {
|
|
101
|
+
if (typeof grad !== 'number' || isNaN(grad) || Math.abs(grad) > maxGradientNorm) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
integrateSolution(solution) {
|
|
109
|
+
const gradients = solution.gradients;
|
|
110
|
+
for (let i = 0; i < this.weights.length; i++) {
|
|
111
|
+
this.weights[i] -= this.config.learningRate * gradients[i];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -65,7 +65,7 @@ class ChallengeUtils
|
|
|
65
65
|
if ($idx < 0 || $idx >= 64) {
|
|
66
66
|
return false;
|
|
67
67
|
}
|
|
68
|
-
$x = self::fround($numericSeed + $idx * 0.015);
|
|
68
|
+
$x = self::fround(fmod($numericSeed + $idx * 0.015, 1.0));
|
|
69
69
|
$rFloat = self::fround($r);
|
|
70
70
|
for ($i = 0; $i < $iterations; $i++) {
|
|
71
71
|
$x = self::fround($rFloat * $x * self::fround(1.0 - $x));
|
|
@@ -1134,7 +1134,7 @@ JS;
|
|
|
1134
1134
|
}
|
|
1135
1135
|
let memSolution = 0;
|
|
1136
1136
|
try {
|
|
1137
|
-
const memSeed = nonce + ":" + clientSecret;
|
|
1137
|
+
const memSeed = ":" + nonce + ":" + clientSecret;
|
|
1138
1138
|
memSolution = await window.solveMemoryChallenge(memSeed, memDifficulty);
|
|
1139
1139
|
} catch(e) {
|
|
1140
1140
|
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
@@ -40,8 +40,8 @@ class SecurityProfiles
|
|
|
40
40
|
'clickVarianceScore' => 0.6, // Poids pour la variance des clics
|
|
41
41
|
'subnetScore' => 0.5, // Pénalise les sous-réseaux IP avec une activité suspecte agrégée
|
|
42
42
|
'botnetClusterScore' => 0.6, // NOUVEAU: Poids pour le clustering botnet
|
|
43
|
-
'tcpAnomalyScore' => 0.8,
|
|
44
|
-
'
|
|
43
|
+
'tcpAnomalyScore' => 0.8,
|
|
44
|
+
'protocolAnomalyScore' => 0.8, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
45
45
|
'renderingAnomalyScore' => 0.8, // NEW: Anomalie de rendu
|
|
46
46
|
'ipReputationScore' => 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
47
47
|
],
|
|
@@ -90,7 +90,7 @@ class SecurityProfiles
|
|
|
90
90
|
'subnetScore' => 0.7, // Poids plus élevé en mode strict
|
|
91
91
|
'botnetClusterScore' => 0.8, // NOUVEAU: Poids pour le clustering botnet
|
|
92
92
|
'tcpAnomalyScore' => 1.0, // NEW: Anomalie de pile TCP/IP
|
|
93
|
-
'
|
|
93
|
+
'protocolAnomalyScore' => 1.0, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
94
94
|
'renderingAnomalyScore' => 1.0, // NEW: Anomalie de rendu
|
|
95
95
|
|
|
96
96
|
],
|
|
@@ -139,7 +139,7 @@ class SecurityProfiles
|
|
|
139
139
|
'subnetScore' => 0.8, // Très important pour les API pour détecter les botnets
|
|
140
140
|
'botnetClusterScore' => 0.7, // NOUVEAU: Poids pour le clustering botnet
|
|
141
141
|
'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
|
|
142
|
-
'
|
|
142
|
+
'protocolAnomalyScore' => 0.8, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
143
143
|
|
|
144
144
|
],
|
|
145
145
|
'thresholds' => ['low' => 25, 'medium' => 50, 'high' => 80, 'block' => 95],
|
|
@@ -190,7 +190,7 @@ class SecurityProfiles
|
|
|
190
190
|
'ipReputationScore' => 0.3, // NOUVEAU: Poids pour la réputation IP
|
|
191
191
|
'botnetClusterScore' => 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
192
192
|
'tcpAnomalyScore' => 0.5, // NEW: Anomalie de pile TCP/IP
|
|
193
|
-
'
|
|
193
|
+
'protocolAnomalyScore' => 0.5, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
194
194
|
'renderingAnomalyScore' => 0.5, // NEW: Anomalie de rendu
|
|
195
195
|
|
|
196
196
|
],
|
|
@@ -240,7 +240,7 @@ class SecurityProfiles
|
|
|
240
240
|
'subnetScore' => 0.9, // Crucial contre les attaques de scalping distribuées
|
|
241
241
|
'botnetClusterScore' => 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
242
242
|
'tcpAnomalyScore' => 0.9, // NEW: Anomalie de pile TCP/IP
|
|
243
|
-
'
|
|
243
|
+
'protocolAnomalyScore' => 0.9, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
244
244
|
'renderingAnomalyScore' => 0.9, // NEW: Anomalie de rendu
|
|
245
245
|
|
|
246
246
|
],
|