1+ const SERVICE_URL = "govguard-internal.wmsdev.pl" ;
2+ const TARGET_DOMAIN = "gov.pl"
3+
4+ let serverIpCache = { } ;
5+ const confusablesMap = {
6+ '0' : 'o' , '1' : 'l' , '3' : 'e' , '4' : 'a' , '5' : 's' , '7' : 't' , '@' : 'a' ,
7+ '!' : 'i' , '$' : 's' , 'α' : 'a' , 'ρ' : 'p' , 'rn' : 'm' , 'vv' : 'w' ,
8+ // cyrillic lookalikes common in phishing
9+ 'а' : 'a' , 'с' : 'c' , 'е' : 'e' , 'о' : 'o' , 'р' : 'p' , 'х' : 'x' , 'у' : 'y' ,
10+ 'і' : 'i' , 'к' : 'k' , 'м' : 'm' , 'т' : 't' , 'в' : 'b'
11+ } ;
12+
13+ const normalizeDomain = ( str ) => {
14+ let normalized = str . toLowerCase ( ) ;
15+
16+ normalized = normalized . replace ( / r n / g, "m" ) . replace ( / v v / g, "w" ) ;
17+
18+ return normalized . split ( '' ) . map ( char => confusablesMap [ char ] || char ) . join ( '' ) ;
19+ } ;
20+
21+ const checkDomain = ( domain , tabId ) => {
22+ const TARGET_DOMAIN = "gov.pl" ;
23+
24+ // ignore valid *.gov.pl domain
25+ if ( domain . endsWith ( ".gov.pl" ) || domain === "gov.pl" ) return 0 ;
26+
27+ const segments = domain . split ( "." ) . filter ( Boolean ) ;
28+ const trimmedDomain = segments . slice ( - 2 ) . join ( "." ) || domain ;
29+
30+ // NORMALIZE - turn g0v.pl -> gov.pl etc
31+ const skeletonDomain = normalizeDomain ( trimmedDomain ) ;
32+ const skeletonTarget = normalizeDomain ( TARGET_DOMAIN ) ;
33+
34+ // calculate Distance
35+ const distance = levenshteinFullMatrix ( skeletonDomain , skeletonTarget ) ;
36+
37+ const THRESHOLD = 3 ;
38+
39+ if ( distance <= THRESHOLD && distance !== 0 ) {
40+ console . warn ( `Suspicious Domain Detected! Input: ${ domain } (Skeleton: ${ skeletonDomain } )` ) ;
41+
42+ setTimeout ( ( ) => {
43+ chrome . tabs . sendMessage ( tabId , {
44+ action : "showSuspiciousNotification" ,
45+ domain : domain ,
46+ reason : "Visual similarity to gov.pl"
47+ } )
48+ } , 2000 )
49+ }
50+
51+ return distance ;
52+ } ;
53+
54+ function levenshteinFullMatrix ( str1 , str2 ) {
55+ const m = str1 . length ;
56+ const n = str2 . length ;
57+
58+ const dp = new Array ( m + 1 ) . fill ( null ) . map ( ( ) => new Array ( n + 1 ) . fill ( 0 ) ) ;
59+
60+ for ( let i = 0 ; i <= m ; i ++ ) {
61+ dp [ i ] [ 0 ] = i ;
62+ }
63+ for ( let j = 0 ; j <= n ; j ++ ) {
64+ dp [ 0 ] [ j ] = j ;
65+ }
66+
67+ for ( let i = 1 ; i <= m ; i ++ ) {
68+ for ( let j = 1 ; j <= n ; j ++ ) {
69+ if ( str1 [ i - 1 ] === str2 [ j - 1 ] ) {
70+ dp [ i ] [ j ] = dp [ i - 1 ] [ j - 1 ] ;
71+ } else {
72+ dp [ i ] [ j ] = 1 + Math . min (
73+ // insert
74+ dp [ i ] [ j - 1 ] ,
75+ Math . min (
76+ // remove
77+ dp [ i - 1 ] [ j ] ,
78+ // replace
79+ dp [ i - 1 ] [ j - 1 ]
80+ )
81+ ) ;
82+ }
83+ }
84+ }
85+ return dp [ m ] [ n ] ;
86+ }
87+
88+ chrome . webRequest . onCompleted . addListener (
89+ async ( details ) => {
90+ console . log ( details ) ;
91+
92+ if ( details . ip && details . tabId >= 0 ) {
93+ serverIpCache [ details . tabId ] = details . ip ;
94+ }
95+ try {
96+ const hostname = new URL ( details . url ) . hostname ;
97+ checkDomain ( hostname , details . tabId ) ;
98+ } catch ( _ ) { }
99+ } ,
100+ { urls : [ "<all_urls>" ] , types : [ "main_frame" ] }
101+ ) ;
102+
103+ chrome . runtime . onMessage . addListener ( ( request , sender , sendResponse ) => {
104+ if ( request . action === "getServerIp" ) {
105+ const tabId = sender . tab ?. id ;
106+ if ( tabId !== undefined ) {
107+ const ip = serverIpCache [ tabId ] || "unknown" ;
108+ sendResponse ( ip ) ;
109+ } else {
110+ sendResponse ( "unknown" ) ;
111+ }
112+ }
113+ } ) ;
114+
115+ chrome . tabs . onRemoved . addListener ( ( tabId ) => {
116+ delete serverIpCache [ tabId ] ;
117+ } ) ;
0 commit comments