mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-04 12:18:17 +00:00 
			
		
		
		
	Refactor CSP filters to use get_url for domain resolution and update tests to check CSP directives order-independently. See: https://chatgpt.com/share/68b49e5c-6774-800f-9d8e-a3f980799c08
This commit is contained in:
		@@ -5,6 +5,7 @@ import sys, os
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 | 
					sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 | 
				
			||||||
from module_utils.config_utils import get_app_conf
 | 
					from module_utils.config_utils import get_app_conf
 | 
				
			||||||
 | 
					from module_utils.get_url import get_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FilterModule(object):
 | 
					class FilterModule(object):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
@@ -131,17 +132,14 @@ class FilterModule(object):
 | 
				
			|||||||
                flags = self.get_csp_flags(applications, application_id, directive)
 | 
					                flags = self.get_csp_flags(applications, application_id, directive)
 | 
				
			||||||
                tokens += flags
 | 
					                tokens += flags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if directive in [ 'script-src-elem', 'connect-src', 'style-src-elem' ]:
 | 
				
			||||||
 | 
					                    # Allow fetching from internal CDN as default for all applications
 | 
				
			||||||
 | 
					                    tokens.append(get_url(domains,'web-svc-cdn',web_protocol))
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                if directive in ['script-src-elem', 'connect-src']:
 | 
					                if directive in ['script-src-elem', 'connect-src']:
 | 
				
			||||||
                    # Matomo integration
 | 
					                    # Matomo integration
 | 
				
			||||||
                    if self.is_feature_enabled(applications, matomo_feature_name, application_id):
 | 
					                    if self.is_feature_enabled(applications, matomo_feature_name, application_id):
 | 
				
			||||||
                        matomo_domain = domains.get('web-app-matomo')[0]
 | 
					                        tokens.append(get_url(domains,'web-app-matomo',web_protocol))
 | 
				
			||||||
                        if matomo_domain:
 | 
					 | 
				
			||||||
                            tokens.append(f"{web_protocol}://{matomo_domain}")
 | 
					 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                    # Allow fetching from internal CDN as default for all applications
 | 
					 | 
				
			||||||
                    domain = domains.get('web-svc-cdn')[0]
 | 
					 | 
				
			||||||
                    tokens.append(f"{web_protocol}://{domain}")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # ReCaptcha integration: allow loading scripts from Google if feature enabled
 | 
					                # ReCaptcha integration: allow loading scripts from Google if feature enabled
 | 
				
			||||||
                if self.is_feature_enabled(applications, 'recaptcha', application_id):
 | 
					                if self.is_feature_enabled(applications, 'recaptcha', application_id):
 | 
				
			||||||
@@ -159,12 +157,10 @@ class FilterModule(object):
 | 
				
			|||||||
                    if self.is_feature_enabled(applications, 'logout', application_id):
 | 
					                    if self.is_feature_enabled(applications, 'logout', application_id):
 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
                        # Allow logout via infinito logout proxy
 | 
					                        # Allow logout via infinito logout proxy
 | 
				
			||||||
                        domain = domains.get('web-svc-logout')[0] 
 | 
					                        tokens.append(get_url(domains,'web-svc-logout',web_protocol))
 | 
				
			||||||
                        tokens.append(f"{web_protocol}://{domain}")
 | 
					 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
                        # Allow logout via keycloak app
 | 
					                        # Allow logout via keycloak app
 | 
				
			||||||
                        domain = domains.get('web-app-keycloak')[0]
 | 
					                        tokens.append(get_url(domains,'web-app-keycloak',web_protocol))
 | 
				
			||||||
                        tokens.append(f"{web_protocol}://{domain}")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # whitelist
 | 
					                # whitelist
 | 
				
			||||||
                tokens += self.get_csp_whitelist(applications, application_id, directive)
 | 
					                tokens += self.get_csp_whitelist(applications, application_id, directive)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,6 +55,28 @@ class TestCspFilters(unittest.TestCase):
 | 
				
			|||||||
            'web-svc-cdn': ['cdn.example.org'],
 | 
					            'web-svc-cdn': ['cdn.example.org'],
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # --- Helpers -------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_directive_tokens(self, header: str, directive: str):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Extract tokens (as a list of strings) for a given directive from a CSP header.
 | 
				
			||||||
 | 
					        Example: for "connect-src 'self' https://a https://b;" -> ["'self'", "https://a", "https://b"]
 | 
				
			||||||
 | 
					        Returns [] if not found.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        for part in header.split(';'):
 | 
				
			||||||
 | 
					            part = part.strip()
 | 
				
			||||||
 | 
					            if not part:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					            if part.startswith(directive + ' '):
 | 
				
			||||||
 | 
					                # remove directive name, split remainder by spaces
 | 
				
			||||||
 | 
					                remainder = part[len(directive):].strip()
 | 
				
			||||||
 | 
					                return [tok for tok in remainder.split(' ') if tok]
 | 
				
			||||||
 | 
					            if part == directive:  # unlikely, but guard
 | 
				
			||||||
 | 
					                return []
 | 
				
			||||||
 | 
					        return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # --- Tests ---------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get_csp_whitelist_list(self):
 | 
					    def test_get_csp_whitelist_list(self):
 | 
				
			||||||
        result = self.filter.get_csp_whitelist(self.apps, 'app1', 'script-src-elem')
 | 
					        result = self.filter.get_csp_whitelist(self.apps, 'app1', 'script-src-elem')
 | 
				
			||||||
        self.assertEqual(result, ['https://cdn.example.com'])
 | 
					        self.assertEqual(result, ['https://cdn.example.com'])
 | 
				
			||||||
