Discovery: Audit the AI Gateway

This issue is to capture work to audit the AI Gateway for FIPS/FEDRAMP compliance.

To audit the AIGW, we would need something similar to the below as an audit script. This will give us a rough idea of what the LoE would be to swap out the offending packages and break that work into multiple issues, similar to how we approached Source Code BE FIPS Compliance (&6476 - closed) • Unassigned:

import os
import re
import subprocess
from pathlib import Path
from typing import List, Dict, Set, Tuple

class GitLabFIPSAuditor:
    """
    Audits Python codebase for FIPS compliance according to GitLab standards.
    Focuses on:
    1. Approved cryptographic modules
    2. Approved algorithms and key sizes
    3. Non-compliant cryptographic operations
    """
    
    # Approved cryptographic modules per GitLab standards
    APPROVED_MODULES = {
        'openssl',  # OpenSSL Cryptographic Module
        'cryptography',  # Python cryptography package using OpenSSL
        'libgcrypt',  # Libgcrypt Cryptographic Module
        'linux-crypto'  # Linux Kernel Crypto API
    }
    
    # Non-FIPS compliant algorithms per GitLab standards
    NON_COMPLIANT_ALGORITHMS = {
        'MD5',          # Not allowed at all per GitLab standard
        'SHA1',         # Not allowed for cryptographic functions
        'DSA',          # No longer approved per FIPS 186-5
        'RC4',
        'DES', 
        'Blowfish',
        'bcrypt',       # Not FIPS approved (though used with feature flag)
        'IDEA'
    }
    
    # Approved TLS cipher suites in order of preference
    APPROVED_TLS_CIPHERS = {
        'ECDHE-ECDSA-AES256-GCM-SHA384',
        'ECDHE-RSA-AES256-GCM-SHA384',
        'ECDHE-ECDSA-AES128-GCM-SHA256',
        'ECDHE-RSA-AES128-GCM-SHA256',
        'TLS_AES_256_GCM_SHA384',
        'TLS_AES_128_GCM_SHA256'
    }

    def __init__(self, project_root: str):
        self.project_root = Path(project_root)
        
    def scan_dependencies(self) -> Dict[str, str]:
        """Scan requirements files for crypto-related dependencies."""
        deps = {}
        req_files = list(self.project_root.glob("*requirements*.txt"))
        
        for req_file in req_files:
            with open(req_file) as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith('#'):
                        pkg_name = line.split('==')[0].lower()
                        if any(mod in pkg_name for mod in self.APPROVED_MODULES):
                            deps[line] = "Approved module"
                        elif 'crypto' in pkg_name or 'ssl' in pkg_name or 'tls' in pkg_name:
                            deps[line] = "Needs review"
        return deps

    def check_openssl_usage(self) -> List[Tuple[str, int, str]]:
        """Specifically check OpenSSL configurations and usage."""
        findings = []
        
        for py_file in self.project_root.rglob("*.py"):
            with open(py_file) as f:
                for i, line in enumerate(f, 1):
                    # Look for OpenSSL configurations
                    if 'ssl' in line.lower():
                        if 'PROTOCOL_TLSv1' in line or 'PROTOCOL_TLSv1_1' in line:
                            findings.append((str(py_file), i, f"Deprecated TLS version: {line.strip()}"))
                        if 'verify=False' in line or 'verify_mode=CERT_NONE' in line:
                            findings.append((str(py_file), i, f"SSL verification disabled: {line.strip()}"))
        return findings

    def check_random_generation(self) -> List[Tuple[str, int, str]]:
        """Check for proper random number generation usage."""
        findings = []
        
        for py_file in self.project_root.rglob("*.py"):
            with open(py_file) as f:
                content = f.read()
                # Look for random number generation
                if '/dev/urandom' in content:
                    line_no = content.count('\n', 0, content.find('/dev/urandom')) + 1
                    findings.append((str(py_file), line_no, 
                                  "Using /dev/urandom instead of preferred /dev/random"))
                # Check for Python's random module which isn't cryptographically secure
                if 'import random' in content and 'random.' in content:
                    line_no = content.count('\n', 0, content.find('import random')) + 1
                    findings.append((str(py_file), line_no, 
                                  "Using Python's random module - not cryptographically secure"))
        return findings

    def check_algorithm_compliance(self) -> List[Tuple[str, int, str, str]]:
        """Check for usage of non-FIPS compliant algorithms."""
        findings = []
        
        for py_file in self.project_root.rglob("*.py"):
            with open(py_file) as f:
                content = f.read()
                for algo in self.NON_COMPLIANT_ALGORITHMS:
                    for match in re.finditer(r'\b' + algo + r'\b', content):
                        line_no = content.count('\n', 0, match.start()) + 1
                        line = content.split('\n')[line_no - 1].strip()
                        findings.append((str(py_file), line_no, line, algo))
        return findings

    def check_hash_algorithms(self) -> List[Tuple[str, int, str]]:
        """Check for proper hash algorithm usage."""
        findings = []
        
        for py_file in self.project_root.rglob("*.py"):
            with open(py_file) as f:
                for i, line in enumerate(f, 1):
                    # Check for SHA-1 usage in digital signatures
                    if 'sha1' in line.lower() and any(word in line.lower() 
                                                    for word in ['sign', 'signature', 'verify']):
                        findings.append((str(py_file), i, 
                                      f"SHA-1 used for digital signature: {line.strip()}"))
                    # Check for minimum SHA-2 requirements
                    if 'sha224' in line.lower():
                        findings.append((str(py_file), i, 
                                      f"SHA-224 below recommended minimum of SHA-256: {line.strip()}"))
        return findings

    def check_key_sizes(self) -> List[Tuple[str, int, str]]:
        """Check for compliant key sizes."""
        findings = []
        
        for py_file in self.project_root.rglob("*.py"):
            with open(py_file) as f:
                content = f.read()
                # Check RSA key sizes
                rsa_matches = re.finditer(r'RSA.*?(\d+)', content)
                for match in rsa_matches:
                    key_size = int(match.group(1))
                    if key_size < 2048:
                        line_no = content.count('\n', 0, match.start()) + 1
                        findings.append((str(py_file), line_no, 
                                      f"RSA key size {key_size} below minimum 2048 bits"))
                
                # Check ECDSA key sizes
                ecdsa_matches = re.finditer(r'ECDSA.*?(\d+)', content)
                for match in ecdsa_matches:
                    key_size = int(match.group(1))
                    if key_size < 224:
                        line_no = content.count('\n', 0, match.start()) + 1
                        findings.append((str(py_file), line_no, 
                                      f"ECDSA key size {key_size} below minimum 224 bits"))
        return findings

    def run_audit(self) -> Dict:
        """Run full FIPS compliance audit."""
        return {
            'dependencies': self.scan_dependencies(),
            'openssl_usage': self.check_openssl_usage(),
            'random_generation': self.check_random_generation(),
            'algorithm_compliance': self.check_algorithm_compliance(),
            'hash_algorithms': self.check_hash_algorithms(),
            'key_sizes': self.check_key_sizes()
        }

