diff --git a/MobSF_AI/LICENSE b/MobSF_AI/LICENSE new file mode 100644 index 0000000000..1e4acd6a78 --- /dev/null +++ b/MobSF_AI/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 AshishSecDev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MobSF_AI/README.md b/MobSF_AI/README.md new file mode 100644 index 0000000000..66b6ce88bb --- /dev/null +++ b/MobSF_AI/README.md @@ -0,0 +1,101 @@ +# MobSF AI Sidecar + +This project adds an AI-powered chat assistant to the [Mobile Security Framework (MobSF)](https://github.com/MobSF/Mobile-Security-Framework-MobSF). It allows security analysts to ask questions about the analysis report generated by MobSF directly within the interface, leveraging OpenAI's model. + +## Features + +- **Seamless Integration**: Injects a specialized chat interface directly into MobSF reports using an Nginx reverse proxy. +- **Context-Aware**: The AI assistant automatically retrieves the current analysis report (static analysis) and uses it as context to answer your questions. +- **Interactive Q&A**: Ask about vulnerabilities, code snippets, remediation steps or general security concepts related to your app. + +## Architecture & How It Works + +The solution is composed of three Docker services working together: + +1. **MobSF (`mobsf`)**: The standard Mobile Security Framework application running on port 8000. +2. **Sidecar (`chat`)**: A Python Flask application that runs on port 5000. + - Serves the chat UI. + - Integrates the user, the MobSF API and OpenAI API. + - Fetches the report JSON from MobSF using the unique scan hash. + - Sends the relevant parts of the report along with the user's query to the OpenAI API. +3. **Gateway (`gateway`)**: An Nginx reverse proxy (listening on port 8080) that coordinates everything: + - Proxies standard traffic to the MobSF container. + - Routes `/chat/` and `/api/chat` requests to the Sidecar container. + +### Data Flow +1. User uploads the Mobile App in MobSF. +2. User opens the view report in MobSF when mobile app analysis completed. +3. User clicks the Chat button. +4. Browser opens a separate chat window. +5. User can integract with this particular app report data. +6. Sidecar fetches the report from MobSF API then sends prompt + report + question to OpenAI and returns answer. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) +- An [OpenAI API Key](https://platform.openai.com/api-keys) + +## Setup & Run + +1. **Clone the repository:** + ```bash + git clone + cd Mobsf_Side_Car + ``` + +2. **Configure Environment:** + Open `docker-compose-mobsfai.yml` and check the environment variables. + + You generally need to provide your OpenAI API Key. You can set it directly in the file (not recommended for committed code) or export it in your shell: + ```bash + export MOBSF_OPENAI_API_KEY="sk-..." + ``` + + *Note: The `MOBSF_API_KEY` is currently hardcoded to `1234567890` in the docker-compose file for both the MobSF instance and the Sidecar to communicate. If you change it in one place, ensure you update it in both services. You can the API in MobSF GUI as well.* + +3. **Build and Start:** + Run the following command to build the chat container and start the stack: + ```bash + docker-compose -f docker-compose-mobsfai.yml up --build + ``` + OR + ```bash + docker-compose -f docker-compose-mobsfai.yml up -d --build + ``` + OR + ```bash + docker-compose -f docker-compose-mobsfai.yml down -v + ``` + + +4. **Access the Application:** + Open your browser and navigate to: + **http://localhost:8080** + + *(Note: Do not use port 8000, as that bypasses the Nginx gateway and the chat button will not appear.)* + +## Usage + +1. Go to **http://localhost:8080**. +2. Upload an Android (APK) or iOS (IPA) application for analysis. +3. Wait for the scan to complete. +4. Once the "Static Analysis" report loads, look for a **floating Chat button (💬)** in the bottom-right corner. +5. Click the button to open the AI assistant. +6. Ask questions like: + - "Summarize the critical vulnerabilities." + - "How do I fix the high-severity issues found in the manifest?" + - "Explain the 'Application is debuggable' finding." + +## Troubleshooting + +- **Chat button not appearing?** + - Ensure you are accessing via `http://localhost:8080` and not port 8000. + - Check the browser console for any JavaScript errors. + - Ensure the URL contains `/StaticAnalyzer/` or matches the pattern expected by the injected script. +- **AI Error / OpenAI Issues?** + - Check the logs of the chat container: `docker logs mobsf_chat`. + - Verify your `OPENAI_API_KEY` is valid and has access to the model. +- **MobSF Communication Errors?** + - Ensure the `MOBSF_API_KEY` matches in both the `mobsf` and `chat` service definitions in `docker-compose-mobsfai.yml`. + +## AshishSecDev diff --git a/MobSF_AI/docker-compose-mobsfai.yml b/MobSF_AI/docker-compose-mobsfai.yml new file mode 100644 index 0000000000..1337d17dbd --- /dev/null +++ b/MobSF_AI/docker-compose-mobsfai.yml @@ -0,0 +1,41 @@ +version: '3' + +services: + mobsf: + image: opensecurity/mobile-security-framework-mobsf:latest + container_name: mobsf_core + expose: + - "8000" + environment: + - MOBSF_API_KEY=1234567890 + networks: + - mobsf_net + + chat: + build: ./sidecar + container_name: mobsf_chat + expose: + - "5000" + environment: + - MOBSF_URL=http://mobsf:8000 + - MOBSF_API_KEY=1234567890 + - OPENAI_API_KEY=${MOBSF_OPENAI_API_KEY} + networks: + - mobsf_net + + gateway: + image: nginx:alpine + container_name: mobsf_gateway + ports: + - "8080:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + networks: + - mobsf_net + depends_on: + - mobsf + - chat + +networks: + mobsf_net: + driver: bridge diff --git a/MobSF_AI/nginx/nginx.conf b/MobSF_AI/nginx/nginx.conf new file mode 100644 index 0000000000..9974724307 --- /dev/null +++ b/MobSF_AI/nginx/nginx.conf @@ -0,0 +1,55 @@ +events {} + +http { + server { + listen 80; + client_max_body_size 1024M; + proxy_read_timeout 3600s; + proxy_connect_timeout 3600s; + proxy_send_timeout 3600s; + send_timeout 3600s; + + location / { + proxy_pass http://mobsf:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + + # Ensure gzipped responses from upstream are decompressed so sub_filter works + proxy_set_header Accept-Encoding ""; + + # 1. Capture scan_hash from URL if present + # Pattern: /StaticAnalyzer/hash/ or /DynamicAnalyzer/hash/ + # We use map or regex in location? + # Creating a variable $scan_hash is tricky in pure nginx without Lua. + # But we can use a simple JS injection that parses URL client-side. + + # Inject Chat Button + sub_filter '' ''; + sub_filter_once on; + } + + # Route /chat/ to sidecar + location /chat/ { + proxy_pass http://chat:5000; + } + + # Route /api/chat to sidecar + location /api/chat { + proxy_pass http://chat:5000; + } + } +} diff --git a/MobSF_AI/sidecar/Dockerfile b/MobSF_AI/sidecar/Dockerfile new file mode 100644 index 0000000000..bccc175b83 --- /dev/null +++ b/MobSF_AI/sidecar/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.9-slim + +WORKDIR /app + +RUN pip install flask requests openai + +COPY . . + +CMD ["python", "app.py"] diff --git a/MobSF_AI/sidecar/app.py b/MobSF_AI/sidecar/app.py new file mode 100644 index 0000000000..be13f35b3e --- /dev/null +++ b/MobSF_AI/sidecar/app.py @@ -0,0 +1,78 @@ +from flask import Flask, render_template, request, jsonify +import requests +import os +from openai import OpenAI +import json + +app = Flask(__name__) + +MOBSF_URL = os.environ.get('MOBSF_URL', 'http://mobsf:8000') +MOBSF_API_KEY = os.environ.get('MOBSF_API_KEY', '') +OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '') + +client = OpenAI(api_key=OPENAI_API_KEY) + + +@app.route('/chat//') +def chat_ui(scan_hash): + # We can pass scan_hash to the template + return render_template('chat.html', scan_hash=scan_hash) + + +@app.route('/api/chat', methods=['POST']) +def chat_api(): + data = request.json + scan_hash = data.get('hash') + message = data.get('message') + history = data.get('history', []) + + if not scan_hash or not message: + return jsonify({'error': 'Missing hash or message'}), 400 + + headers = {'Authorization': MOBSF_API_KEY} + try: + # Fetch report from MobSF + resp = requests.post(f"{MOBSF_URL}/api/v1/report_json", + data={'hash': scan_hash}, headers=headers) + if resp.status_code != 200: + return jsonify({'error': f"MobSF Error: {resp.status_code} {resp.text}"}), 500 + + report_data = resp.json() + except Exception as e: + return jsonify({'error': str(e)}), 500 + + system_instruction = f""" + You are a security analyst assistant for MobSF. + Report Context: + {json.dumps(report_data, default=str)[:50000]} + + Answer the user's question based on this report. + """ + + messages = [{"role": "system", "content": system_instruction}] + for turn in history: + # Gemini format adaptation if needed, but sidecar uses new format + role = turn.get('role', 'user') + if role == 'model': + role = 'assistant' + + content = turn.get('content', '') + if not content and 'parts' in turn: + content = turn['parts'][0] + messages.append({"role": role, "content": str(content)}) + + messages.append({"role": "user", "content": message}) + + try: + completion = client.chat.completions.create( + model="gpt-4o", + messages=messages + ) + reply = completion.choices[0].message.content + return jsonify({'response': reply}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +if __name__ == '__main__': + app.run() diff --git a/MobSF_AI/sidecar/static/img/mobsf_icon.png b/MobSF_AI/sidecar/static/img/mobsf_icon.png new file mode 100644 index 0000000000..afeabb586b Binary files /dev/null and b/MobSF_AI/sidecar/static/img/mobsf_icon.png differ diff --git a/MobSF_AI/sidecar/templates/chat.html b/MobSF_AI/sidecar/templates/chat.html new file mode 100644 index 0000000000..5dfe431bc5 --- /dev/null +++ b/MobSF_AI/sidecar/templates/chat.html @@ -0,0 +1,186 @@ + + + + + + + MobSF Chat + + + + + + + + + + +
+
+
+
+
+
+

