From bacc322eb09291f6f2b895bf4f180e55321da1e0 Mon Sep 17 00:00:00 2001 From: IntegerAlex Date: Thu, 25 Jun 2026 09:22:40 +0530 Subject: [PATCH] feat(bot): add pointer mismatch and resolution checks --- src/systemInfo.ts | 25 ++++++++++ test/e2e/cypress/e2e/unit/core.cy.js | 46 +++++++++++++++++++ .../cypress/e2e/unit/hash-serialization.cy.js | 15 ++++-- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/systemInfo.ts b/src/systemInfo.ts index 565d24d..a5e0e42 100644 --- a/src/systemInfo.ts +++ b/src/systemInfo.ts @@ -8,6 +8,7 @@ * For a full copy of the LGPL and ethical contribution guidelines, please refer to the `COPYRIGHT.md` and `NOTICE.md` files. */ import { SystemInfo} from './types.js'; +import Bowser from './bowser/bowser.js'; import { detectIncognito } from './incognito.js'; import { getMockSystemInfo } from './mock.js'; import { detectAdBlockers } from './adblocker.js'; @@ -99,6 +100,30 @@ export function detectBot(): { isBot: boolean; signals: string[]; confidence: nu signals.push('weak:unusual-concurrency'); } + // CSS pointer/hover and screen resolution checks for desktop OS + let isDesktop = false; + try { + const parsed = Bowser.parse(navigator.userAgent); + isDesktop = parsed.platform?.type === 'desktop'; + } catch { + isDesktop = /Linux|Windows|Macintosh|X11/.test(navigator.userAgent) && !/Android|iPhone|iPad|iPod/.test(navigator.userAgent); + } + + if (isDesktop) { + if (window.matchMedia) { + const hoverNone = window.matchMedia('(hover: none)').matches; + const pointerNone = !window.matchMedia('(pointer: fine)').matches && !window.matchMedia('(pointer: coarse)').matches; + if (hoverNone || pointerNone) { + signals.push('strong:desktop-no-pointer-hover-mismatch'); + } + } + if (window.screen) { + if (window.screen.width === 800 && window.screen.height === 600) { + signals.push('medium:desktop-default-headless-resolution'); + } + } + } + // Calculate confidence score let confidence = 0.5; confidence += signals.filter(s => s.startsWith('strong:')).length * confidenceWeights.strong; diff --git a/test/e2e/cypress/e2e/unit/core.cy.js b/test/e2e/cypress/e2e/unit/core.cy.js index 3dffb94..0794a65 100644 --- a/test/e2e/cypress/e2e/unit/core.cy.js +++ b/test/e2e/cypress/e2e/unit/core.cy.js @@ -49,4 +49,50 @@ describe('core system modules', () => { expect(vpn.vpn).to.have.property('probability'); }); }); + + it('detects bot characteristics', () => { + cy.loadFingerprintOSS().then((fp) => { + const result = fp.detectBot(); + expect(result).to.be.ok; + expect(result.isBot).to.be.a('boolean'); + expect(result.signals).to.be.an('array'); + expect(result.confidence).to.be.a('number'); + }); + }); + + it('detects desktop pointer/hover mismatch bot signal', () => { + cy.visit('/'); + cy.window().then((win) => { + Object.defineProperty(win.navigator, 'userAgent', { + value: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', + configurable: true + }); + cy.stub(win, 'matchMedia').callsFake((query) => { + if (query === '(hover: none)') return { matches: true }; + if (query === '(pointer: fine)') return { matches: false }; + if (query === '(pointer: coarse)') return { matches: false }; + return { matches: false }; + }); + + const result = win.fingerprintOSS.detectBot(); + expect(result.signals).to.include('strong:desktop-no-pointer-hover-mismatch'); + }); + }); + + it('detects desktop headless resolution bot signal', () => { + cy.visit('/'); + cy.window().then((win) => { + Object.defineProperty(win.navigator, 'userAgent', { + value: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36', + configurable: true + }); + Object.defineProperty(win, 'screen', { + value: { width: 800, height: 600 }, + configurable: true + }); + + const result = win.fingerprintOSS.detectBot(); + expect(result.signals).to.include('medium:desktop-default-headless-resolution'); + }); + }); }); diff --git a/test/e2e/cypress/e2e/unit/hash-serialization.cy.js b/test/e2e/cypress/e2e/unit/hash-serialization.cy.js index 22d3700..b07656b 100644 --- a/test/e2e/cypress/e2e/unit/hash-serialization.cy.js +++ b/test/e2e/cypress/e2e/unit/hash-serialization.cy.js @@ -16,11 +16,16 @@ describe('hashing and serialization', () => { it('serializes output to JSON', () => { cy.loadFingerprintOSS().then((fp) => { return fp.getSystemInfo().then((info) => { - return fp.fetchGeolocationInfo().then((geo) => { - return fp.generateJSON(geo, info, 0.5).then((result) => { - expect(result).to.be.ok; - expect(result).to.have.property('hash'); - }); + const geoMock = { + ipAddress: '127.0.0.1', + country: { isoCode: 'US', name: 'United States' }, + city: { name: 'New York', geonameId: 5128581 }, + location: { accuracyRadius: 10, latitude: 40.7128, longitude: -74.0060, timeZone: 'America/New_York' }, + traits: { isAnonymous: false, isAnonymousProxy: false, isAnonymousVpn: false, network: '127.0.0.1/32' } + }; + return fp.generateJSON(geoMock, info, 0.5).then((result) => { + expect(result).to.be.ok; + expect(result).to.have.property('hash'); }); }); });