SpyScan

发现网站上的跟踪脚本、指纹识别和监控技术。

  1. // ==UserScript==
  2. // @name SpyScan
  3. // @namespace https://greasyfork.org/fr/users/1451802
  4. // @version 1.0
  5. // @description Uncover tracking scripts, fingerprinting, and surveillance tactics lurking on the websites you visit
  6. // @description:de Untersuche Websites auf Tracking-Skripte, Fingerprinting und Überwachungsmethoden.
  7. // @description:es Descubre scripts de seguimiento, técnicas de huellas digitales y tácticas de vigilancia en las páginas web que visitas.
  8. // @description:fr Détecte les scripts de suivi, le fingerprinting et les techniques de surveillance cachées sur les sites que vous visitez.
  9. // @description:it Scopri script di tracciamento, tecniche di fingerprinting e metodi di sorveglianza sui siti web che visiti.
  10. // @description:ru Раскрывает трекинговые скрипты, отпечатки браузера и методы слежки на посещаемых сайтах.
  11. // @description:zh-CN 发现网站上的跟踪脚本、指纹识别和监控技术。
  12. // @description:zh-TW 發現網站上的追蹤腳本、指紋辨識和監控技術。
  13. // @description:ja 訪問したサイトに潜むトラッキングスクリプト、フィンガープリント、監視技術を検出。
  14. // @description:ko 방문한 웹사이트에서 추적 스크립트, 브라우저 지문, 감시 기술을 찾아냅니다.
  15. // @author NormalRandomPeople (https://github.com/NormalRandomPeople)
  16. // @match *://*/*
  17. // @grant GM_addStyle
  18. // @license MIT
  19. // @icon https://www.svgrepo.com/show/360090/analyse.svg
  20. // @compatible chrome
  21. // @compatible firefox
  22. // @compatible opera
  23. // @compatible edge
  24. // @compatible brave
  25. // @run-at document-end
  26. // @noframes
  27. // ==/UserScript==
  28.  
  29. /* jshint esversion: 8 */
  30.  
  31. (function() {
  32. 'use strict';
  33.  
  34. // Global arrays to hold detected network responses
  35. let detectedETags = [];
  36. let detectedIPGeolocationRequests = [];
  37. let detectedWebRTCLeaks = [];
  38.  
  39. // List of known IP Geolocation service domains
  40. const ipGeoServices = [
  41. "ipinfo.io",
  42. "ip-api.com",
  43. "ipgeolocation.io",
  44. "geoip-db.com",
  45. "freegeoip.app",
  46. "ip2location.com",
  47. "extreme-ip-lookup.com",
  48. "ip-geolocation.whoisxmlapi.com",
  49. "ipligence.com",
  50. "bigdatacloud.com",
  51. "maxmind.com",
  52. "db-ip.com",
  53. "ipinfodb.com",
  54. "ipdata.co",
  55. "abstractapi.com",
  56. "ipapi.com",
  57. "ipstack.com",
  58. "geo.ipify.org",
  59. "ipwhois.io",
  60. "ipregistry.co",
  61. "telize.com",
  62. "geoplugin.com"
  63. ];
  64.  
  65. // Patch fetch to capture responses with ETag headers and IP Geolocation requests
  66. const originalFetch = window.fetch;
  67. window.fetch = async function(...args) {
  68. let reqUrl = "";
  69. if (typeof args[0] === "string") {
  70. reqUrl = args[0];
  71. } else if (args[0] instanceof Request) {
  72. reqUrl = args[0].url;
  73. }
  74. if (ipGeoServices.some(domain => reqUrl.includes(domain))) {
  75. detectedIPGeolocationRequests.push({ url: reqUrl });
  76. }
  77. const response = await originalFetch.apply(this, args);
  78. const responseClone = response.clone();
  79. try {
  80. const etag = responseClone.headers.get("ETag");
  81. if (etag) {
  82. detectedETags.push({
  83. url: responseClone.url,
  84. etag: etag
  85. });
  86. }
  87. } catch (err) {
  88. console.warn("ETag header could not be read:", err);
  89. }
  90. return response;
  91. };
  92.  
  93. // Patch XMLHttpRequest to capture responses with ETag headers and IP Geolocation requests
  94. const originalXHROpen = XMLHttpRequest.prototype.open;
  95. XMLHttpRequest.prototype.open = function(...args) {
  96. let reqUrl = args[1];
  97. if (ipGeoServices.some(domain => reqUrl.includes(domain))) {
  98. detectedIPGeolocationRequests.push({ url: reqUrl });
  99. }
  100. this.addEventListener("readystatechange", function() {
  101. if (this.readyState === 4) {
  102. try {
  103. const etag = this.getResponseHeader("ETag");
  104. if (etag) {
  105. detectedETags.push({
  106. url: this.responseURL,
  107. etag: etag
  108. });
  109. }
  110. } catch (err) {
  111. console.warn("ETag header could not be read from XHR:", err);
  112. }
  113. }
  114. });
  115. return originalXHROpen.apply(this, args);
  116. };
  117.  
  118. let scanButton = document.createElement("button");
  119. scanButton.id = "aptScanButton";
  120. scanButton.textContent = "";
  121. let svgImg = document.createElement("img");
  122. svgImg.src = "https://www.svgrepo.com/show/360090/analyse.svg";
  123. svgImg.style.width = "32px";
  124. svgImg.style.height = "32px";
  125. svgImg.style.display = "block";
  126. svgImg.style.margin = "0 auto";
  127. scanButton.appendChild(svgImg);
  128. scanButton.style.position = "fixed";
  129. scanButton.style.bottom = "10px";
  130. scanButton.style.left = "10px";
  131. scanButton.style.padding = "15px 20px";
  132. scanButton.style.border = "none";
  133. scanButton.style.backgroundColor = "black";
  134. scanButton.style.color = "#fff";
  135. scanButton.style.borderRadius = "10px";
  136. scanButton.style.cursor = "pointer";
  137. scanButton.style.zIndex = "9999999999";
  138. scanButton.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.3)";
  139. scanButton.style.transition = "background-color 0.3s, transform 0.3s";
  140. scanButton.addEventListener("mouseover", function() {
  141. scanButton.style.backgroundColor = "#333";
  142. scanButton.style.transform = "scale(1.05)";
  143. });
  144. scanButton.addEventListener("mouseout", function() {
  145. scanButton.style.backgroundColor = "black";
  146. scanButton.style.transform = "scale(1)";
  147. });
  148.  
  149. document.body.appendChild(scanButton);
  150.  
  151. let auditWindow = document.createElement("div");
  152. auditWindow.id = "aptAuditWindow";
  153. let windowContent = document.createElement("div");
  154. windowContent.className = "aptWindowContent";
  155. auditWindow.appendChild(windowContent);
  156. document.body.appendChild(auditWindow);
  157.  
  158. auditWindow.addEventListener("click", function(event) {
  159. if (event.target === auditWindow) {
  160. auditWindow.style.display = "none";
  161. }
  162. });
  163.  
  164. GM_addStyle(`
  165. #aptScanButton {
  166. font-family: Arial, sans-serif;
  167. background-color: black;
  168. color: #fff;
  169. border: none;
  170. padding: 15px 20px;
  171. font-size: 18px;
  172. border-radius: 10px;
  173. cursor: pointer;
  174. transition: background-color 0.3s, transform 0.3s;
  175. }
  176. #aptScanButton:hover {
  177. background-color: #333;
  178. }
  179. #aptAuditWindow {
  180. display: none;
  181. position: fixed;
  182. top: 0;
  183. left: 0;
  184. width: 100%;
  185. height: 100%;
  186. background-color: rgba(0, 0, 0, 0.7);
  187. color: #fff;
  188. font-family: Arial, sans-serif;
  189. overflow: auto;
  190. padding: 20px;
  191. z-index: 99999999999;
  192. box-sizing: border-box;
  193. }
  194. .aptWindowContent {
  195. max-width: 800px;
  196. margin: 50px auto;
  197. background-color: #333;
  198. border-radius: 8px;
  199. padding: 20px;
  200. box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
  201. overflow-y: auto;
  202. max-height: 80%;
  203. }
  204. .aptWindowContent h2 {
  205. text-align: center;
  206. margin-bottom: 10px;
  207. font-size: 1.8em;
  208. }
  209. .aptWindowContent p {
  210. font-size: 1em;
  211. line-height: 1.5;
  212. }
  213. .aptWindowContent ul {
  214. list-style-type: none;
  215. padding: 0;
  216. }
  217. .aptWindowContent li {
  218. background-color: #444;
  219. padding: 10px;
  220. margin: 5px 0;
  221. border-radius: 5px;
  222. word-wrap: break-word;
  223. position: relative;
  224. }
  225. .aptTitle {
  226. font-weight: bold;
  227. font-family: Arial;
  228. color: grey;
  229. }
  230. .aptSectionTitle {
  231. font-size: 1.3em;
  232. font-weight: bold;
  233. margin-bottom: 10px;
  234. padding-bottom: 5px;
  235. border-bottom: 2px solid #666;
  236. margin-top: 20px;
  237. }
  238. .aptDangerLevel {
  239. font-weight: bold;
  240. font-size: 1.1em;
  241. }
  242. .aptDangerLevelLow {
  243. color: #28A745;
  244. }
  245. .aptDangerLevelMedium {
  246. color: #FFA500;
  247. }
  248. .aptDangerLevelHigh {
  249. color: #FF4C4C;
  250. }
  251. .aptloading-spinner {
  252. border: 4px solid rgba(255, 255, 255, 0.3);
  253. border-top: 4px solid #fff;
  254. border-radius: 50%;
  255. width: 40px;
  256. height: 40px;
  257. animation: spin 1s linear infinite;
  258. margin: 20px auto;
  259. }
  260. @keyframes spin {
  261. 0% { transform: rotate(0deg); }
  262. 100% { transform: rotate(360deg); }
  263. }
  264. `);
  265.  
  266. function getCookies() {
  267. return document.cookie.split(';').map(cookie => cookie.trim()).filter(cookie => cookie);
  268. }
  269.  
  270. async function detectWebRTCLeak() {
  271. return new Promise(resolve => {
  272. const rtcPeerConnection = new RTCPeerConnection({
  273. iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
  274. });
  275.  
  276. rtcPeerConnection.createDataChannel("");
  277. rtcPeerConnection.createOffer().then(offer => rtcPeerConnection.setLocalDescription(offer));
  278.  
  279. rtcPeerConnection.onicecandidate = event => {
  280. if (event.candidate) {
  281. const ipRegex = /([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/;
  282. const match = ipRegex.exec(event.candidate.candidate);
  283. if (match && !match[1].startsWith("192.168") && !match[1].startsWith("10.") && !match[1].startsWith("172.")) {
  284. detectedWebRTCLeaks.push({
  285. name: "WebRTC Leak",
  286. danger: "high",
  287. description: `Your real IP is exposed via WebRTC: ${match[1]}`
  288. });
  289. }
  290. }
  291. };
  292.  
  293. setTimeout(() => {
  294. rtcPeerConnection.close();
  295. resolve(detectedWebRTCLeaks);
  296. }, 5000);
  297. });
  298. }
  299.  
  300. function detectWebBeacons() {
  301. let beacons = [];
  302. let images = document.getElementsByTagName("img");
  303. for (let img of images) {
  304. let width = img.getAttribute("width") || img.width;
  305. let height = img.getAttribute("height") || img.height;
  306. let computedStyle = window.getComputedStyle(img);
  307. if ((parseInt(width) === 1 && parseInt(height) === 1) ||
  308. (img.naturalWidth === 1 && img.naturalHeight === 1) ||
  309. (computedStyle.width === "1px" && computedStyle.height === "1px")) {
  310. beacons.push({
  311. name: "Web Beacon",
  312. src: img.src,
  313. danger: "medium",
  314. description: "Detected a 1x1 pixel image that could be used as a web beacon."
  315. });
  316. }
  317. }
  318. return beacons;
  319. }
  320.  
  321. function detectEtagTracking() {
  322. let etagTrackers = [];
  323. detectedETags.forEach(item => {
  324. etagTrackers.push({
  325. name: "Etag Tracking",
  326. danger: "medium",
  327. description: `ETag detected from ${item.url} with value ${item.etag}`
  328. });
  329. });
  330. return etagTrackers;
  331. }
  332.  
  333. function detectIPGeolocation() {
  334. let ipGeoTrackers = [];
  335. detectedIPGeolocationRequests.forEach(item => {
  336. ipGeoTrackers.push({
  337. name: "IP Geolocation",
  338. danger: "high",
  339. description: `IP Geolocation request detected to ${item.url}`
  340. });
  341. });
  342. return ipGeoTrackers;
  343. }
  344.  
  345. function detectTrackersSync() {
  346. const trackers = [];
  347. const knownTrackers = [
  348. { name: 'Google Analytics', regex: /analytics\.js/, danger: 'medium', description: 'Tracks user behavior for analytics and advertising purposes.' },
  349. { name: 'Facebook Pixel', regex: /facebook\.com\/tr\.js/, danger: 'medium', description: 'Tracks user activity for targeted ads on Facebook.' },
  350. { name: 'Hotjar', regex: /hotjar\.com/, danger: 'medium', description: 'Records user behavior such as clicks and scrolling for website optimization.' },
  351. { name: 'AdSense', regex: /pagead2\.googlesyndication\.com/, danger: 'medium', description: 'Google\'s ad network, tracks user activity for ads.' },
  352. { name: 'Google Tag Manager', regex: /googletagmanager\.com/, danger: 'medium', description: 'Manages JavaScript and HTML tags for tracking purposes.' },
  353. { name: 'Amazon Tracking', regex: /amazon\.com\/at\/tag/, danger: 'low', description: 'Tracks activity for Amazon ads and recommendations.' },
  354. { name: 'Twitter', regex: /twitter\.com\/widgets\.js/, danger: 'low', description: 'Tracks activity for Twitter widgets and ads.' },
  355. { name: 'Local Storage', regex: /localStorage/, danger: 'low', description: 'Stores data in the browser that can be used for persistent tracking.' },
  356. { name: 'Session Storage', regex: /sessionStorage/, danger: 'low', description: 'Stores data temporarily in the browser during a session.' },
  357. ];
  358.  
  359. knownTrackers.forEach(tracker => {
  360. if (document.body.innerHTML.match(tracker.regex)) {
  361. trackers.push({ name: tracker.name, danger: tracker.danger, description: tracker.description });
  362. }
  363. });
  364.  
  365. let webBeacons = detectWebBeacons();
  366. webBeacons.forEach(beacon => trackers.push(beacon));
  367.  
  368. let etagTrackers = detectEtagTracking();
  369. etagTrackers.forEach(etag => trackers.push(etag));
  370.  
  371. let ipGeoTrackers = detectIPGeolocation();
  372. ipGeoTrackers.forEach(ipgeo => trackers.push(ipgeo));
  373.  
  374. return trackers;
  375. }
  376.  
  377. function detectZombieCookies() {
  378. return new Promise(resolve => {
  379. const testName = "aptZombieTest";
  380. document.cookie = `${testName}=test; path=/;`;
  381. document.cookie = `${testName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
  382. setTimeout(() => {
  383. if (document.cookie.includes(testName + "=")) {
  384. resolve([{
  385. name: "Zombie Cookies",
  386. danger: "high",
  387. description: "Test cookie was recreated, indicating persistent zombie cookie behavior."
  388. }]);
  389. } else {
  390. resolve([]);
  391. }
  392. }, 1000);
  393. });
  394. }
  395.  
  396. async function detectAllTrackers() {
  397. const trackersSync = detectTrackersSync();
  398. const zombieTrackers = await detectZombieCookies();
  399. const webrtcLeaks = await detectWebRTCLeak();
  400. return trackersSync.concat(zombieTrackers, webrtcLeaks);
  401. }
  402.  
  403. async function detectFingerprinting() {
  404. let fingerprintingMethods = [];
  405. try {
  406. // Canvas Fingerprinting
  407. const canvas = document.createElement('canvas');
  408. const ctx = canvas.getContext('2d');
  409. ctx.textBaseline = "top";
  410. ctx.font = "14px 'Arial'";
  411. ctx.fillText('test', 2, 2);
  412. const data = canvas.toDataURL();
  413. if (data !== 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA') {
  414. fingerprintingMethods.push({ name: 'Canvas Fingerprinting', danger: 'high', description: 'Uses HTML5 canvas to uniquely identify users based on their rendering properties.' });
  415. }
  416. } catch (e) {}
  417.  
  418. try {
  419. // WebGL Fingerprinting
  420. const glCanvas = document.createElement('canvas');
  421. const gl = glCanvas.getContext('webgl');
  422. if (gl) {
  423. const fingerprint = gl.getParameter(gl.VERSION);
  424. if (fingerprint) {
  425. fingerprintingMethods.push({ name: 'WebGL Fingerprinting', danger: 'high', description: 'Uses WebGL rendering data to track users.' });
  426. }
  427. }
  428. } catch (e) {}
  429.  
  430. try {
  431. // Font Fingerprinting
  432. const fontFingerprint = document.createElement('div');
  433. fontFingerprint.style.fontFamily = "'Arial', 'sans-serif'";
  434. fontFingerprint.innerText = "test";
  435. document.body.appendChild(fontFingerprint);
  436. const fontFingerprintData = window.getComputedStyle(fontFingerprint).fontFamily;
  437. document.body.removeChild(fontFingerprint);
  438. if (fontFingerprintData.includes('Arial')) {
  439. fingerprintingMethods.push({ name: 'Font Fingerprinting', danger: 'medium', description: 'Uses unique system fonts to track users across sessions.' });
  440. }
  441. } catch (e) {}
  442.  
  443. try {
  444. // AudioContext Fingerprinting using AudioWorkletNode if available
  445. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  446. let audioFingerprintValue = 0;
  447. if (audioContext.audioWorklet) {
  448. // Define an inline AudioWorkletProcessor as a string
  449. const workletCode = `
  450. class FingerprintProcessor extends AudioWorkletProcessor {
  451. process(inputs, outputs, parameters) {
  452. let sum = 0;
  453. if (inputs[0] && inputs[0][0]) {
  454. const input = inputs[0][0];
  455. for (let i = 0; i < input.length; i++) {
  456. sum += input[i];
  457. }
  458. }
  459. this.port.postMessage(sum);
  460. return false;
  461. }
  462. }
  463. registerProcessor('fingerprint-processor', FingerprintProcessor);
  464. `;
  465. const blob = new Blob([workletCode], { type: 'application/javascript' });
  466. const moduleUrl = URL.createObjectURL(blob);
  467. await audioContext.audioWorklet.addModule(moduleUrl);
  468. const workletNode = new AudioWorkletNode(audioContext, 'fingerprint-processor');
  469. audioFingerprintValue = await new Promise(resolve => {
  470. workletNode.port.onmessage = (event) => {
  471. resolve(event.data);
  472. };
  473. workletNode.connect(audioContext.destination);
  474. });
  475. workletNode.disconnect();
  476. } else {
  477. // Fallback to ScriptProcessorNode if AudioWorklet is not available
  478. const buffer = audioContext.createBuffer(1, 1, 22050);
  479. const processor = audioContext.createScriptProcessor(0, 1, 1);
  480. processor.connect(audioContext.destination);
  481. audioFingerprintValue = buffer.getChannelData(0)[0];
  482. processor.disconnect();
  483. }
  484. if (audioFingerprintValue !== 0) {
  485. fingerprintingMethods.push({ name: 'AudioContext Fingerprinting', danger: 'high', description: 'Uses audio hardware properties to uniquely identify users. Value: ' + audioFingerprintValue });
  486. }
  487. } catch (e) {}
  488. return fingerprintingMethods;
  489. }
  490.  
  491. async function showAuditResults() {
  492. windowContent.innerHTML = '<div class="aptloading-spinner"></div><p style="text-align: center;">Scanning...</p>';
  493. auditWindow.style.display = "block";
  494. const cookies = getCookies();
  495. const trackers = await detectAllTrackers();
  496. const fingerprinting = await detectFingerprinting();
  497. windowContent.innerHTML = `
  498. <h2 class="aptTitle">Privacy Audit Results</h2>
  499. <div class="aptSectionTitle">Trackers & Fingerprinting</div>
  500. <ul>
  501. ${trackers.length > 0 ? trackers.map(tracker => `
  502. <li>${tracker.name} <span class="aptDangerLevel aptDangerLevel${capitalizeFirstLetter(tracker.danger)}">${capitalizeFirstLetter(tracker.danger)}</span> - ${tracker.description}</li>`).join('') : '<li>No trackers found.</li>'}
  503. </ul>
  504. <div class="aptSectionTitle">Cookies</div>
  505. <ul>
  506. ${!cookies.length ? '<li>No cookies found.</li>' : cookies.map(cookie => `<li>${cookie}</li>`).join('') }
  507. </ul>
  508. `;
  509. auditWindow.style.display = "block";
  510. }
  511.  
  512. function capitalizeFirstLetter(string) {
  513. return string.charAt(0).toUpperCase() + string.slice(1);
  514. }
  515.  
  516. scanButton.addEventListener("click", async function() {
  517. await showAuditResults();
  518. });
  519.  
  520. })();