#!/bin/bash # Security check for potential unencrypted file writes to disk # This script helps prevent accidentally bypassing encryption at rest set -e echo "Checking for potential unencrypted file writes to disk..." echo "=========================================" # Patterns that might indicate writing sensitive data to disk # Note: Using basic grep patterns without lookaheads SUSPICIOUS_PATTERNS=( # Python patterns "\.write\(" "\.save\(" "\.dump\(" "open\(.*['\"]w['\"].*\)" "open\(.*['\"]wb['\"].*\)" "with.*open\(.*['\"]w['\"]" "with.*open\(.*['\"]wb['\"]" "\.to_csv\(" "\.to_json\(" "\.to_excel\(" "\.to_pickle\(" "tempfile\.NamedTemporaryFile.*delete=False" "Path.*\.write_text\(" "Path.*\.write_bytes\(" "shutil\.copy" "shutil\.move" "\.export_to_file\(" "\.save_to_file\(" "\.write_pdf\(" "\.savefig\(" # JavaScript patterns "fs\.writeFile" "fs\.writeFileSync" "fs\.createWriteStream" "fs\.appendFile" ) # Directories to exclude from checks EXCLUDE_DIRS=( "tests" "test" "__pycache__" ".git" "node_modules" ".venv" "venv" "migrations" "static" "vendor" "dist" "build" ".next" "coverage" "examples" "scripts" ".github" "cookiecutter-docker" ) # Files to exclude EXCLUDE_FILES=( "*_test.py" "test_*.py" "*.test.js" "*.spec.js" "*.test.ts" "*.spec.ts" "setup.py" "webpack.config.js" "**/migrations/*.py" "*.min.js" "*.bundle.js" "*-min.js" "*.min.css" ) # Safe keywords that indicate encrypted or safe operations # These patterns indicate that file writes have been security-verified SAFE_KEYWORDS=( "write_file_verified" "write_json_verified" ) # Known safe usage patterns (logs, configs, etc.) SAFE_USAGE_PATTERNS=( "security/file_write_verifier.py" "import tempfile" "tempfile\.mkdtemp" "tmp_path" "tmp_file" ) # Build exclude arguments for grep EXCLUDE_ARGS="" for dir in "${EXCLUDE_DIRS[@]}"; do EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude-dir=$dir" done for file in "${EXCLUDE_FILES[@]}"; do EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$file" done # Track if we found any issues FOUND_ISSUES=0 ALL_MATCHES="" echo "Scanning codebase for suspicious patterns..." # Search only in src/ directory to avoid .venv and other non-source directories SEARCH_PATHS="src/" # Single pass to collect all matches for pattern in "${SUSPICIOUS_PATTERNS[@]}"; do # Use grep with binary files excluded and max line length to avoid issues with minified files # shellcheck disable=SC2086 # Word splitting is intentional for EXCLUDE_ARGS matches=$(grep -rn -I $EXCLUDE_ARGS -- "$pattern" $SEARCH_PATHS --include="*.py" --include="*.js" --include="*.ts" 2>/dev/null | head -1000 || true) if [ -n "$matches" ]; then ALL_MATCHES="$ALL_MATCHES$matches\n" fi done # Also check for specific problematic patterns in one pass # shellcheck disable=SC2086 # Word splitting is intentional for EXCLUDE_ARGS temp_matches=$(grep -rn -I $EXCLUDE_ARGS -E "tmp_path|tempfile|/tmp/" $SEARCH_PATHS --include="*.py" 2>/dev/null | head -500 || true) if [ -n "$temp_matches" ]; then ALL_MATCHES="$ALL_MATCHES$temp_matches\n" fi # shellcheck disable=SC2086 # Word splitting is intentional for EXCLUDE_ARGS db_matches=$(grep -rn -I $EXCLUDE_ARGS -E "report_content.*open|report_content.*write|markdown_content.*open|markdown_content.*write" $SEARCH_PATHS --include="*.py" 2>/dev/null | head -500 || true) if [ -n "$db_matches" ]; then ALL_MATCHES="$ALL_MATCHES$db_matches\n" fi # shellcheck disable=SC2086 # Word splitting is intentional for EXCLUDE_ARGS export_matches=$(grep -rn -I $EXCLUDE_ARGS -E "export.*Path|export.*path\.open|export.*\.write" $SEARCH_PATHS --include="*.py" 2>/dev/null | head -500 || true) if [ -n "$export_matches" ]; then ALL_MATCHES="$ALL_MATCHES$export_matches\n" fi # Now filter all matches at once if [ -n "$ALL_MATCHES" ]; then echo "Filtering results for false positives..." # Remove duplicates and sort (use tr to handle potential null bytes) ALL_MATCHES=$(echo -e "$ALL_MATCHES" | tr -d '\0' | sort -u) filtered_matches="" while IFS= read -r line; do [ -z "$line" ] && continue # Check if line contains safe keywords skip_line=0 for safe_pattern in "${SAFE_KEYWORDS[@]}"; do if echo "$line" | grep -qE -- "$safe_pattern"; then skip_line=1 break fi done # Check if line contains safe usage patterns if [ "$skip_line" -eq 0 ]; then for usage_pattern in "${SAFE_USAGE_PATTERNS[@]}"; do if echo "$line" | grep -qE -- "$usage_pattern"; then skip_line=1 break fi done fi # Additional filters for test/mock files that might not be caught by path exclusion if [ "$skip_line" -eq 0 ]; then if echo "$line" | grep -qE "test|mock|stub" && ! echo "$line" | grep -q "#"; then skip_line=1 fi fi # Allowlist of files that legitimately write to disk without encryption. # These must NOT touch user data or secrets — only: # - web/app_factory.py — Flask/framework config writes # - document_loaders/bytes_loader.py — in-memory → tmp for parsers # - journal_quality/downloader.py — public OpenAlex/DOAJ/predatory/ # JabRef/ROR snapshots downloaded to the user data dir (bibliographic # metadata only — journal names, ISSNs, h-indices; no PII/secrets) # - journal_quality/data_sources/*.py — same family, per-source adapters # that write the intermediate JSON manifests under the user data dir # If you add an entry here, document WHY the file's writes are safe # (public data, not user-specific, not encrypted at rest by design). if [ "$skip_line" -eq 0 ]; then if echo "$line" | grep -qE "web/app_factory\.py|document_loaders/bytes_loader\.py|journal_quality/downloader\.py|journal_quality/data_sources/.+\.py"; then skip_line=1 fi fi # Filter safe temp files with proper cleanup if [ "$skip_line" -eq 0 ]; then if echo "$line" | grep -q "database/encrypted_db.py"; then skip_line=1 fi fi if [ "$skip_line" -eq 0 ]; then filtered_matches="$filtered_matches$line\n" FOUND_ISSUES=1 fi done <<< "$ALL_MATCHES" if [ -n "$filtered_matches" ] && [ "$FOUND_ISSUES" -eq 1 ]; then echo "⚠️ Found potential unencrypted file writes:" echo "=========================================" echo -e "$filtered_matches" fi fi echo "=========================================" if [ $FOUND_ISSUES -eq 1 ]; then echo "❌ Security check failed: Found potential unencrypted file writes" echo "" echo "Please review the above findings and ensure:" echo "1. Sensitive data is not written to disk unencrypted" echo "2. Temporary files are properly cleaned up" echo "3. Use in-memory operations where possible" echo "4. If file writes are necessary, ensure they're encrypted or add '# Safe: ' comment" echo "" echo "For exports, use the in-memory pattern like in export_report_to_memory()" exit 1 else echo "✅ Security check passed: No suspicious unencrypted file writes detected" fi