Repository: latiotech/insecure-kubernetes-deployments
Pull request #91: add insecure-ai
Author: login: confusedcrib
Created: 2025-04-01T21:59:03Z | Merged: 2025-04-01T21:59:13Z



Diff:
diff --git a/.github/workflows/publish-insecure.yml b/.github/workflows/publish-insecure.yml
index d706cf6..40e32a9 100644
--- a/.github/workflows/publish-insecure.yml
+++ b/.github/workflows/publish-insecure.yml
@@ -35,6 +35,10 @@ jobs:
             image: confusedcrib/insecure-api
             context: ./insecure-api
             dockerfile: ./insecure-api/Dockerfile
+          - name: insecure-ai
+            image: confusedcrib/insecure-ai
+            context: ./insecure-ai
+            dockerfile: ./insecure-ai/Dockerfile
 
     steps:
       - name: Check out the repo
diff --git a/insecure-ai/Dockerfile b/insecure-ai/Dockerfile
new file mode 100644
index 0000000..b164ee0
--- /dev/null
+++ b/insecure-ai/Dockerfile
@@ -0,0 +1,26 @@
+FROM python:3.12-slim
+
+WORKDIR /app
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y \
+    build-essential \
+    graphviz \
+    && rm -rf /var/lib/apt/lists/*
+
+# Copy requirements first to leverage Docker cache
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt gunicorn
+
+# Copy the rest of the application
+COPY . .
+
+# Expose the port the app runs on
+EXPOSE 5000
+
+# Set environment variables
+ENV FLASK_APP=app.py
+ENV FLASK_ENV=production
+
+# Run the application with gunicorn
+CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--timeout", "120", "--config", "python:app", "app:app"] 
\ No newline at end of file
diff --git a/insecure-ai/README.md b/insecure-ai/README.md
new file mode 100644
index 0000000..8cf25da
--- /dev/null
+++ b/insecure-ai/README.md
@@ -0,0 +1,52 @@
+# Code Security Analyzer
+
+A Flask web application that allows users to analyze their code for security issues using the `latio` package.
+
+## Features
+
+- Paste code directly into the web interface
+- Upload code as a ZIP file
+- Real-time code analysis using `latio`
+- Modern, responsive UI with syntax highlighting
+
+## Prerequisites
+
+- Python 3.7 or higher
+- pip (Python package manager)
+- `latio` package installed globally
+
+## Installation
+
+1. Clone this repository or download the files
+2. Create a virtual environment (recommended):
+   ```bash
+   python -m venv venv
+   source venv/bin/activate  # On Windows: venv\Scripts\activate
+   ```
+3. Install the required packages:
+   ```bash
+   pip install -r requirements.txt
+   ```
+
+## Usage
+
+1. Start the Flask application:
+   ```bash
+   python app.py
+   ```
+2. Open your web browser and navigate to `http://localhost:5000`
+3. Either paste your code into the text editor or upload a ZIP file containing your code
+4. Click "Analyze Code" to run the security analysis
+5. View the results in the results section below
+
+## Security Considerations
+
+- The application uses temporary directories for file processing
+- File uploads are limited to 16MB
+- Only ZIP files are accepted for upload
+- All uploaded files are processed in isolated temporary directories
+- The application runs in debug mode for development purposes
+
+## License
+
+MIT License 
\ No newline at end of file
diff --git a/insecure-ai/app.py b/insecure-ai/app.py
new file mode 100644
index 0000000..4e4efce
--- /dev/null
+++ b/insecure-ai/app.py
@@ -0,0 +1,306 @@
+import os
+import tempfile
+import zipfile
+from flask import Flask, request, render_template, jsonify, url_for
+from werkzeug.utils import secure_filename
+import shutil
+import threading
+import time
+import logging
+import sys
+import asyncio
+from latio.core import full_agent_scan, partial_agent_scan
+import markdown
+import traceback
+import gunicorn.app.base
+
+# Set up logging before anything else
+def setup_logging():
+    try:
+        # Create logs directory if it doesn't exist
+        os.makedirs('/app/logs', exist_ok=True)
+        
+        # Configure root logger
+        root_logger = logging.getLogger()
+        root_logger.setLevel(logging.DEBUG)
+        
+        # Remove any existing handlers
+        for handler in root_logger.handlers[:]:
+            root_logger.removeHandler(handler)
+        
+        # Create formatters
+        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+        
+        # Console handler
+        console_handler = logging.StreamHandler(sys.stdout)
+        console_handler.setFormatter(formatter)
+        root_logger.addHandler(console_handler)
+        
+        # File handler
+        file_handler = logging.FileHandler('/app/logs/app.log')
+        file_handler.setFormatter(formatter)
+        root_logger.addHandler(file_handler)
+        
+        # Set up specific logger for our app
+        logger = logging.getLogger(__name__)
+        logger.setLevel(logging.DEBUG)
+        
+        logger.info("Logging setup completed successfully")
+        return logger
+    except Exception as e:
+        print(f"Error setting up logging: {str(e)}", file=sys.stderr)
+        raise
+
+# Initialize logging
+logger = setup_logging()
+
+# Add a custom exception handler for unhandled exceptions
+def handle_exception(exc_type, exc_value, exc_traceback):
+    if issubclass(exc_type, KeyboardInterrupt):
+        sys.__excepthook__(exc_type, exc_value, exc_traceback)
+    else:
+        logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
+sys.excepthook = handle_exception
+
+app = Flask(__name__)
+app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB max file size
+app.config['UPLOAD_FOLDER'] = tempfile.gettempdir()
+app.config['APPLICATION_ROOT'] = '/ai'
+
+# Configure url_for to use the correct prefix
+def url_for_external(*args, **kwargs):
+    url = url_for(*args, **kwargs)
+    if app.config['APPLICATION_ROOT']:
+        url = app.config['APPLICATION_ROOT'] + url
+    return url
+
+app.jinja_env.globals['url_for'] = url_for_external
+
+# Gunicorn configuration
+class GunicornConfig:
+    def __init__(self):
+        self.bind = "0.0.0.0:5000"
+        self.workers = 4
+        self.timeout = 120
+        self.script_name = '/ai'
+
+# Create gunicorn config object
+gunicorn_config = GunicornConfig()
+
+# Add error handler for Flask
+@app.errorhandler(Exception)
+def handle_error(error):
+    error_msg = f"Flask error: {str(error)}\n{traceback.format_exc()}"
+    logger.error(error_msg)
+    return jsonify({'error': error_msg}), 500
+
+# Add request logging middleware
+@app.before_request
+def log_request_info():
+    logger.info('Request received: %s %s', request.method, request.url)
+    logger.debug('Headers: %s', dict(request.headers))
+    logger.debug('Form data: %s', dict(request.form))
+    logger.debug('Files: %s', dict(request.files))
+    logger.debug('Body: %s', request.get_data())
+
+@app.after_request
+def log_response_info(response):
+    logger.debug('Response status: %s', response.status)
+    logger.debug('Response headers: %s', dict(response.headers))
+    logger.debug('Response body: %s', response.get_data())
+    return response
+
+# Add error logging for unhandled exceptions in threads
+def thread_exception_handler(args):
+    logger.error("Unhandled exception in thread: %s", args.exc_value, exc_info=(args.exc_type, args.exc_value, args.exc_traceback))
+threading.excepthook = thread_exception_handler
+
+ALLOWED_EXTENSIONS = {'zip'}
+
+# Store analysis results
+analysis_results = {}
+analysis_status = {}
+
+def allowed_file(filename):
+    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
+
+async def analyze_directory(directory, analysis_id):
+    try:
+        analysis_status[analysis_id] = "running"
+        logger.info(f"Starting analysis in directory: {directory}")
+        
+        # Check if directory exists and is accessible
+        if not os.path.exists(directory):
+            error_msg = f"Directory {directory} does not exist"
+            logger.error(error_msg)
+            raise Exception(error_msg)
+        
+        # Log directory contents
+        try:
+            dir_contents = os.listdir(directory)
+            logger.info(f"Directory contents: {dir_contents}")
+        except Exception as e:
+            logger.warning(f"Could not list directory contents: {e}")
+        
+        # Run the analysis using full_agent_scan
+        try:
+            logger.info("Starting full_agent_scan...")
+            result = await full_agent_scan(directory, model='gpt-4o')
+            logger.info("full_agent_scan completed successfully")
+        except Exception as e:
+            error_msg = f"Error during full_agent_scan: {str(e)}\n{traceback.format_exc()}"
+            logger.error(error_msg)
+            raise Exception(error_msg)
+        
+        # Convert the result to a markdown string
+        if isinstance(result, list):
+            # Format each item as a markdown section
+            result_str = "\n\n".join(f"## {item['title']}\n\n{item['description']}" for item in result)
+        else:
+            result_str = str(result)
+        
+        logger.info(f"Analysis completed successfully")
+        analysis_results[analysis_id] = result_str
+        analysis_status[analysis_id] = "completed"
+    except Exception as e:
+        error_msg = f"Unexpected error: {str(e)}\n{traceback.format_exc()}"
+        logger.error(error_msg)
+        analysis_results[analysis_id] = error_msg
+        analysis_status[analysis_id] = "error"
+    finally:
+        # Clean up the temporary directory
+        try:
+            if os.path.exists(directory):
+                shutil.rmtree(directory)
+                logger.info(f"Cleaned up temporary directory: {directory}")
+        except Exception as e:
+            logger.warning(f"Failed to clean up temporary directory: {e}")
+
+def run_async_analysis(directory, analysis_id):
+    try:
+        logger.info(f"Starting async analysis for ID: {analysis_id}")
+        asyncio.run(analyze_directory(directory, analysis_id))
+    except Exception as e:
+        error_msg = f"Error in async analysis thread: {str(e)}\n{traceback.format_exc()}"
+        logger.error(error_msg)
+        analysis_results[analysis_id] = error_msg
+        analysis_status[analysis_id] = "error"
+
+@app.route('/')
+def index():
+    logger.info("Serving index page")
+    return render_template('index.html', url_for=url_for_external)
+
+@app.route('/analyze', methods=['POST'])
+def analyze():
+    analysis_id = str(time.time())
+    analysis_status[analysis_id] = "starting"
+    
+    try:
+        logger.info(f"Received analyze request with ID: {analysis_id}")
+        logger.debug(f"Request form data: {request.form}")
+        logger.debug(f"Request files: {request.files}")
+        
+        if 'code' in request.form:
+            # Create a temporary directory for the code
+            temp_dir = tempfile.mkdtemp()
+            logger.info(f"Created temporary directory: {temp_dir}")
+            try:
+                # Write the code to a file
+                code_file = os.path.join(temp_dir, 'code.py')
+                with open(code_file, 'w') as f:
+                    f.write(request.form['code'])
+                logger.info(f"Wrote code to {code_file}")
+                
+                # Start analysis in a separate thread
+                thread = threading.Thread(target=run_async_analysis, args=(temp_dir, analysis_id))
+                thread.start()
+                logger.info(f"Started analysis thread for ID: {analysis_id}")
+                
+                return jsonify({'analysis_id': analysis_id})
+            except Exception as e:
+                error_msg = f"Error processing code: {str(e)}\n{traceback.format_exc()}"
+                logger.error(error_msg)
+                return jsonify({'error': error_msg}), 500
+        
+        elif 'file' in request.files:
+            file = request.files['file']
+            if file.filename == '':
+                return jsonify({'error': 'No file selected'}), 400
+            
+            if file and allowed_file(file.filename):
+                # Create a temporary directory for the uploaded files
+                temp_dir = tempfile.mkdtemp()
+                logger.info(f"Created temporary directory: {temp_dir}")
+                try:
+                    # Save the uploaded file
+                    filename = secure_filename(file.filename)
+                    filepath = os.path.join(temp_dir, filename)
+                    file.save(filepath)
+                    logger.info(f"Saved uploaded file to: {filepath}")
+                    
+                    # Extract the zip file
+                    with zipfile.ZipFile(filepath, 'r') as zip_ref:
+                        zip_ref.extractall(temp_dir)
+                    logger.info("Extracted zip file contents")
+                    
+                    # Start analysis in a separate thread
+                    thread = threading.Thread(target=run_async_analysis, args=(temp_dir, analysis_id))
+                    thread.start()
+                    logger.info(f"Started analysis thread for ID: {analysis_id}")
+                    
+                    return jsonify({'analysis_id': analysis_id})
+                except Exception as e:
+                    error_msg = f"Error processing file: {str(e)}\n{traceback.format_exc()}"
+                    logger.error(error_msg)
+                    return jsonify({'error': error_msg}), 500
+            
+            return jsonify({'error': 'Invalid file type'}), 400
+        
+        return jsonify({'error': 'No code or file provided'}), 400
+    except Exception as e:
+        error_msg = f"Unexpected error in analyze endpoint: {str(e)}\n{traceback.format_exc()}"
+        logger.error(error_msg)
+        return jsonify({'error': error_msg}), 500
+
+@app.route('/status/<analysis_id>')
+def get_status(analysis_id):
+    logger.debug(f"Status check for analysis ID: {analysis_id}")
+    if analysis_id not in analysis_status:
+        logger.warning(f"Analysis ID not found: {analysis_id}")
+        return jsonify({'error': 'Analysis ID not found'}), 404
+    
+    status = analysis_status[analysis_id]
+    result = analysis_results.get(analysis_id) if status in ['completed', 'error'] else None
+    
+    logger.debug(f"Status for {analysis_id}: {status}")
+    return jsonify({
+        'status': status,
+        'result': result
+    })
+
+@app.route('/view/<analysis_id>')
+def view_result(analysis_id):
+    logger.debug(f"View request for analysis ID: {analysis_id}")
+    if analysis_id not in analysis_status:
+        logger.warning(f"Analysis ID not found: {analysis_id}")
+        return "Analysis not found", 404
+    
+    status = analysis_status[analysis_id]
+    if status not in ['completed', 'error']:
+        logger.info(f"Analysis {analysis_id} is still running")
+        return "Analysis is still running", 400
+    
+    result = analysis_results.get(analysis_id)
+    if not result:
+        logger.warning(f"No results available for analysis ID: {analysis_id}")
+        return "No results available", 404
+    
+    # Convert markdown to HTML
+    html_content = markdown.markdown(result, extensions=['fenced_code', 'tables'])
+    
+    return render_template('view.html', content=html_content)
+
+if __name__ == '__main__':
+    logger.info("Starting Flask application")
+    app.run(host='0.0.0.0', port=5000, debug=True) 
\ No newline at end of file
diff --git a/insecure-ai/requirements.txt b/insecure-ai/requirements.txt
new file mode 100644
index 0000000..2fb709b
--- /dev/null
+++ b/insecure-ai/requirements.txt
@@ -0,0 +1,8 @@
+flask==3.0.2
+latio==1.2.5
+python-magic==0.4.27
+Werkzeug==3.0.1 
+markdown==3.5.2
+graphviz==0.20.1
+gunicorn==23.0.0
+openai>=1.0.0
\ No newline at end of file
diff --git a/insecure-ai/templates/index.html b/insecure-ai/templates/index.html
new file mode 100644
index 0000000..07d5fcb
--- /dev/null
+++ b/insecure-ai/templates/index.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Code Security Analyzer</title>
+    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
+    <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.2/lib/codemirror.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.2/mode/python/python.js"></script>
+    <link href="https://cdn.jsdelivr.net/npm/codemirror@5.65.2/lib/codemirror.css" rel="stylesheet">
+    <!-- Add marked.js for markdown rendering -->
+    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
+    <!-- Add highlight.js for code syntax highlighting -->
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
+    <style>
+        .loading-spinner {
+            border: 4px solid #f3f3f3;
+            border-top: 4px solid #3498db;
+            border-radius: 50%;
+            width: 40px;
+            height: 40px;
+            animation: spin 1s linear infinite;
+            margin: 20px auto;
+        }
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+        .loading-text {
+            color: #666;
+            text-align: center;
+            margin-top: 10px;
+        }
+        /* Markdown styles */
+        .markdown-content {
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+            line-height: 1.6;
+            color: #24292e;
+        }
+        .markdown-content h1 { font-size: 2em; margin-bottom: 0.5em; }
+        .markdown-content h2 { font-size: 1.5em; margin-bottom: 0.5em; }
+        .markdown-content h3 { font-size: 1.25em; margin-bottom: 0.5em; }
+        .markdown-content p { margin-bottom: 1em; }
+        .markdown-content code {
+            background-color: #f6f8fa;
+            padding: 0.2em 0.4em;
+            border-radius: 3px;
+            font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
+        }
+        .markdown-content pre {
+            background-color: #f6f8fa;
+            padding: 1em;
+            border-radius: 6px;
+            overflow-x: auto;
+        }
+        .markdown-content pre code {
+            background-color: transparent;
+            padding: 0;
+        }
+        .markdown-content ul, .markdown-content ol {
+            margin-bottom: 1em;
+            padding-left: 1.5em;
+        }
+        .markdown-content li {
+            margin-bottom: 0.5em;
+        }
+        .markdown-content blockquote {
+            border-left: 4px solid #dfe2e5;
+            padding-left: 1em;
+            margin-left: 1em;
+            color: #6a737d;
+        }
+    </style>
+</head>
+<body class="bg-gray-100 min-h-screen">
+    <div class="container mx-auto px-4 py-8">
+        <h1 class="text-4xl font-bold text-center mb-8 text-gray-800">Code Security Analyzer</h1>
+        
+        <div class="bg-white rounded-lg shadow-lg p-6 mb-6">
+            <h2 class="text-2xl font-semibold mb-4 text-gray-700">Analyze Your Code</h2>
+            
+            <div class="mb-6">
+                <label class="block text-gray-700 text-sm font-bold mb-2" for="code">
+                    Paste your code here:
+                </label>
+                <textarea id="code" class="w-full h-64 p-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
+            </div>
+            
+            <div class="mb-6">
+                <label class="block text-gray-700 text-sm font-bold mb-2">
+                    Or upload a ZIP file:
+                </label>
+                <input type="file" id="file" accept=".zip" class="w-full p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
+            </div>
+            
+            <button onclick="analyzeCode()" id="analyze-btn" class="w-full bg-blue-500 text-white font-bold py-2 px-4 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
+                Analyze Code
+            </button>
+        </div>
+        
+        <div id="loading" class="hidden">
+            <div class="loading-spinner"></div>
+            <div class="loading-text">Analyzing your code... This may take a few minutes.</div>
+        </div>
+        
+        <div id="result" class="bg-white rounded-lg shadow-lg p-6 hidden">
+            <div class="flex justify-between items-center mb-4">
+                <h2 class="text-2xl font-semibold text-gray-700">Analysis Results</h2>
+                <button onclick="openInNewTab()" class="bg-green-500 text-white font-bold py-2 px-4 rounded-lg hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50">
+                    View in New Tab
+                </button>
+            </div>
+            <div id="result-content" class="markdown-content"></div>
+        </div>
+    </div>
+
+    <script>
+        // Initialize CodeMirror
+        var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
+            mode: "python",
+            lineNumbers: true,
+            theme: "default",
+            lineWrapping: true
+        });
+
+        let statusCheckInterval;
+        let currentAnalysisId = null;
+
+        // Configure marked.js
+        marked.setOptions({
+            highlight: function(code, lang) {
+                if (lang && hljs.getLanguage(lang)) {
+                    return hljs.highlight(code, { language: lang }).value;
+                }
+                return hljs.highlightAuto(code).value;
+            },
+            breaks: true
+        });
+
+        function checkStatus(analysisId) {
+            fetch(`{{ url_for('get_status', analysis_id='') }}${analysisId}`)
+                .then(response => response.json())
+                .then(data => {
+                    if (data.status === 'completed' || data.status === 'error') {
+                        clearInterval(statusCheckInterval);
+                        document.getElementById('loading').classList.add('hidden');
+                        document.getElementById('analyze-btn').disabled = false;
+                        
+                        const resultDiv = document.getElementById('result');
+                        const resultContent = document.getElementById('result-content');
+                        
+                        if (data.status === 'error') {
+                            resultContent.innerHTML = `<div class="text-red-600">Error: ${data.result}</div>`;
+                        } else {
+                            resultContent.innerHTML = marked.parse(data.result);
+                            // Apply syntax highlighting to code blocks
+                            resultContent.querySelectorAll('pre code').forEach((block) => {
+                                hljs.highlightBlock(block);
+                            });
+                        }
+                        
+                        resultDiv.classList.remove('hidden');
+                        currentAnalysisId = analysisId;
+                    }
+                })
+                .catch(error => {
+                    console.error('Error:', error);
+                    clearInterval(statusCheckInterval);
+                    document.getElementById('loading').classList.add('hidden');
+                    document.getElementById('analyze-btn').disabled = false;
+                    alert('An error occurred while checking the analysis status');
+                });
+        }
+
+        function openInNewTab() {
+            if (currentAnalysisId) {
+                window.open(`{{ url_for('view_result', analysis_id='') }}${currentAnalysisId}`, '_blank');
+            }
+        }
+
+        function analyzeCode() {
+            const code = editor.getValue();
+            const fileInput = document.getElementById('file');
+            const formData = new FormData();
+            
+            if (code.trim()) {
+                formData.append('code', code);
+            } else if (fileInput.files.length > 0) {
+                formData.append('file', fileInput.files[0]);
+            } else {
+                alert('Please provide either code or a file to analyze');
+                return;
+            }
+
+            // Show loading state
+            document.getElementById('loading').classList.remove('hidden');
+            document.getElementById('analyze-btn').disabled = true;
+            document.getElementById('result').classList.add('hidden');
+
+            fetch('{{ url_for("analyze") }}', {
+                method: 'POST',
+                body: formData
+            })
+            .then(response => response.json())
+            .then(data => {
+                if (data.error) {
+                    throw new Error(data.error);
+                }
+                
+                // Start polling for status
+                statusCheckInterval = setInterval(() => checkStatus(data.analysis_id), 2000);
+            })
+            .catch(error => {
+                console.error('Error:', error);
+                document.getElementById('loading').classList.add('hidden');
+                document.getElementById('analyze-btn').disabled = false;
+                alert('An error occurred while starting the analysis');
+            });
+        }
+    </script>
+</body>
+</html> 
\ No newline at end of file
diff --git a/insecure-ai/templates/view.html b/insecure-ai/templates/view.html
new file mode 100644
index 0000000..9810a7b
--- /dev/null
+++ b/insecure-ai/templates/view.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Analysis Results</title>
+    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
+    <!-- Add marked.js for markdown rendering -->
+    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
+    <!-- Add highlight.js for code syntax highlighting -->
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github.min.css">
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
+    <style>
+        /* Markdown styles */
+        .markdown-content {
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+            line-height: 1.6;
+            color: #24292e;
+            max-width: 800px;
+            margin: 0 auto;
+            padding: 2rem;
+        }
+        .markdown-content h1 { font-size: 2em; margin-bottom: 0.5em; }
+        .markdown-content h2 { font-size: 1.5em; margin-bottom: 0.5em; }
+        .markdown-content h3 { font-size: 1.25em; margin-bottom: 0.5em; }
+        .markdown-content p { margin-bottom: 1em; }
+        .markdown-content code {
+            background-color: #f6f8fa;
+            padding: 0.2em 0.4em;
+            border-radius: 3px;
+            font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
+        }
+        .markdown-content pre {
+            background-color: #f6f8fa;
+            padding: 1em;
+            border-radius: 6px;
+            overflow-x: auto;
+        }
+        .markdown-content pre code {
+            background-color: transparent;
+            padding: 0;
+        }
+        .markdown-content ul, .markdown-content ol {
+            margin-bottom: 1em;
+            padding-left: 1.5em;
+        }
+        .markdown-content li {
+            margin-bottom: 0.5em;
+        }
+        .markdown-content blockquote {
+            border-left: 4px solid #dfe2e5;
+            padding-left: 1em;
+            margin-left: 1em;
+            color: #6a737d;
+        }
+    </style>
+</head>
+<body class="bg-gray-50">
+    <div class="markdown-content">
+        {{ content | safe }}
+    </div>
+
+    <script>
+        // Configure marked.js
+        marked.setOptions({
+            highlight: function(code, lang) {
+                if (lang && hljs.getLanguage(lang)) {
+                    return hljs.highlight(code, { language: lang }).value;
+                }
+                return hljs.highlightAuto(code).value;
+            },
+            breaks: true
+        });
+
+        // Apply syntax highlighting to code blocks
+        document.querySelectorAll('pre code').forEach((block) => {
+            hljs.highlightBlock(block);
+        });
+    </script>
+</body>
+</html> 
\ No newline at end of file
diff --git a/insecure-chart/templates/insecure-ai.yaml b/insecure-chart/templates/insecure-ai.yaml
new file mode 100644
index 0000000..3395ba3
--- /dev/null
+++ b/insecure-chart/templates/insecure-ai.yaml
@@ -0,0 +1,93 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: {{ .Values.insecureAi.namespace | default "insecure-ai" }}
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: insecure-ai-secrets
+  namespace: {{ .Values.insecureAi.namespace | default "insecure-ai" }}
+  labels:
+    app.kubernetes.io/managed-by: {{ .Release.Service }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+type: Opaque
+data:
+  openai-api-key: {{ .Values.insecureAi.env.openaiApiKey | b64enc }}
+  gemini-api-key: {{ .Values.insecureAi.env.geminiApiKey | b64enc }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ .Values.insecureAi.service.name | default "insecure-ai" }}
+  namespace: {{ .Values.insecureAi.namespace | default "insecure-ai" }}
+spec:
+  selector:
+    app: {{ .Values.insecureAi.appName | default "insecure-ai" }}
+  ports:
+    - port: {{ .Values.insecureAi.service.port | default 80 }}
+      targetPort: {{ .Values.insecureAi.service.targetPort | default 5000 }}
+  type: ClusterIP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ .Values.insecureAi.appName | default "insecure-ai" }}
+  namespace: {{ .Values.insecureAi.namespace | default "insecure-ai" }}
+  labels:
+    app.kubernetes.io/managed-by: {{ .Release.Service }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+spec:
+  replicas: {{ .Values.insecureAi.replicas | default 1 }}
+  selector:
+    matchLabels:
+      app: {{ .Values.insecureAi.appName | default "insecure-ai" }}
+  template:
+    metadata:
+      labels:
+        app: {{ .Values.insecureAi.appName | default "insecure-ai" }}
+    spec:
+      containers:
+      - name: {{ .Values.insecureAi.containerName | default "insecure-ai" }}
+        image: {{ .Values.insecureAi.image.repository }}:{{ .Values.insecureAi.image.tag | default "latest" }}
+        ports:
+        - containerPort: {{ .Values.insecureAi.containerPort | default 5000 }}
+        env:
+        - name: OPENAI_API_KEY
+          valueFrom:
+            secretKeyRef:
+              name: insecure-ai-secrets
+              key: openai-api-key
+              optional: true
+        - name: GEMINI_API_KEY
+          valueFrom:
+            secretKeyRef:
+              name: insecure-ai-secrets
+              key: gemini-api-key
+              optional: true
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: insecure-ai-ingress
+  namespace: {{ .Values.insecureAi.namespace | default "insecure-ai" }}
+  labels:
+    app.kubernetes.io/managed-by: {{ .Release.Service }}
+    app.kubernetes.io/instance: {{ .Release.Name }}
+  annotations:
+    nginx.ingress.kubernetes.io/rewrite-target: /$1
+    nginx.ingress.kubernetes.io/use-regex: "true"
+    meta.helm.sh/release-name: {{ .Release.Name }}
+    meta.helm.sh/release-namespace: {{ .Release.Namespace }}
+spec:
+  ingressClassName: nginx
+  rules:
+  - http:
+      paths:
+      - path: /ai/?(.*)
+        pathType: ImplementationSpecific
+        backend:
+          service:
+            name: {{ .Values.insecureAi.service.name | default "insecure-ai" }}
+            port:
+              number: {{ .Values.insecureAi.service.port | default 80 }} 
\ No newline at end of file
diff --git a/insecure-chart/values.yaml b/insecure-chart/values.yaml
index 944f79e..0409cae 100644
--- a/insecure-chart/values.yaml
+++ b/insecure-chart/values.yaml
@@ -66,4 +66,21 @@ insecureApi:
   image:
     repository: confusedcrib/insecure-api
     tag: latest
-  containerPort: 8000
\ No newline at end of file
+  containerPort: 8000
+
+insecureAi:
+  namespace: insecure-ai
+  appName: insecure-ai
+  containerName: insecure-ai
+  service:
+    name: insecure-ai
+    port: 80
+    targetPort: 5000
+  replicas: 1
+  image:
+    repository: confusedcrib/insecure-ai
+    tag: latest
+  containerPort: 5000
+  env:
+    openaiApiKey: ""
+    geminiApiKey: ""
\ No newline at end of file


Reviews:
user: login: jit-ci[bot]
body: ❌ **Jit has detected 3 important findings in this PR that you should review.**
_The findings are detailed below as separate comments._
_It’s highly recommended that you fix these security issues before merge._
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-01T22:01:02Z

Review comments:
diff_hunk: @@ -0,0 +1,8 @@
+flask==3.0.2
+latio==1.2.5
+python-magic==0.4.27
+Werkzeug==3.0.1 
path: insecure-ai/requirements.txt
user: login: jit-ci[bot]
body: **Security control:** Software Component Analysis Python

**Werkzeug Debugger Vulnerable To Remote Execution When Interacting With Attacker Controlled Domain**

The debugger in affected versions of Werkzeug can allow an attacker to execute code on a developer&#39;s machine under some circumstances. This requires the attacker to get the developer to interact with a domain and subdomain they control, and enter the debugger PIN, but if they are successful it allows access to the debugger even if it is only running on localhost. This also requires the attacker to guess a URL in the developer&#39;s application that will trigger the debugger.

**Is fix available?** Yes, this issue was fixed in version 3.0.3

**Severity:** HIGH

[Learn more about this issue](https://nvd.nist.gov/vuln/detail/CVE-2024-34069)

---
<details>
<summary><b>Jit Bot commands and options (e.g., ignore issue)</b></summary>
<br />

You can trigger Jit actions by commenting on this PR review:
- `#jit_ignore_fp` Ignore and mark this specific single instance of finding as “False Positive”
- `#jit_ignore_accept` Ignore and mark this specific single instance of finding as “Accept Risk”
- `#jit_ignore_type_in_file` Ignore any finding of type "Werkzeug debugger vulnerable to remote execution when interacting with attacker controlled domain" in insecure-ai/requirements.txt; future occurrences will also be ignored.
- `#jit_undo_ignore` Undo ignore command
</details>

<!-- f438c98d-98bf-4237-b6af-7f3bb2a5fb2a -->
created_at: 2025-04-01T22:01:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,26 @@
+FROM python:3.12-slim
+
+WORKDIR /app
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y \
+    build-essential \
+    graphviz \
+    && rm -rf /var/lib/apt/lists/*
path: insecure-ai/Dockerfile
user: login: jit-ci[bot]
body: **Security control:** Docker Scan

**'Apt-Get' Missing '--No-Install-Recommends'**

'apt-get' install should use '--no-install-recommends' to minimize image size.

**Severity:** HIGH

[Learn more about this issue](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)

---
<details>
<summary><b>Jit Bot commands and options (e.g., ignore issue)</b></summary>
<br />

You can trigger Jit actions by commenting on this PR review:
- `#jit_ignore_fp` Ignore and mark this specific single instance of finding as “False Positive”
- `#jit_ignore_accept` Ignore and mark this specific single instance of finding as “Accept Risk”
- `#jit_ignore_type_in_file` Ignore any finding of type "'apt-get' missing '--no-install-recommends'" in insecure-ai/Dockerfile; future occurrences will also be ignored.
- `#jit_undo_ignore` Undo ignore command
</details>

<!-- af92ccff-60f1-47ae-a4e9-1c27a403ed55 -->
created_at: 2025-04-01T22:01:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,26 @@
+FROM python:3.12-slim
path: insecure-ai/Dockerfile
user: login: jit-ci[bot]
body: **Security control:** Docker Scan

**Image User Should Not Be 'Root'**

Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.

**Severity:** HIGH

[Learn more about this issue](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)

---

**Fix suggestion:** 

**This fix suggestion was generated by Jit.** Please note that the suggestion might not always fit every use case. It is highly recommended that you check and review it before merging.

**Suggestion guidelines**

* First of all, check if your container is running as a root user. In most of the cases, you can do it by running a command like this: `docker run <image> whoami`. If it returns `root`, then you should consider using a non-root user, by following one of the next steps:
    * If a non-root user already exists in your container, consider using it.
    * If not, you can create a new user by adding a `USER` command to the Dockerfile, with a non-root user as argument, for example: `USER <non-root-user-name>`.


```suggestion
FROM python:3.12-slim

RUN addgroup --system <group>
RUN adduser --system <user> --ingroup <group>
USER <user>:<group>

```

---
<details>
<summary><b>Jit Bot commands and options (e.g., ignore issue)</b></summary>
<br />

You can trigger Jit actions by commenting on this PR review:
- `#jit_ignore_fp` Ignore and mark this specific single instance of finding as “False Positive”
- `#jit_ignore_accept` Ignore and mark this specific single instance of finding as “Accept Risk”
- `#jit_ignore_type_in_file` Ignore any finding of type "Image user should not be 'root'" in insecure-ai/Dockerfile; future occurrences will also be ignored.
- `#jit_undo_ignore` Undo ignore command
</details>

<!-- 69aa985f-3165-416c-b3ae-5abf7fbdd5ad -->
created_at: 2025-04-01T22:01:02Z
author_association: NONE

PR comments:
user: login: zeropath-ai[bot]
created_at: 2025-04-01T21:59:28Z
body: We have finished reviewing your PR. We have found no vulnerabilities.

Reply to this PR with `@zeropath-ai` followed by a description of what change you want and we'll auto-submit a change to this PR to implement it.
author_association: NONE
user: login: dryrunsecurity[bot]
created_at: 2025-04-01T22:00:00Z
body: ### DryRun Security Summary

A Flask-based AI code security analysis application with multiple security vulnerabilities spanning network exposure, information disclosure, dependency risks, credential handling, file upload security, development environment configurations, container deployment, and template rendering.

<details>
<summary>Expand for full summary</summary>
<br/>

Summary: A Flask-based AI code security analysis application with multiple components, including web interface, Dockerfile, Kubernetes deployment, and GitHub Actions workflow, revealing several potential security vulnerabilities across different configuration files.

Security Findings:

1. Network Exposure Risks
   - Application bound to 0.0.0.0:5000, potentially exposing service to unintended network access
   - Hardcoded Gunicorn configuration exposes service on all network interfaces

2. Information Disclosure Vulnerabilities
   - Extensive logging of request details could expose sensitive information
   - Detailed error messages might leak implementation details
   - README and templates provide potentially excessive application architecture insights

3. External Dependency Risks
   - Multiple CDN-loaded resources in templates from various sources
   - Potential supply chain attacks or dependency compromise
   - Possible XSS if CDN is compromised

4. Credential and Secret Handling
   - Kubernetes Secrets base64 encoded (not encrypted)
   - Optional credential flag might allow deployment without authentication
   - API key placeholders in values.yaml with potential insecure management

5. File Upload Security Concerns
   - Minimal ZIP file upload validation
   - Potential race conditions with global dictionaries for analysis results
   - Unvalidated file upload in client-side interface

6. Development Environment Exposure
   - Debug mode explicitly enabled for development
   - Unencrypted HTTP protocol recommended for local development

7. Container and Deployment Risks
   - Using `:latest` image tag, introducing unpredictable changes
   - No explicit user creation or root privilege dropping in Dockerfile
   - No resource constraints defined in Kubernetes deployment

8. Template and Rendering Risks
   - Potential server-side template injection
   - Unsafe content rendering with `{{ content | safe }}`
   - No visible CSRF protection mechanisms
</details>

[View PR in the DryRun Dashboard.](https://app.dryrun.security/pull-requests/?org=latiotech)
author_association: NONE