@@ -85,18 +107,23 @@ class TestCspFilters(unittest.TestCase):
 | 
				
			|||||||
        header = self.filter.build_csp_header(self.apps, 'app1', self.domains, web_protocol='https')
 | 
					        header = self.filter.build_csp_header(self.apps, 'app1', self.domains, web_protocol='https')
 | 
				
			||||||
        # Ensure core directives are present
 | 
					        # Ensure core directives are present
 | 
				
			||||||
        self.assertIn("default-src 'self';", header)
 | 
					        self.assertIn("default-src 'self';", header)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # script-src-elem should include 'self', Matomo, internes CDN und explizite Whitelist-CDN
 | 
					        # script-src-elem should include 'self', Matomo, internes CDN und explizite Whitelist-CDN
 | 
				
			||||||
        self.assertIn("script-src-elem 'self'", header)
 | 
					        self.assertIn("script-src-elem 'self'", header)
 | 
				
			||||||
        self.assertIn("https://matomo.example.org", header)
 | 
					        self.assertIn("https://matomo.example.org", header)
 | 
				
			||||||
        self.assertIn("https://cdn.example.org", header)   # internes CDN
 | 
					        self.assertIn("https://cdn.example.org", header)   # internes CDN
 | 
				
			||||||
        self.assertIn("https://cdn.example.com", header)   # Whitelist
 | 
					        self.assertIn("https://cdn.example.com", header)   # Whitelist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # script-src directive should include unsafe-eval
 | 
					        # script-src directive should include unsafe-eval
 | 
				
			||||||
        self.assertIn("script-src 'self' 'unsafe-eval'", header)
 | 
					        self.assertIn("script-src 'self' 'unsafe-eval'", header)
 | 
				
			||||||
        # connect-src directive
 | 
					
 | 
				
			||||||
        self.assertIn(
 | 
					        # connect-src directive (reihenfolgeunabhängig prüfen)
 | 
				
			||||||
            "connect-src 'self' https://matomo.example.org https://cdn.example.org https://api.example.com;",
 | 
					        tokens = self._get_directive_tokens(header, "connect-src")
 | 
				
			||||||
            header
 | 
					        self.assertIn("'self'", tokens)
 | 
				
			||||||
        )
 | 
					        self.assertIn("https://matomo.example.org", tokens)
 | 
				
			||||||
 | 
					        self.assertIn("https://cdn.example.org", tokens)
 | 
				
			||||||
 | 
					        self.assertIn("https://api.example.com", tokens)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # ends with img-src
 | 
					        # ends with img-src
 | 
				
			||||||
        self.assertTrue(header.strip().endswith('img-src * data: blob:;'))
 | 
					        self.assertTrue(header.strip().endswith('img-src * data: blob:;'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user