@@ -11,6 +11,24 @@ use crate::error::{report_error, CliResult};
1111
1212static GTM_REGEX : LazyLock < Regex > =
1313 LazyLock :: new ( || Regex :: new ( r"\bGTM-[A-Z0-9]+\b" ) . expect ( "should compile GTM regex" ) ) ;
14+ static GPT_INLINE_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
15+ Regex :: new ( r"(?i)\b(?:googletag|gpt\.js|googletagservices|securepubads)\b" )
16+ . expect ( "should compile GPT inline regex" )
17+ } ) ;
18+ static DIDOMI_INLINE_REGEX : LazyLock < Regex > =
19+ LazyLock :: new ( || Regex :: new ( r"(?i)\bdidomi\b" ) . expect ( "should compile Didomi inline regex" ) ) ;
20+ static DATADOME_INLINE_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
21+ Regex :: new ( r"(?i)\bdatadome\b" ) . expect ( "should compile DataDome inline regex" )
22+ } ) ;
23+ static PERMUTIVE_INLINE_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
24+ Regex :: new ( r"(?i)\bpermutive\b" ) . expect ( "should compile Permutive inline regex" )
25+ } ) ;
26+ static LOCKR_INLINE_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
27+ Regex :: new ( r"(?i)(?:\blockr\b|\bloc\.kr\b)" ) . expect ( "should compile Lockr inline regex" )
28+ } ) ;
29+ static PREBID_INLINE_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
30+ Regex :: new ( r"(?i)(?:\bprebid\b|\bpbjs\b)" ) . expect ( "should compile Prebid inline regex" )
31+ } ) ;
1432
1533pub ( crate ) fn analyze_collected_page ( collected : & CollectedPage ) -> CliResult < AuditArtifact > {
1634 let final_url = collected
@@ -22,7 +40,6 @@ pub(crate) fn analyze_collected_page(collected: &CollectedPage) -> CliResult<Aud
2240
2341 let document = Html :: parse_document ( & collected. html ) ;
2442 let title_selector = Selector :: parse ( "title" ) . expect ( "should parse title selector" ) ;
25- let script_selector = Selector :: parse ( "script" ) . expect ( "should parse script selector" ) ;
2643 let derived_title = document
2744 . select ( & title_selector)
2845 . next ( )
@@ -46,30 +63,15 @@ pub(crate) fn analyze_collected_page(collected: &CollectedPage) -> CliResult<Aud
4663 ) ) ;
4764 }
4865
49- for element in document . select ( & script_selector ) {
50- if let Some ( src) = element . value ( ) . attr ( " src" ) {
66+ for tag in & collected . script_tags {
67+ if let Some ( src) = & tag . src {
5168 if let Ok ( asset_url) = final_url. join ( src) {
5269 let integration = detect_integration_from_url ( & asset_url) ;
5370 record_integration ( & mut integrations, & integration, asset_url. as_str ( ) ) ;
5471 insert_asset ( & mut assets_by_url, & final_url, & asset_url, integration) ;
5572 } else {
5673 warnings. push ( format ! ( "could not resolve script URL `{src}`" ) ) ;
5774 }
58- } else {
59- let inline_text = element. text ( ) . collect :: < Vec < _ > > ( ) . join ( " " ) ;
60- for ( integration_id, evidence) in detect_integrations_from_inline_script ( & inline_text) {
61- integrations. entry ( integration_id) . or_insert ( evidence) ;
62- }
63- }
64- }
65-
66- for tag in & collected. script_tags {
67- if let Some ( src) = & tag. src {
68- if let Ok ( asset_url) = Url :: parse ( src) {
69- let integration = detect_integration_from_url ( & asset_url) ;
70- record_integration ( & mut integrations, & integration, asset_url. as_str ( ) ) ;
71- insert_asset ( & mut assets_by_url, & final_url, & asset_url, integration) ;
72- }
7375 }
7476
7577 if let Some ( inline_text) = & tag. inline_text {
@@ -167,6 +169,7 @@ pub(crate) fn classify_party(page_url: &Url, asset_url: &Url) -> AssetParty {
167169}
168170
169171fn host_matches ( page_host : & str , asset_host : & str ) -> bool {
172+ // This is an advisory heuristic, not public-suffix-aware eTLD+1 classification.
170173 asset_host == page_host
171174 || asset_host
172175 . strip_suffix ( page_host)
@@ -213,9 +216,15 @@ pub(crate) fn detect_integrations_from_inline_script(script: &str) -> Vec<(Strin
213216 ) ) ;
214217 }
215218
216- let lowered = script. to_ascii_lowercase ( ) ;
217- for integration in [ "gpt" , "didomi" , "datadome" , "permutive" , "lockr" , "prebid" ] {
218- if lowered. contains ( integration) {
219+ for ( integration, regex) in [
220+ ( "gpt" , & * GPT_INLINE_REGEX ) ,
221+ ( "didomi" , & * DIDOMI_INLINE_REGEX ) ,
222+ ( "datadome" , & * DATADOME_INLINE_REGEX ) ,
223+ ( "permutive" , & * PERMUTIVE_INLINE_REGEX ) ,
224+ ( "lockr" , & * LOCKR_INLINE_REGEX ) ,
225+ ( "prebid" , & * PREBID_INLINE_REGEX ) ,
226+ ] {
227+ if regex. is_match ( script) {
219228 matches. push ( (
220229 integration. to_string ( ) ,
221230 format ! ( "inline script matched `{integration}`" ) ,
@@ -259,16 +268,20 @@ mod tests {
259268 requested_url : "https://publisher.example/page" . to_string ( ) ,
260269 final_url : "https://publisher.example/page" . to_string ( ) ,
261270 page_title : Some ( "Browser Title" . to_string ( ) ) ,
262- html : r#"<html><head><title>HTML Title</title><script src="https://www.googletagmanager.com/gtm.js?id=GTM-ABCD123"></script></head></html>"# . to_string ( ) ,
263- script_tags : vec ! [ CollectedScriptTag {
264- src: Some ( "https://securepubads.g.doubleclick.net/tag/js/gpt.js" . to_string( ) ) ,
265- inline_text: None ,
266- } ] ,
271+ html : r#"<html><head><title>HTML Title</title></head></html>"# . to_string ( ) ,
272+ script_tags : vec ! [
273+ CollectedScriptTag {
274+ src: Some ( "https://www.googletagmanager.com/gtm.js?id=GTM-ABCD123" . to_string( ) ) ,
275+ inline_text: None ,
276+ } ,
277+ CollectedScriptTag {
278+ src: Some ( "https://securepubads.g.doubleclick.net/tag/js/gpt.js" . to_string( ) ) ,
279+ inline_text: None ,
280+ } ,
281+ ] ,
267282 network_requests : vec ! [ CollectedRequest {
268283 url: "https://cdn.example.com/dynamic.js" . to_string( ) ,
269- method: "GET" . to_string( ) ,
270284 resource_type: Some ( "Script" . to_string( ) ) ,
271- status: Some ( 200 ) ,
272285 } ] ,
273286 warnings : vec ! [ "partial settle" . to_string( ) ] ,
274287 } ;
@@ -345,9 +358,7 @@ mod tests {
345358 } ] ,
346359 network_requests : vec ! [ CollectedRequest {
347360 url: "https://cdn.example.com/prebid.js" . to_string( ) ,
348- method: "GET" . to_string( ) ,
349361 resource_type: Some ( "script" . to_string( ) ) ,
350- status: Some ( 200 ) ,
351362 } ] ,
352363 warnings : Vec :: new ( ) ,
353364 } ;
@@ -371,8 +382,17 @@ mod tests {
371382 requested_url : "https://publisher.example/page" . to_string ( ) ,
372383 final_url : "https://publisher.example/path/page" . to_string ( ) ,
373384 page_title : None ,
374- html : r#"<html><head><script src="/static/app.js"></script><script src="http://[invalid"></script></head></html>"# . to_string ( ) ,
375- script_tags : Vec :: new ( ) ,
385+ html : "<html><head></head></html>" . to_string ( ) ,
386+ script_tags : vec ! [
387+ CollectedScriptTag {
388+ src: Some ( "/static/app.js" . to_string( ) ) ,
389+ inline_text: None ,
390+ } ,
391+ CollectedScriptTag {
392+ src: Some ( "http://[invalid" . to_string( ) ) ,
393+ inline_text: None ,
394+ } ,
395+ ] ,
376396 network_requests : Vec :: new ( ) ,
377397 warnings : Vec :: new ( ) ,
378398 } ;
@@ -463,6 +483,22 @@ mod tests {
463483 . any( |( integration, _) | integration == "didomi" ) ) ;
464484 }
465485
486+ #[ test]
487+ fn detect_integrations_from_inline_script_avoids_short_substring_matches ( ) {
488+ let matches = detect_integrations_from_inline_script ( "const svgptimize = blockrResult;" ) ;
489+
490+ assert ! (
491+ !matches. iter( ) . any( |( integration, _) | integration == "gpt" ) ,
492+ "should not match incidental GPT substrings"
493+ ) ;
494+ assert ! (
495+ !matches
496+ . iter( )
497+ . any( |( integration, _) | integration == "lockr" ) ,
498+ "should not match lockr inside a larger token"
499+ ) ;
500+ }
501+
466502 #[ test]
467503 fn detect_integration_from_url_recognizes_known_patterns ( ) {
468504 let cases = [
0 commit comments