def generate_report(audit_results: Dict) -> str:
    """Generate a formatted report from audit results."""
    report = []
    report.append("GitLab FIPS Compliance Audit Report\n" + "="*50 + "\n")
    
    report.append("\n1. Cryptographic Dependencies")
    report.append("-"*30)
    for dep, status in audit_results['dependencies'].items():
        report.append(f"- {dep}: {status}")
    
    report.append("\n2. OpenSSL Configuration Issues")
    report.append("-"*30)
    for file, line_no, issue in audit_results['openssl_usage']:
        report.append(f"- {file}:{line_no} - {issue}")
    
    report.append("\n3. Random Number Generation")
    report.append("-"*30)
    for file, line_no, issue in audit_results['random_generation']:
        report.append(f"- {file}:{line_no} - {issue}")
    
    report.append("\n4. Non-FIPS Compliant Algorithms")
    report.append("-"*30)
    for file, line_no, line, algo in audit_results['algorithm_compliance']:
        report.append(f"- {algo} found in {file}:{line_no}")
        report.append(f"  {line}")
    
    report.append("\n5. Hash Algorithm Issues")
    report.append("-"*30)
    for file, line_no, issue in audit_results['hash_algorithms']:
        report.append(f"- {file}:{line_no} - {issue}")
    
    report.append("\n6. Key Size Issues")
    report.append("-"*30)
    for file, line_no, issue in audit_results['key_sizes']:
        report.append(f"- {file}:{line_no} - {issue}")
    
    return "\n".join(report)

if __name__ == "__main__":
    auditor = GitLabFIPSAuditor("./")  # Adjust path as needed
    results = auditor.run_audit()
    report = generate_report(results)
    print(report)

Definition of Done

  • Create audit script for the AI Gateway
Edited by Susie Bitters