1+ import os
2+ import re
3+ import uuid
4+ from pathlib import Path
5+ import logging
6+
7+ class CSPFixer :
8+ # static compiled regex patterns
9+ SCRIPT_TAG_PATTERN = re .compile (r'<script([^>]*)>' )
10+ CSP_META_PATTERN = re .compile (r'<meta\s+http-equiv="Content-Security-Policy"[^>]*>\n?' , re .IGNORECASE )
11+ HEAD_TAG_PATTERN = re .compile (r'<head>' , re .IGNORECASE )
12+ def __init__ (self , log_level = logging .INFO ):
13+ self .nonce = str (uuid .uuid4 ())
14+ # CSP meta tags
15+ self .csp = (
16+ "default-src 'self'; "
17+ f"script-src 'self' 'nonce-{ self .nonce } '; "
18+ )
19+ self .csp_meta = f'<meta http-equiv="Content-Security-Policy" content="{ self .csp } ">\n '
20+ logging .basicConfig (
21+ level = log_level ,
22+ format = '%(asctime)s - %(levelname)s - %(message)s' ,
23+ datefmt = '%Y-%m-%d %H:%M:%S'
24+ )
25+
26+ def __add_nonce (self , html_content :str ) -> str :
27+ if not html_content :
28+ return html_content
29+
30+ # replace existing script tags with nonce
31+ modified_content = self .SCRIPT_TAG_PATTERN .sub (
32+ f'<script nonce="{ self .nonce } "\\ 1>' ,
33+ html_content
34+ )
35+
36+ # remove any existing CSP meta tags so there is no conflict
37+ modified_content = self .CSP_META_PATTERN .sub (
38+ '' ,
39+ modified_content
40+ )
41+
42+ # add CSP meta tag in head
43+ with_csp = self .HEAD_TAG_PATTERN .sub (
44+ f'<head>\n { self .csp_meta } ' ,
45+ modified_content
46+ )
47+
48+ return with_csp
49+
50+ def update_files (self , path : Path ) -> bool :
51+ # check valid path
52+ if not os .path .exists (path ):
53+ logging .error (f'Path does not exist: { path } ' )
54+ return False
55+
56+ if not os .path .isdir (path ):
57+ logging .error (f'Path is not a directory: { path } ' )
58+ return False
59+ success = True
60+ processed_count = 0
61+ failed_count = 0
62+ for dirpath , dirnames , filenames in os .walk (path ):
63+ for filename in filenames :
64+ if filename .endswith ('.html' ):
65+ file_path = Path (dirpath ) / filename
66+ logging .debug (f'Processing: { file_path } ' )
67+ try :
68+ with open (file_path , 'r' , encoding = 'utf-8' ) as file :
69+ content = file .read ()
70+
71+ processed = self .__add_nonce (content )
72+
73+ with open (file_path , 'w' , encoding = 'utf-8' ) as file :
74+ file .write (processed )
75+ logging .debug (f'Finished: { file_path } ' )
76+ processed_count += 1
77+ except (OSError , IOError ) as e :
78+ logging .error (f'Error processing { file_path } : { e } ' )
79+ failed_count += 1
80+ success = False
81+ logging .info (f"CSP update process completed. Processed { processed_count } files. Failed { failed_count } files" )
82+ return success
0 commit comments