From 50c16acbf949f5aa4037cc7b86b7cf68189c1e67 Mon Sep 17 00:00:00 2001 From: James Berthoty Date: Tue, 1 Apr 2025 17:58:44 -0400 Subject: [PATCH] add insecure-ai --- .github/workflows/publish-insecure.yml | 4 + insecure-ai/Dockerfile | 26 ++ insecure-ai/README.md | 52 ++++ insecure-ai/app.py | 306 ++++++++++++++++++++++ insecure-ai/requirements.txt | 8 + insecure-ai/templates/index.html | 223 ++++++++++++++++ insecure-ai/templates/view.html | 81 ++++++ insecure-chart/templates/insecure-ai.yaml | 93 +++++++ insecure-chart/values.yaml | 19 +- 9 files changed, 811 insertions(+), 1 deletion(-) create mode 100644 insecure-ai/Dockerfile create mode 100644 insecure-ai/README.md create mode 100644 insecure-ai/app.py create mode 100644 insecure-ai/requirements.txt create mode 100644 insecure-ai/templates/index.html create mode 100644 insecure-ai/templates/view.html create mode 100644 insecure-chart/templates/insecure-ai.yaml 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/') +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/') +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 @@ + + + + + + Code Security Analyzer + + + + + + + + + + + + +
+

Code Security Analyzer

+ +
+

Analyze Your Code

+ +
+ + +
+ +
+ + +
+ + +
+ + + + +
+ + + + \ 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 @@ + + + + + + Analysis Results + + + + + + + + + +
+ {{ content | safe }} +
+ + + + \ 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