Security Assistant

+
+
+ +
+
+
+
+ +
+
+ +
+
+

Chat Analysis for {{ scan_hash }}

+
+ +
+ +
+ +
+
+ MobSF Bot + Now +
+ + Bot + +
+ Hello! I analyzed the report for {{ scan_hash }}. Ask me anything about + vulnerabilities, code issues, or security recommendations. +
+ +
+ +
+ +
+ + + +
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/mobsf/StaticAnalyzer/tests_ai.py b/mobsf/StaticAnalyzer/tests_ai.py new file mode 100644 index 0000000000..8a7b162d32 --- /dev/null +++ b/mobsf/StaticAnalyzer/tests_ai.py @@ -0,0 +1,58 @@ +from django.test import TestCase +from django.urls import reverse +from unittest.mock import patch +import json + + +class MobSFAITests(TestCase): + + @patch("mobsf.StaticAnalyzer.ai.service.call_openai") + def test_api_chat_success(self, mock_ai): + """Test successful chat interaction via AI endpoint.""" + # Mock the AI service to return a structured response + mock_ai.return_value = {"response": "This is a mocked bot response."} + + payload = { + "hash": "test_hash_abc", + "message": "What vulnerabilities do you see?", + "history": [] + } + + # NOTE: Ensure 'api_chat' is the correct URL name in your urls.py + # e.g., re_path(r'^api/v1/chat$', views.api_chat, name='api_chat') + try: + url = reverse('ai_endpoint_name') + except Exception: + # Fallback direct path if reverse fails (e.g. url pattern not yet named) + url = "/api/v1/chat" + + response = self.client.post( + url, + data=json.dumps(payload), + content_type="application/json", + ) + + self.assertEqual(response.status_code, 200) + self.assertIn("response", response.json()) + self.assertEqual(response.json()["response"], "This is a mocked bot response.") + + def test_api_chat_missing_params(self): + """Test AI endpoint with missing message parameter.""" + payload = { + "scan_id": "123", + # Missing 'message' + } + + try: + url = reverse('ai_endpoint_name') + except Exception: + url = "/api/v1/chat" + + response = self.client.post( + url, + data=json.dumps(payload), + content_type="application/json", + ) + + self.assertEqual(response.status_code, 400) + self.assertIn("error", response.json())