├── .github └── images │ ├── logo.webp │ ├── logo2.webp │ └── webserver.png ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── oasis.py ├── oasis ├── __init__.py ├── analyze.py ├── cache.py ├── config.py ├── embedding.py ├── enums.py ├── images │ ├── oasis-logo.jpg │ └── oasis-logo.webp ├── oasis.py ├── ollama_manager.py ├── report.py ├── static │ ├── css │ │ └── dashboard.css │ ├── img │ │ ├── favicon │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-96x96.png │ │ │ ├── favicon.ico │ │ │ ├── favicon.svg │ │ │ ├── site.webmanifest │ │ │ ├── web-app-manifest-192x192.png │ │ │ └── web-app-manifest-512x512.png │ │ └── oasis-logo.jpg │ ├── js │ │ ├── dashboard-app.js │ │ └── dashboard │ │ │ ├── api.js │ │ │ ├── filters.js │ │ │ ├── interactions.js │ │ │ ├── modal.js │ │ │ ├── utils.js │ │ │ └── views.js │ └── templates │ │ └── dashboard_card.html ├── templates │ ├── base.html │ ├── dashboard.html │ ├── footer.html │ ├── header.html │ ├── login.html │ ├── report_styles.css │ └── report_template.html ├── tools.py └── web.py ├── pyproject.toml └── test_files ├── Vulnerable.cs ├── Vulnerable.java ├── vulnerable.php ├── vulnerable.py └── vulnerable.sh /.github/images/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/.github/images/logo.webp -------------------------------------------------------------------------------- /.github/images/logo2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/.github/images/logo2.webp -------------------------------------------------------------------------------- /.github/images/webserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/.github/images/webserver.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | venv/ 25 | ENV/ 26 | env/ 27 | .env 28 | 29 | # IDE 30 | .idea/ 31 | .vscode/ 32 | *.swp 33 | *.swo 34 | 35 | # OASIS specific 36 | .oasis_cache/ 37 | security_reports/ 38 | models.md 39 | *.pdf 40 | 41 | # Logs 42 | *.log -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 🚀 [0.4.0] - 2025-03-21 2 | 3 | ### ✨ Added 4 | - 🔐 Added web interface authentication with password protection 5 | - 🌐 Added option to expose web interface on different network interfaces 6 | - ⚙️ Added command line arguments for web interface configuration: 7 | - `--web-expose`: Control web interface exposure (local/all, default: local) 8 | - `--web-password`: Set a password for web interface access 9 | - `--web-port`: Configure the web server port (default: 5000) 10 | - 🖥️ Added login page with consistent design to match the application's style 11 | - 🔍 Added two-phase scanning architecture for optimized analysis workflow 12 | - 🤖 Added support for separate scan and analysis models with `--scan-model` parameter 13 | - 🧠 Added adaptive multi-level analysis mode that adjusts depth based on risk assessment 14 | - 🔄 Added interactive model selection with separate prompts for scan and deep analysis models 15 | - 💡 Added intelligent model filtering to recommend smaller parameter-count models (4-7B) for initial scanning phase 16 | - 📊 Added enhanced progress tracking with nested progress bars for each analysis phase 17 | - 📏 Added model parameter detection for intelligent model recommendations 18 | - 🎮 Added new command-line options: 19 | - `--scan-model` / `-sm`: Specify lightweight model for initial scanning 20 | - `--adaptive` / `-ad`: Use adaptive multi-level analysis instead of standard 21 | - `--clear-cache-scan` / `-ccs`: Clear scan cache before starting 22 | 23 | ### 🐛 Fixed 24 | - 🔄 Fixed model selection and switching to use the correct model for each phase 25 | - 📈 Fixed progress bar rendering for nested analysis operations 26 | - 💾 Fixed cache handling for different analysis modes 27 | - 🔄 Fixed inconsistencies between displayed and actual models used 28 | - 🧮 Fixed memory usage issues with large models during scanning 29 | 30 | ### ⚡ Changed 31 | - 🚀 Improved analysis workflow to reduce model switching and optimize GPU memory usage 32 | - 🎯 Enhanced model selection interface with clearer prompts and recommendations 33 | - 📝 Improved logging with better status updates for each analysis phase 34 | - 🔍 Enhanced vulnerability scanning with optimized two-phase scanning 35 | - 🏗️ Reorganized analysis architecture for better code organization and modularity 36 | - 📊 Updated progress bars to show more detailed progress information 37 | - 💾 Improved caching system to handle both deep and quick scan results 38 | - 📚 Enhanced documentation with new examples and usage patterns 39 | 40 | ## 🚀 [0.3.0] - 2025-03-17 41 | 42 | Complete codebase refactoring and improvements. 43 | 44 | ### ✨ Added 45 | - 🛡️ Added support for new vulnerability types (RCE, SSRF, XXE, Path Traversal, IDOR, CSRF) 46 | - 📋 Added detailed vulnerability descriptions and examples 47 | - 🎨 Added HTML template and CSS styling for better report readability 48 | - 😊 Added better emoji support in logging for better readability 49 | - 🧪 Added more comprehensive test files with vulnerability examples 50 | - 🔗 Added support for custom Ollama URL 51 | 52 | ### ⚡ Changed 53 | - 📁 Improved codebase organization and readability 54 | - 🧩 Improved embedding and analysis process 55 | - 💾 Improved cache management with dedicated .oasis_cache/ directory 56 | - 📝 Enhanced logging system with custom EmojiFormatter 57 | - 📊 Improved report generation with better styling and formatting 58 | - 🏗️ Refactored package structure for better organization 59 | - 📦 Updated dependency management in pyproject.toml 60 | 61 | ### 🐛 Fixed 62 | - 💾 Fixed embeddings cache storage and validation 63 | - 📄 Fixed report rendering with proper page breaks 64 | - 📥 Fixed issue with model installation progress tracking 65 | - 💾 Fixed issue with cache saving during interruption 66 | - 🔍 Fixed issue with model availability check 67 | - 📊 Fixed issue with progress bar updates 68 | - 📝 Fixed issue with log message formatting 69 | 70 | ### 🔬 Technical 71 | - ⚙️ Added configuration constants for better maintainability 72 | - 🧩 Added Jinja2 templating for report generation 73 | - 📝 Implemented normalized heading levels in reports 74 | - 🛠️ Improved error handling and logging 75 | 76 | ### 📚 Documentation 77 | - 📝 Enhanced code documentation with proper docstrings 78 | - 📖 Added more comprehensive README with examples and usage instructions 79 | - 💻 Improved command line interface documentation 80 | - 📋 Added more detailed changelog 81 | - 🗂️ Updated project structure documentation 82 | - 💬 Added more comprehensive code comments 83 | - 📖 Improved code readability and maintainability 84 | 85 | ## 🚀 [0.2.0] - 2025-01-29 86 | 87 | ### ✨ Added 88 | - 📝 Enhanced logging system with contextual emojis 89 | - 😊 Automatic emoji detection in log messages 90 | - 🔍 Debug logging for file operations 91 | - 📚 Proper docstrings and documentation 92 | - 📊 Progress bar for model installation 93 | - 🔍 Model availability check before analysis 94 | - 🤖 Interactive model installation 95 | 96 | ### ⚡ Changed 97 | - 📋 Moved keyword lists to global constants 98 | - ⌨️ Improved KeyboardInterrupt handling 99 | - 💾 Enhanced cache saving during interruption 100 | - 📝 Improved error messages clarity 101 | - 📄 Better handling of newlines in logs 102 | - 🔄 Refactored logging formatter 103 | - 📊 Enhanced progress bar updates 104 | - 🏗️ Improved code organization 105 | 106 | ### 🐛 Fixed 107 | - 🧪 Cache structure validation 108 | - 📥 Model installation progress tracking 109 | - 😊 Emoji spacing consistency 110 | - 📝 Newline handling in log messages 111 | - 💾 Cache saving during interruption 112 | - 🛠️ Error handling robustness 113 | - 📊 Progress bar updates 114 | 115 | ### 🔬 Technical 116 | - 😊 Added emoji detection system 117 | - 🛠️ Enhanced error handling architecture 118 | - 🔍 Improved cache validation system 119 | - 🧹 Added cleanup utilities 120 | - 🚪 Better exit code handling 121 | - 📊 More robust progress tracking 122 | - 📁 Clearer code organization 123 | - 🔬 Enhanced debugging capabilities 124 | 125 | ### 📚 Documentation 126 | - 📝 Added detailed docstrings 127 | - 💬 Improved code comments 128 | - 📋 Enhanced error messages 129 | - 📝 Better logging feedback 130 | - 📊 Clearer progress indicators 131 | 132 | ## 🚀 [0.1.0] - 2024-01-15 133 | 134 | ### ✨ Added 135 | - 🎉 Initial release 136 | - 🔒 Basic code security analysis with Ollama models 137 | - 📄 Support for multiple file types and extensions 138 | - 💾 Embedding cache system for performance 139 | - 📑 PDF and HTML report generation 140 | - 💻 Command line interface with basic options 141 | - 🎨 Logo and ASCII art display 142 | - 📝 Basic logging system 143 | 144 | ### 🌟 Features 145 | - 🤖 Multi-model analysis support 146 | - 🔍 File extension filtering 147 | - 🛡️ Vulnerability type selection 148 | - 📊 Progress bars for analysis tracking 149 | - 📋 Executive summary generation 150 | - 🛠️ Basic error handling 151 | 152 | ### 🔬 Technical 153 | - 🔗 Integration with Ollama API 154 | - 📄 WeasyPrint for PDF generation 155 | - 📝 Markdown report formatting 156 | - 💾 Basic cache management 157 | - 🏗️ Initial project structure -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors Guide 2 | 3 | Contributions are what make the open-source community such an amazing place to learn, inspire and create. Every contribution you make is **greatly appreciated**. Your contributions can be as simple as fixing the indentation or UI, or as complex as adding new modules and features. 4 | 5 | ## For who is this guide? 6 | 7 | This guide is meant for users who want to contribute to the codebase of Oasis, whether that is the application code or documentation. To keep all processes streamlined and consistent, we're asking you to stick to this guide whenever contributing. 8 | 9 | Even though the guide is made for contributors, it's also strongly recommended that the Oasis team sticks to these guidelines. After all, we're a prime example. 10 | 11 | ## What are the guidelines? 12 | 13 | ### Submitting issues 14 | 15 | You can submit issues related to this project, but you should do it in a way that helps developers to resolve it as quickly as possible. 16 | 17 | For that, you need to add as much valuable information as possible. 18 | 19 | You can have this valuable information by running Oasis in debug mode: 20 | 21 | ```bash 22 | oasis --debug -i my_project 23 | ``` 24 | 25 | **Activating debug mode will give you more information** instead of a generic error without any details. 26 | 27 | Happy issuing ;) 28 | 29 | ### Support 30 | 31 | We are volunteers, working hard on Oasis to add new features. Help is welcome, you can help us out by opening a PR. 32 | 33 | * Add a GitHub Star to the project. 34 | * Tweet about this project, or maybe blogs? 35 | 36 | Any support is greatly appreciated! Thank you! 37 | 38 | ### First-time Open Source contributors 39 | 40 | Please note that Oasis is beginner friendly. If you have never done open-source before, we encourage you to do so. **We will be happy and proud of your first PR ever.** 41 | 42 | You can start by resolving any open issues. 43 | 44 | ### Branching strategy 45 | 46 | As for our branching strategy, we're using [Release Branching](https://www.split.io/blog/the-basics-of-release-branching/). 47 | 48 | In short, a release branch is created from the main branch when the team is ready to roll out a new version. Only necessary changes like bug fixes and final touch-ups are made. Once finalized, it merges with the main branch for deployment. Urgent fixes after the release are handled using hotfix branches, which merge back into both the release and main branches. We do not use a `develop` branch as that adds complexity. 49 | 50 | Some examples of branches are: 51 | 52 | * Features (`feature/*`) 53 | * Fixes (`hotfix/*` or simply `fix/*`) 54 | * Dependency updates (`deps/*`) 55 | * Releases (`release/*`) 56 | 57 | Do mind that these branch names do only not apply when there's already an issue for the pull request. In that case we use the following scheme: `[issue number][issue title]`. This can be done [automatically](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) too. 58 | 59 | This is how it looks like and works. The difference here is that we don't have a develop branch (so the purple dots that are connected with its mainline should not be included). 60 | 61 | drawing 62 | 63 | So in short: 64 | 65 | 1. PR with feature/fix is opened 66 | 1. PR is merged into release branch 67 | 1. When we release a new version, release branch is merged to main 68 | 69 | ### Commit messages 70 | 71 | As for commits, we prefer using [Conventional Commit Messages](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13). When working in any of the branches listed above (if there's an existing issue for it), close it using a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword). For more information regarding Conventional Commit Messages, see as well. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | License 4 | 5 | 6 | Release 7 | 8 | 9 | Python 10 | 11 |

12 | 13 |
14 | 15 | 16 | 17 |
18 | 19 |
20 |

OASIS

21 |
22 |

23 | 🏝️ Ollama Automated Security Intelligence Scanner 24 |

25 | 26 |

27 | OASIS Logo 28 |

29 | 30 |

31 | 🛡️ An AI-powered security auditing tool that leverages Ollama models to detect and analyze potential security vulnerabilities in your code. 32 |

33 | 34 |

35 | Advanced code security analysis through the power of AI 36 |

37 | 38 | ## 🌟 Features 39 | 40 | - 🔍 **Multi-Model Analysis**: Leverage multiple Ollama models for comprehensive security scanning 41 | - 🔄 **Two-Phase Scanning**: Use lightweight models for initial scanning and powerful models for deep analysis 42 | - 🧠 **Adaptive Analysis**: Smart multi-level scanning that adjusts depth based on risk assessment 43 | - 🔄 **Interactive Model Selection**: Guided selection of scan and analysis models with parameter-based filtering 44 | - 💾 **Dual-Layer Caching**: Efficient caching for both embeddings and analysis results to dramatically speed up repeated scans 45 | - 🔧 **Scan Result Caching**: Store and reuse vulnerability analysis results with model-specific caching 46 | - 📊 **Rich Reporting**: Detailed reports in multiple formats (Markdown, PDF, HTML) 47 | - 🔄 **Parallel Processing**: Optimized performance through parallel vulnerability analysis 48 | - 📝 **Executive Summaries**: Clear overview of all detected vulnerabilities 49 | - 🎯 **Customizable Scans**: Support for specific vulnerability types and file extensions 50 | - 📈 **Distribution Analysis**: Advanced audit mode for embedding distribution analysis 51 | - 🔄 **Content Chunking**: Intelligent content splitting for better analysis of large files 52 | - 🤖 **Interactive Model Installation**: Guided installation for required Ollama models 53 | - 🌐 **Web Interface**: Secure, password-protected web dashboard for exploring reports 54 | 55 | ## 🚀 Prerequisites 56 | 57 | - Python 3.9+ 58 | - [Ollama](https://ollama.ai) installed and running 59 | - pipx (for isolated installation) 60 | ```bash 61 | # On macOS 62 | brew install pipx 63 | pipx ensurepath 64 | 65 | # On Ubuntu/Debian 66 | python3 -m pip install --user pipx 67 | python3 -m pipx ensurepath 68 | 69 | # On Windows (with pip) 70 | pip install --user pipx 71 | python -m pipx ensurepath 72 | ``` 73 | 74 | 75 | ## 🛠️ Hardware Requirements 76 | 77 | ### Minimum Requirements 78 | - **CPU**: 4+ cores (Intel i5/AMD Ryzen 5 or better) 79 | - **RAM**: 16 GB minimum, 32 GB recommended 80 | - **Storage**: 100 GB+ free space for models (more for caching large codebases) 81 | - **GPU**: Not required for basic usage (will use CPU but really slow) 82 | 83 | ### Recommended Setup 84 | - **CPU**: 8+ cores (Intel i7/i9 or AMD Ryzen 7/9) 85 | - **RAM**: 32 GB-64 GB for large codebases 86 | - **GPU**: NVIDIA with 8 GB+ VRAM (RTX 3060 or better) 87 | - **Storage**: SSD with 100 GB+ free space 88 | 89 | ### Scaling Guidelines 90 | - **Small Projects** (< 10,000 Lines of Code (LOC)): Minimum requirements sufficient 91 | - **Medium Projects** (10,000-100,000 Lines of Code (LOC)): 8-core CPU, 32 GB+ RAM recommended 92 | - **Large Projects** (> 100,000 Lines of Code (LOC)): High-end CPU, 64 GB+ RAM, dedicated GPU essential 93 | 94 | ### GPU Recommendations by Model Size 95 | - **4-8B parameter models**: 8 GB VRAM minimum 96 | - **12-20B parameter models**: 16 GB VRAM recommended 97 | - **30B+ parameter models**: 24 GB+ VRAM (RTX 3090/4090/A5000 or better) 98 | 99 | ### Network Requirements 100 | - Stable internet connection for model downloads 101 | - Initial model downloads: 3GB-15GB per model 102 | 103 | ### Performance Tips 104 | - Use SSD storage for cache directories 105 | - Prioritize GPU memory over compute performance 106 | - Consider running overnight for large codebases 107 | - For enterprise usage, dedicated server with 128GB+ RAM and A100/H100 GPU recommended 108 | 109 | 110 | ## 📦 Installation 111 | 112 | 1. Clone the repository: 113 | ```bash 114 | git clone https://github.com/psyray/oasis.git 115 | cd oasis 116 | ``` 117 | 118 | 2. Install with pipx: 119 | ```bash 120 | # First time installation 121 | pipx install --editable . 122 | ``` 123 | 124 | ## 🔄 Update 125 | 126 | If new releases are available, you can update the installation with: 127 | ```bash 128 | git pull origin master 129 | pipx upgrade oasis 130 | ``` 131 | **NOTE**: because of the editable installation, you just need to pull the latest changes from the repository to update your global **oasis** command installed with pipx. 132 | So the pipx upgrade is not mandatory, only if needed to bump version in pipx 133 | 134 | Or test a feature branch before official release (could be unstable) 135 | ``` 136 | git fetch --all 137 | git checkout feat/vX.X 138 | ``` 139 | 140 | ## 🗑️ Uninstallation 141 | ```bash 142 | pipx uninstall oasis 143 | ``` 144 | 145 | ## 🔧 Usage 146 | 147 | Basic usage: 148 | ```bash 149 | oasis --input [path_to_analyze] 150 | ``` 151 | 152 | ## 🚀 Quick Test 153 | 154 | To quickly test OASIS with sample files: 155 | ```bash 156 | # Clone and install 157 | git clone https://github.com/psyray/oasis.git 158 | cd oasis 159 | pipx install --editable . 160 | 161 | # Run analysis on test files 162 | oasis --input test_files/ 163 | ``` 164 | 165 | This will analyze the provided test files and generate security reports in the parent directory of the folder to analyze, `security_reports`. 166 | 167 | ## 🔥 Advanced Usage Examples 168 | 169 | Standard two-phase analysis with separate models: 170 | ```bash 171 | # Use a lightweight model for initial scanning and a powerful model for deep analysis 172 | oasis -i [path_to_analyze] -sm gemma3:4b -m gemma3:27b 173 | ``` 174 | 175 | Adaptive multi-level analysis: 176 | ```bash 177 | # Use adaptive analysis mode with custom threshold 178 | oasis -i [path_to_analyze] --adaptive -t 0.6 -m llama3 179 | ``` 180 | 181 | Targeted vulnerability scan with caching control: 182 | ```bash 183 | # Analyze only for SQL Injection and XSS, clear cache, specify models 184 | oasis -i [path_to_analyze] -v sqli,xss --clear-cache-scan -sm gemma3:4b -m gemma3:27b 185 | ``` 186 | 187 | Full production scan: 188 | ```bash 189 | # Comprehensive scan of a large codebase 190 | oasis -i [path_to_analyze] -sm gemma3:4b -m llama3:latest,codellama:lates -t 0.7 --vulns all 191 | ``` 192 | 193 | ## 🎮 Command Line Arguments 194 | 195 | ### Input/Output Options 196 | - `--input` `-i`: Path to file, directory, or .txt file containing newline-separated paths to analyze 197 | - `--output-format` `-of`: Output format [pdf, html, md] (default: all) 198 | - `--extensions` `-x`: Custom file extensions to analyze (e.g., "py,js,java") 199 | 200 | ### Analysis Configuration 201 | - `--analyze-type` `-at`: Analyze type [standard, deep] (default: standard) 202 | - `--embeddings-analyze-type` `-eat`: Analyze code by entire file or by individual functions [file, function] (default: file) 203 | - file: Performs the embedding on the entire file as a single unit, preserving overall context but potentially diluting details. 204 | - function (**EXPERIMENTAL**): Splits the file into individual functions for analysis, allowing for more precise detection of issues within specific code blocks but with less contextual linkage across functions. 205 | 206 | - `--adaptive` `-ad`: Use adaptive multi-level analysis that adjusts depth based on risk assessment 207 | - `--threshold` `-t`: Similarity threshold (default: 0.5) 208 | - `--vulns` `-v`: Vulnerability types to check (comma-separated or 'all') 209 | - `--chunk-size` `-ch`: Maximum size of text chunks for embedding (default: auto-detected) 210 | 211 | ### Model Selection 212 | - `--models` `-m`: Comma-separated list of models to use for deep analysis 213 | - `--scan-model` `-sm`: Model to use for quick scanning (default: same as main model) 214 | - `--embed-model` `-em`: Model to use for embeddings (default: nomic-embed-text:latest) 215 | - `--list-models` `-lm`: List available models and exit 216 | 217 | ### Cache Management 218 | - `--clear-cache-embeddings` `-cce`: Clear embeddings cache before starting 219 | - `--clear-cache-scan` `-ccs`: Clear scan analysis cache for the current analysis type 220 | - `--cache-days` `-cd`: Maximum age in days for both embedding and analysis caches (default: 7) 221 | 222 | ### Web Interface 223 | - `--web` `-w`: Serve reports via a web interface 224 | - `--web-expose` `-we`: Web interface exposure (local: 127.0.0.1, all: 0.0.0.0) (default: local) 225 | - `--web-password` `-wpw`: Web interface password (if not specified, a random password will be generated) 226 | - `--web-port` `-wp`: Web interface port (default: 5000) 227 | 228 | ### Logging and Debug 229 | - `--debug` `-d`: Enable debug output 230 | - `--silent` `-s`: Disable all output messages 231 | 232 | ### Special Modes 233 | - `--audit` `-a`: Run embedding distribution analysis 234 | - `--ollama-url` `-ol`: Ollama URL (default: http://localhost:11434) 235 | - `--version` `-V`: Show OASIS version and exit 236 | 237 | ## 💡 Getting the Most out of OASIS 238 | 239 | ### Model Selection Strategy 240 | 241 | OASIS uses a two-phase scanning approach that leverages different models for optimal results: 242 | 243 | #### Model Selection by Purpose 244 | - **Initial Scanning Models** (4-7B parameters): 245 | - Optimized for speed: `gemma3:4b`, `llama3.2:3b`, `phi3:mini` 246 | - Used for quick pattern matching and identifying potentially suspicious code segments 247 | - Resource-efficient for scanning large codebases 248 | 249 | - **Deep Analysis Models** (>20B parameters): 250 | - Optimized for thorough analysis: `gemma3:27b`, `deepseek-r1:32b`, `qwen2.5-coder:32b`, `mistral-nemo`, `mixtral:instruct` 251 | - Used only for code sections flagged as suspicious in the initial scan 252 | - Provides detailed vulnerability assessment 253 | 254 | - **Specialized Code Models**: 255 | - Code-specific models: `codellama`, `codestral`, `starcoder`, `phind-codellama` 256 | - Best for specific languages and frameworks 257 | - `codellama` for general code, `codestral` for Python/C++, `starcoder/phind-codellama` for web technologies 258 | 259 | #### Example Model Combinations 260 | 261 | ```bash 262 | # For quick analysis of a small project 263 | oasis -i ./src -sm llama3.2:3b -m llama3.2:8b 264 | 265 | # For thorough analysis of web application code (PHP, JavaScript) 266 | oasis -i ./webapp -sm gemma3:4b -m codellama:34b -v xss,sqli,csrf 267 | 268 | # For security audit of Python backend with specialized models 269 | oasis -i ./backend -sm phi3:mini -m deepseek-r1:32b,qwen2.5-coder:32b -v rce,input,data 270 | 271 | # For critical infrastructure security analysis (most thorough) 272 | oasis -i ./critical-service -sm gemma3:7b -m mixtral:instruct -v all --adaptive -t 0.6 273 | ``` 274 | 275 | ### Scanning Workflows: Standard vs Adaptive 276 | 277 | OASIS offers two different analysis approaches, each with distinct advantages: 278 | 279 | #### Standard Two-Phase Workflow 280 | 281 | This workflow uses a sequential approach with two distinct phases: 282 | 283 | 1. **Initial Scanning Phase**: 284 | - Uses a lightweight model specified by `-sm` 285 | - Scans entire codebase to identify potentially suspicious chunks 286 | - Creates a map of suspicious sections for deep analysis 287 | 288 | 2. **Deep Analysis Phase**: 289 | - Uses more powerful model(s) specified by `-m` 290 | - Analyzes only chunks flagged as suspicious in phase 1 291 | - Generates comprehensive analysis reports 292 | 293 | **Best for**: Large codebases with uniform risk profiles, predictable resource planning 294 | 295 | #### Adaptive Multi-Level Workflow 296 | 297 | The adaptive workflow employs a dynamic approach that adjusts analysis depth based on risk assessment: 298 | 299 | 1. **Level 1**: Static pattern-based analysis (fastest) 300 | 2. **Level 2**: Lightweight model scan for initial screening 301 | 3. **Level 3**: Medium-depth context analysis with risk scoring 302 | 4. **Level 4**: Deep analysis only for high-risk chunks 303 | 304 | **Best for**: Critical systems with varied risk profiles, complex codebases requiring nuanced analysis 305 | 306 | #### Comparison Table 307 | 308 | | Aspect | Standard Two-Phase | Adaptive Multi-Level | 309 | |--------|-------------------|----------------------| 310 | | **Speed** | Faster for average cases | Faster for low-risk code, slower overall | 311 | | **Resource Usage** | Predictable, efficient | Variable, optimized for risk | 312 | | **Detection Accuracy** | Good for obvious vulnerabilities | Better for subtle, context-dependent issues | 313 | | **False Positives** | More common | Reduced through context analysis | 314 | | **Resource Allocation** | Fixed per phase | Dynamically adjusted by risk | 315 | | **Command Flag** | Default | Use `--adaptive` `-ad` | 316 | 317 | ### Optimization Tips 318 | 319 | For the best results with OASIS: 320 | 321 | 1. **Caching Strategy**: 322 | - Leverage the dual-layer caching system for repeated scans 323 | - Only clear embedding cache (`-cce`) when changing embedding models or after major code changes 324 | - Clear scan cache (`-ccs`) when upgrading to better models or after fixing vulnerabilities 325 | 326 | 2. **Workflow Optimization**: 327 | - Start with higher thresholds (0.7-0.8) for large codebases to focus on high-probability issues 328 | - Use `--audit` mode to understand vulnerability distribution before full analysis 329 | - Specify relevant vulnerability types (`-v`) and file extensions (`-x`) to target your analysis 330 | 331 | 3. **Resource Management**: 332 | - For large projects, run initial scans during off-hours 333 | - Balance CPU/GPU usage by choosing appropriate model sizes 334 | - Use model combinations that maximize speed and accuracy based on your hardware 335 | 336 | 4. **Report Utilization**: 337 | - View HTML reports for the best interactive experience 338 | - Use the web interface (`--web`) for team collaboration 339 | - Export PDF reports for documentation and sharing 340 | 341 | ## 🛡️ Supported Vulnerability Types 342 | 343 | | Tag | Description | 344 | |-----|-------------| 345 | | `sqli` | SQL Injection | 346 | | `xss` | Cross-Site Scripting | 347 | | `input` | Insufficient Input Validation | 348 | | `data` | Sensitive Data Exposure | 349 | | `session` | Session Management Issues | 350 | | `config` | Security Misconfiguration | 351 | | `logging` | Sensitive Data Logging | 352 | | `crypto` | Insecure Cryptographic Function Usage | 353 | | `rce` | Remote Code Execution | 354 | | `ssrf` | Server-Side Request Forgery | 355 | | `xxe` | XML External Entity | 356 | | `path` | Path Traversal | 357 | | `idor` | Insecure Direct Object Reference | 358 | | `auth` | Authentication Issues | 359 | | `csrf` | Cross-Site Request Forgery | 360 | 361 | ## 📁 Output Structure 362 | 363 | ``` 364 | security_reports/ 365 | ├── [model_name]/ 366 | │ ├── markdown/ 367 | │ │ ├── vulnerability_type.md 368 | │ │ └── executive_summary.md 369 | │ ├── pdf/ 370 | │ │ ├── vulnerability_type.pdf 371 | │ │ └── executive_summary.pdf 372 | │ └── html/ 373 | │ ├── vulnerability_type.html 374 | │ └── executive_summary.html 375 | ``` 376 | 377 | ## 💾 Cache Management 378 | 379 | OASIS implements a sophisticated dual-layer caching system to optimize performance: 380 | 381 | ### Embedding Cache 382 | - Stores vector embeddings of your codebase to avoid recomputing them for repeated analyses 383 | - Default cache duration: 7 days 384 | - Cache location: `.oasis_cache/[embedding_model_name]/` 385 | - Use `--clear-cache-embeddings` (`-cce`) to force regeneration of embeddings 386 | 387 | ### Analysis Cache 388 | - Stores the results of LLM-based vulnerability scanning for each model and analysis mode 389 | - Separate caches for scan (lightweight) and deep analysis results 390 | - Model-specific caching ensures results are tied to the specific model used 391 | - Analysis type-aware (standard vs. adaptive) 392 | - Use `--clear-cache-scan` (`-ccs`) to force fresh vulnerability scanning 393 | 394 | This dual-layer approach dramatically improves performance: 395 | - First-time analysis: Compute embeddings + full scanning 396 | - Repeated analysis (same code): Reuse embeddings + scanning results 397 | - After code changes: Update only changed file embeddings + scan only modified components 398 | 399 | The cache system intelligently handles: 400 | - Different model combinations (scan model + deep model) 401 | - Different analysis types and modes 402 | - Different vulnerability types 403 | - Cache expiration based on configured days 404 | 405 | For the best performance: 406 | - Only clear the embedding cache when changing embedding models or after major code changes 407 | - Clear the scan cache when upgrading to a newer/better model or after fixing vulnerabilities 408 | 409 | ## 📊 Audit Mode 410 | 411 | OASIS offers a specialized Audit Mode that performs an embedding distribution analysis to help you understand your codebase's vulnerability profile before conducting a full scan. 412 | 413 | ```bash 414 | # Run OASIS in audit mode 415 | oasis --input [path_to_analyze] --audit 416 | ``` 417 | 418 | ### What Audit Mode Does 419 | 420 | - **Embedding Analysis**: Generates embeddings for your entire codebase and all vulnerability types 421 | - **Similarity Distribution**: Calculates similarity scores between your code and various vulnerability patterns 422 | - **Threshold Analysis**: Shows the distribution of similarity scores across different thresholds 423 | - **Statistical Overview**: Provides mean, median, and max similarity scores for each vulnerability type 424 | - **Top Matches**: Identifies the files or functions with the highest similarity to each vulnerability type 425 | 426 | ### Benefits of Audit Mode 427 | 428 | - **Pre-Scan Intelligence**: Understand which vulnerability types are most likely to be present in your codebase 429 | - **Threshold Optimization**: Determine the optimal similarity threshold for your specific project 430 | - **Resource Planning**: Identify which vulnerabilities require deeper analysis with more powerful models 431 | - **Faster Insights**: Get a quick overview without running a full security analysis 432 | - **Targeted Scanning**: Use the results to focus your main analysis on the most relevant vulnerability types 433 | 434 | ### Example Workflow 435 | 436 | 1. **Initial Audit**: 437 | ```bash 438 | oasis -i [path_to_analyze] --audit 439 | ``` 440 | 441 | 2. **Targeted Analysis** based on audit results: 442 | ```bash 443 | oasis -i [path_to_analyze] -v sqli,xss,rce -t 0.65 444 | ``` 445 | 446 | The Audit Mode is especially valuable for large codebases where a full scan might be time-consuming, allowing you to make informed decisions about where to focus your security analysis efforts. 447 | 448 | ## 🌐 Web Interface 449 | 450 | OASIS includes a web interface to view and explore security reports: 451 | 452 | OASIS Logo 453 | 454 |
455 | 456 | ```bash 457 | # Start the web interface with default settings (localhost:5000) 458 | oasis --input [path_to_analyze] --web 459 | 460 | # Start with custom port and expose to all network interfaces 461 | oasis --input [path_to_analyze] --web --web-port 8080 --web-expose all 462 | 463 | # Start with a specific password 464 | oasis --input [path_to_analyze] --web --web-password mysecretpassword 465 | ``` 466 | 467 | ### Security Features 468 | 469 | - **Password Protection**: By default, a random password is generated and displayed in the console 470 | - **Network Isolation**: By default, the server only listens on 127.0.0.1 471 | - **Custom Port**: Configurable port to avoid conflicts with other services 472 | 473 | When no password is specified, a secure random password will be generated and displayed in the console output. The web interface provides a dashboard to explore security reports, filter results, and view detailed vulnerability information. 474 | 475 | ## 📝 Changelog 476 | 477 | See [CHANGELOG.md](CHANGELOG.md) for the latest updates and changes. 478 | 479 | ## 🤝 Contributing 480 | 481 | Contributions are welcome! Please feel free to submit a Pull Request. Check out our [Contributing Guidelines](CONTRIBUTING.md) for more details. 482 | 483 | Alternatively, you can also contribute by reporting issues or suggesting features. 484 | 485 | Come and join our [Discord server](https://discord.gg/dW3sFwTtN3) to discuss the project. 486 | 487 | ## 📄 License 488 | 489 | [GPL v3](LICENSE) - feel free to use this project for your security needs. 490 | 491 | ## 🙏 Acknowledgments 492 | 493 | - Built with [Ollama](https://ollama.ai) 494 | - Uses [WeasyPrint](https://weasyprint.org/) for PDF generation 495 | - Uses [Jinja2](https://jinja.palletsprojects.com/) for report templating 496 | - Special thanks to all contributors and the open-source community 497 | 498 | ## 📫 Support 499 | 500 | If you encounter any issues or have questions, come asking help on our [Discord server](https://discord.gg/dW3sFwTtN3) or please file an issue. -------------------------------------------------------------------------------- /oasis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | OASIS Client - Entry point for OASIS scanner 4 | This file imports from the oasis package and serves as an entry point 5 | """ 6 | 7 | import sys 8 | from oasis import main 9 | 10 | if __name__ == "__main__": 11 | sys.exit(main()) -------------------------------------------------------------------------------- /oasis/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | OASIS - Ollama Automated Security Intelligence Scanner 3 | """ 4 | 5 | from .oasis import main 6 | 7 | __version__ = "0.4.0" -------------------------------------------------------------------------------- /oasis/cache.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union, Dict 3 | from datetime import datetime 4 | import hashlib 5 | import pickle 6 | 7 | from .enums import AnalysisMode, AnalysisType 8 | from .tools import create_cache_dir, sanitize_name, logger 9 | 10 | class CacheManager: 11 | """ 12 | Manages caching operations for security analysis results 13 | """ 14 | def __init__(self, input_path: Union[str, Path], llm_model: str, scan_model: str, cache_days: int): 15 | """ 16 | Initialize the cache manager 17 | 18 | Args: 19 | input_path: Path to the input being analyzed 20 | llm_model: Main model name 21 | scan_model: Scanning model name 22 | cache_days: Number of days to keep cache files 23 | """ 24 | self.cache_days = cache_days 25 | 26 | # Create cache directory 27 | self.cache_dir = create_cache_dir(input_path) 28 | 29 | # Create model-specific cache directories 30 | self.model_cache_dir = self.cache_dir / sanitize_name(llm_model) 31 | self.model_cache_dir.mkdir(exist_ok=True) 32 | 33 | # Create scan model cache directory if different from main model 34 | if scan_model != llm_model: 35 | self.scan_model_cache_dir = self.cache_dir / sanitize_name(scan_model) 36 | self.scan_model_cache_dir.mkdir(exist_ok=True) 37 | else: 38 | self.scan_model_cache_dir = self.model_cache_dir 39 | 40 | # Create analysis type subdirectories for both models 41 | self.standard_cache_dir = { 42 | AnalysisMode.DEEP: self.model_cache_dir / AnalysisType.STANDARD.value, 43 | AnalysisMode.SCAN: self.scan_model_cache_dir / AnalysisType.STANDARD.value 44 | } 45 | 46 | self.adaptive_cache_dir = { 47 | AnalysisMode.DEEP: self.model_cache_dir / AnalysisType.ADAPTIVE.value, 48 | AnalysisMode.SCAN: self.scan_model_cache_dir / AnalysisType.ADAPTIVE.value 49 | } 50 | 51 | # Create all required directories 52 | for directory in list(self.standard_cache_dir.values()) + list(self.adaptive_cache_dir.values()): 53 | directory.mkdir(exist_ok=True) 54 | 55 | # Initialize cache dictionaries for current session 56 | self.chunk_cache = { 57 | AnalysisType.STANDARD: {}, 58 | AnalysisType.ADAPTIVE: {} 59 | } 60 | self.scan_chunk_cache = { 61 | AnalysisType.STANDARD: {}, 62 | AnalysisType.ADAPTIVE: {} 63 | } 64 | 65 | # Initialize adaptive cache for storing intermediate results 66 | self.adaptive_analysis_cache = {} 67 | 68 | # Validate cache files and clean expired ones 69 | self.validate_cache_expiration() 70 | 71 | def get_cache_path(self, file_path: str, mode: AnalysisMode, 72 | analysis_type: AnalysisType) -> Path: 73 | """ 74 | Get the path to the cache file for a specific analyzed file 75 | 76 | Args: 77 | file_path: Path to the analyzed file 78 | mode: Analysis mode (scan or deep) 79 | analysis_type: Analysis type (standard or adaptive) 80 | 81 | Returns: 82 | Path object to the cache file 83 | """ 84 | sanitized_file_name = sanitize_name(file_path) 85 | 86 | if analysis_type == AnalysisType.STANDARD: 87 | return self.standard_cache_dir[mode] / f"{sanitized_file_name}.cache" 88 | else: 89 | return self.adaptive_cache_dir[mode] / f"{sanitized_file_name}.cache" 90 | 91 | def get_cache_dict(self, mode: AnalysisMode, analysis_type: AnalysisType) -> Dict: 92 | """ 93 | Get the appropriate cache dictionary based on mode and type 94 | 95 | Args: 96 | mode: Analysis mode (scan or deep) 97 | analysis_type: Analysis type (standard or adaptive) 98 | 99 | Returns: 100 | Cache dictionary 101 | """ 102 | if analysis_type == AnalysisType.ADAPTIVE: 103 | return self.adaptive_analysis_cache 104 | 105 | # Standard analysis cache selection 106 | if mode == AnalysisMode.SCAN: 107 | return self.scan_chunk_cache[analysis_type] 108 | return self.chunk_cache[analysis_type] 109 | 110 | def process_cache(self, action: str, file_path: str, mode: AnalysisMode, 111 | analysis_type: AnalysisType = AnalysisType.STANDARD): 112 | """ 113 | Process cache operations (load or save) 114 | 115 | Args: 116 | action: Action to perform ('load' or 'save') 117 | file_path: Path to the file 118 | mode: Analysis mode (scan or deep) 119 | analysis_type: Analysis type (standard or adaptive) 120 | 121 | Returns: 122 | For 'load' action, returns the loaded cache 123 | For 'save' action, returns None 124 | """ 125 | cache_path = self.get_cache_path(file_path, mode, analysis_type) 126 | cache_dict = self.get_cache_dict(mode, analysis_type) 127 | 128 | if action == 'load': 129 | if file_path in cache_dict: 130 | return cache_dict[file_path] 131 | 132 | if not cache_path.exists(): 133 | cache_dict[file_path] = {} 134 | return {} 135 | 136 | try: 137 | with open(cache_path, 'rb') as f: 138 | cache_dict[file_path] = pickle.load(f) 139 | logger.debug(f"Loaded {mode.value} {analysis_type.value} chunk cache for {file_path}: {len(cache_dict[file_path])} entries") 140 | return cache_dict[file_path] 141 | except Exception as e: 142 | logger.exception(f"Error loading {mode.value} {analysis_type.value} chunk cache: {str(e)}") 143 | cache_dict[file_path] = {} 144 | return {} 145 | 146 | elif action == 'save': 147 | if file_path not in cache_dict: 148 | return 149 | 150 | try: 151 | cache_path.parent.mkdir(parents=True, exist_ok=True) 152 | with open(cache_path, 'wb') as f: 153 | pickle.dump(cache_dict[file_path], f) 154 | logger.debug(f"Saved {mode.value} {analysis_type.value} chunk cache for {file_path}: {len(cache_dict[file_path])} entries") 155 | except Exception as e: 156 | logger.exception(f"Error saving {mode.value} {analysis_type.value} chunk cache: {str(e)}") 157 | 158 | def load_chunk_cache(self, file_path: str, mode: AnalysisMode = AnalysisMode.DEEP, 159 | analysis_type: AnalysisType = AnalysisType.STANDARD) -> Dict: 160 | """ 161 | Load appropriate chunk cache for a specific file based on mode and analysis type 162 | 163 | Args: 164 | file_path: Path to the file 165 | mode: Analysis mode (scan or deep) 166 | analysis_type: Analysis type (standard or adaptive) 167 | 168 | Returns: 169 | Dictionary with cached analysis results 170 | """ 171 | return self.process_cache('load', file_path, mode, analysis_type) 172 | 173 | def save_chunk_cache(self, file_path: str, mode: AnalysisMode = AnalysisMode.DEEP, 174 | analysis_type: AnalysisType = AnalysisType.STANDARD): 175 | """ 176 | Save appropriate chunk cache for a specific file based on mode and analysis type 177 | 178 | Args: 179 | file_path: Path to the file 180 | mode: Analysis mode (scan or deep) 181 | analysis_type: Analysis type (standard or adaptive) 182 | """ 183 | self.process_cache('save', file_path, mode, analysis_type) 184 | 185 | def has_caching_info(self, file_path: str, chunk: str, vuln_name: str, analysis_type: AnalysisType = AnalysisType.STANDARD) -> bool: 186 | """ 187 | Check if we have enough info to cache this analysis 188 | 189 | Args: 190 | file_path: Path to the file 191 | chunk: Code chunk content 192 | vuln_name: Vulnerability name 193 | analysis_type: Type of analysis (standard or adaptive) 194 | 195 | Returns: 196 | True if we have enough info to cache 197 | """ 198 | # For adaptive analysis, we only need file_path and vuln_name 199 | if analysis_type == AnalysisType.ADAPTIVE: 200 | return bool(file_path and vuln_name) 201 | 202 | # For standard analysis, we need all three 203 | return bool(file_path and chunk and vuln_name) 204 | 205 | def get_cached_analysis(self, file_path: str, chunk: str, vuln_name: str, prompt: str, 206 | mode: AnalysisMode = AnalysisMode.DEEP, 207 | analysis_type: AnalysisType = AnalysisType.STANDARD) -> str: 208 | """ 209 | Check if analysis for a chunk is already cached 210 | 211 | Args: 212 | file_path: Path to the file 213 | chunk: Code chunk content 214 | vuln_name: Vulnerability name (for better organization) 215 | prompt: Complete analysis prompt (to detect changes in prompt structure) 216 | mode: Analysis mode (scan or deep) 217 | analysis_type: Analysis type (standard or adaptive) 218 | 219 | Returns: 220 | Cached analysis result or None if not found 221 | """ 222 | # Load cache for this file if not already loaded 223 | cache_dict = self.get_cache_dict(mode, analysis_type) 224 | if file_path not in cache_dict: 225 | self.load_chunk_cache(file_path, mode, analysis_type) 226 | 227 | chunk_key = self.generate_cache_key(chunk, prompt, vuln_name, file_path) 228 | 229 | # Check if analysis exists in cache 230 | return cache_dict[file_path].get(chunk_key) 231 | 232 | def store_analysis(self, file_path: str, chunk: str, vuln_name: str, prompt: str, result: str, 233 | mode: AnalysisMode, analysis_type: AnalysisType): 234 | """ 235 | Store analysis result in cache 236 | 237 | Args: 238 | file_path: Path to the file 239 | chunk: Code chunk content 240 | vuln_name: Vulnerability name 241 | prompt: Analysis prompt 242 | result: Analysis result 243 | mode: Analysis mode 244 | analysis_type: Analysis type 245 | """ 246 | if not self.has_caching_info(file_path, chunk, vuln_name, analysis_type): 247 | return 248 | 249 | cache_dict = self.get_cache_dict(mode, analysis_type) 250 | if file_path not in cache_dict: 251 | cache_dict[file_path] = {} 252 | 253 | chunk_key = self.generate_cache_key(chunk, prompt, vuln_name, file_path) 254 | cache_dict[file_path][chunk_key] = result 255 | 256 | # Save cache after each analysis to allow resuming at any point 257 | self.save_chunk_cache(file_path, mode, analysis_type) 258 | 259 | def generate_cache_key(self, chunk: str, prompt: str, vuln_name: str, file_path: str = None) -> str: 260 | """ 261 | Generate a cache key for a chunk 262 | 263 | Args: 264 | chunk: Code chunk content 265 | prompt: Analysis prompt 266 | vuln_name: Vulnerability name 267 | file_path: Path to the file (for adaptive analysis) 268 | 269 | Returns: 270 | Cache key 271 | """ 272 | # For adaptive analysis (empty chunk) 273 | if not chunk: 274 | if file_path: 275 | return f"{sanitize_name(file_path)}_{sanitize_name(vuln_name)}" 276 | return sanitize_name(vuln_name) 277 | 278 | # For standard analysis 279 | chunk_hash = hashlib.md5(chunk.encode()).hexdigest() 280 | prompt_hash = hashlib.md5(prompt.encode()).hexdigest() 281 | return f"{chunk_hash}_{prompt_hash}_{sanitize_name(vuln_name)}" 282 | 283 | def clear_scan_cache(self, analysis_type: AnalysisType = None) -> None: 284 | """ 285 | Clear scan cache files 286 | 287 | Args: 288 | analysis_type: Analysis type to clear (if None, clears all) 289 | """ 290 | try: 291 | # Determine which cache directories to clear 292 | if analysis_type is None: 293 | # Clear only the current analysis type directories 294 | cache_dirs = [ 295 | self.standard_cache_dir[AnalysisMode.SCAN] if AnalysisType.STANDARD in self.scan_chunk_cache else None, 296 | self.standard_cache_dir[AnalysisMode.DEEP] if AnalysisType.STANDARD in self.chunk_cache else None, 297 | self.adaptive_cache_dir[AnalysisMode.SCAN] if AnalysisType.ADAPTIVE in self.scan_chunk_cache else None, 298 | self.adaptive_cache_dir[AnalysisMode.DEEP] if AnalysisType.ADAPTIVE in self.chunk_cache else None 299 | ] 300 | else: 301 | # Clear only the specified analysis type 302 | cache_dirs = [ 303 | self.standard_cache_dir[AnalysisMode.SCAN] if analysis_type == AnalysisType.STANDARD else None, 304 | self.standard_cache_dir[AnalysisMode.DEEP] if analysis_type == AnalysisType.STANDARD else None, 305 | self.adaptive_cache_dir[AnalysisMode.SCAN] if analysis_type == AnalysisType.ADAPTIVE else None, 306 | self.adaptive_cache_dir[AnalysisMode.DEEP] if analysis_type == AnalysisType.ADAPTIVE else None 307 | ] 308 | cache_dirs = [d for d in cache_dirs if d is not None] 309 | if not cache_dirs: 310 | logger.warning("No cache directories to clear") 311 | return 312 | 313 | # Delete all cache files in these directories 314 | files_count = 0 315 | for cache_dir in cache_dirs: 316 | if cache_dir.exists(): 317 | for cache_file in cache_dir.glob("*.cache"): 318 | cache_file.unlink() 319 | files_count += 1 320 | 321 | # Reset cache dictionaries for the current session 322 | if analysis_type is None or analysis_type == AnalysisType.STANDARD: 323 | self.chunk_cache[AnalysisType.STANDARD] = {} 324 | self.scan_chunk_cache[AnalysisType.STANDARD] = {} 325 | 326 | if analysis_type is None or analysis_type == AnalysisType.ADAPTIVE: 327 | self.chunk_cache[AnalysisType.ADAPTIVE] = {} 328 | self.scan_chunk_cache[AnalysisType.ADAPTIVE] = {} 329 | self.adaptive_analysis_cache = {} 330 | 331 | logger.info(f"Cleared {files_count} scan cache files") 332 | 333 | except Exception as e: 334 | logger.exception(f"Error clearing scan cache: {str(e)}") 335 | 336 | def validate_cache_expiration(self): 337 | """ 338 | Check all cache files and remove expired ones based on cache_days setting 339 | """ 340 | try: 341 | # Get current time 342 | now = datetime.now() 343 | expired_count = 0 344 | 345 | # Check both standard and adaptive cache directories 346 | all_cache_dirs = list(self.standard_cache_dir.values()) + list(self.adaptive_cache_dir.values()) 347 | 348 | for cache_dir in all_cache_dirs: 349 | if not cache_dir.exists(): 350 | continue 351 | 352 | # Check each cache file in this directory 353 | for cache_file in cache_dir.glob("*.cache"): 354 | # Get file modification time 355 | mod_time = datetime.fromtimestamp(cache_file.stat().st_mtime) 356 | cache_age = now - mod_time 357 | 358 | # Check if file is older than cache_days 359 | if cache_age.days > self.cache_days: 360 | try: 361 | cache_file.unlink() 362 | expired_count += 1 363 | except Exception as e: 364 | logger.warning(f"Could not delete expired cache file {cache_file}: {e}") 365 | 366 | if expired_count > 0: 367 | logger.info(f"Removed {expired_count} expired cache files older than {self.cache_days} days") 368 | 369 | except Exception as e: 370 | logger.exception(f"Error validating cache expiration: {str(e)}") 371 | -------------------------------------------------------------------------------- /oasis/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | # Define analysis modes and types 4 | class AnalysisMode(Enum): 5 | SCAN = "scan" # Lightweight scanning mode 6 | DEEP = "deep" # Deep analysis mode 7 | 8 | class AnalysisType(Enum): 9 | STANDARD = "standard" # Standard two-phase analysis 10 | ADAPTIVE = "adaptive" # Multi-level adaptive analysis -------------------------------------------------------------------------------- /oasis/images/oasis-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/oasis/images/oasis-logo.jpg -------------------------------------------------------------------------------- /oasis/images/oasis-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/oasis/images/oasis-logo.webp -------------------------------------------------------------------------------- /oasis/static/img/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/oasis/static/img/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /oasis/static/img/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/oasis/static/img/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /oasis/static/img/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/oasis/static/img/favicon/favicon.ico -------------------------------------------------------------------------------- /oasis/static/img/favicon/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oasis/static/img/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Oasis", 3 | "short_name": "Oasis", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /oasis/static/img/favicon/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/oasis/static/img/favicon/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /oasis/static/img/favicon/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/oasis/static/img/favicon/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /oasis/static/img/oasis-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyray/oasis/8ae0d5a52d3235c6a5ea1bffef9996f9c6f18884/oasis/static/img/oasis-logo.jpg -------------------------------------------------------------------------------- /oasis/static/js/dashboard-app.js: -------------------------------------------------------------------------------- 1 | // Dashboard Application - Main entry point 2 | const DashboardApp = { 3 | // State 4 | currentViewMode: 'list', 5 | reportData: [], 6 | stats: {}, 7 | cardTemplate: null, 8 | activeFilters: { 9 | models: [], 10 | formats: [], 11 | vulnerabilities: [], 12 | dateRange: null 13 | }, 14 | filtersPopulated: false, 15 | currentReportPath: '', 16 | currentReportFormat: '', 17 | currentResizeObserver: null, 18 | debugMode: false, // Initialize debug flag 19 | 20 | // Debug logging function that only logs when debug mode is active 21 | debug: function(message, ...args) { 22 | if (this.debugMode) { 23 | console.log(message, ...args); 24 | } 25 | }, 26 | 27 | // loading utilities used by multiple modules 28 | showLoading: function(containerId) { 29 | const container = document.getElementById(containerId); 30 | if (container) { 31 | container.innerHTML = '
'; 32 | } 33 | }, 34 | 35 | hideLoading: function(containerId) { 36 | // The content will be replaced by the rendering functions 37 | }, 38 | 39 | // Initialize the application 40 | init: function() { 41 | // Check for debug mode from server (data attribute on the body) 42 | const debugModeAttr = document.body.getAttribute('data-debug-mode'); 43 | this.debugMode = debugModeAttr === 'true' || debugModeAttr === '1'; 44 | 45 | this.debug("Initializing DashboardApp in " + (this.debugMode ? "DEBUG" : "PRODUCTION") + " mode..."); 46 | 47 | // Define global functions immediately to avoid duplication 48 | this.defineGlobalFunctions(); 49 | 50 | // Load all modules in the correct order 51 | this.loadModules() 52 | .then(() => { 53 | this.debug("All modules loaded successfully"); 54 | this.startApplication(); 55 | }) 56 | .catch(error => { 57 | console.error("Error loading modules:", error); 58 | document.body.innerHTML = '
Error loading application. Please refresh the page.
'; 59 | }); 60 | }, 61 | 62 | // Define global functions once to avoid duplication 63 | defineGlobalFunctions: function() { 64 | this.debug("Defining global functions"); 65 | 66 | // Global handlers for HTML onclick events - no need to redefine all of them 67 | // Only those used in the HTML as onclick attributes 68 | window.openReport = function(path, format) { 69 | if (DashboardApp.openReport) { 70 | DashboardApp.openReport(path, format); 71 | } else { 72 | console.error("openReport not yet loaded"); 73 | } 74 | }; 75 | 76 | window.downloadReportFile = function(path, format) { 77 | if (DashboardApp.downloadReportFile) { 78 | DashboardApp.downloadReportFile(path, format); 79 | } else { 80 | console.error("downloadReportFile not yet loaded"); 81 | } 82 | }; 83 | 84 | window.closeReportModal = function() { 85 | if (DashboardApp.closeReportModal) { 86 | DashboardApp.closeReportModal(); 87 | } else { 88 | console.error("closeReportModal not yet loaded"); 89 | } 90 | }; 91 | 92 | window.filterDatesByModel = function(modelElement) { 93 | if (DashboardApp.filterDatesByModel) { 94 | DashboardApp.filterDatesByModel(modelElement); 95 | } else { 96 | console.error("filterDatesByModel not yet loaded"); 97 | } 98 | }; 99 | 100 | }, 101 | 102 | // Load all required modules 103 | loadModules: function() { 104 | return new Promise((resolve, reject) => { 105 | // Define modules to load in order 106 | const modules = [ 107 | 'utils.js', 108 | 'filters.js', 109 | 'views.js', 110 | 'api.js', 111 | 'modal.js', 112 | 'interactions.js' 113 | ]; 114 | 115 | let loadedCount = 0; 116 | 117 | // Function to load a script 118 | const loadScript = (src) => { 119 | return new Promise((resolve, reject) => { 120 | const script = document.createElement('script'); 121 | script.src = `/static/js/dashboard/${src}`; 122 | script.onload = () => resolve(); 123 | script.onerror = () => reject(new Error(`Failed to load script: ${src}`)); 124 | document.head.appendChild(script); 125 | }); 126 | }; 127 | 128 | // Load scripts sequentially 129 | const loadNextScript = (index) => { 130 | if (index >= modules.length) { 131 | resolve(); 132 | return; 133 | } 134 | 135 | loadScript(modules[index]) 136 | .then(() => { 137 | loadedCount++; 138 | loadNextScript(index + 1); 139 | }) 140 | .catch(reject); 141 | }; 142 | 143 | // Start loading scripts 144 | loadNextScript(0); 145 | }); 146 | }, 147 | 148 | // Start the application after modules are loaded 149 | startApplication: function() { 150 | DashboardApp.debug("Starting application..."); 151 | 152 | // Load card template 153 | this.loadCardTemplate(); 154 | 155 | // Initialize the dashboard 156 | this.fetchReports(); 157 | this.fetchStats(); 158 | this.initializeFilters(); 159 | 160 | // Initialize modal events if available 161 | if (this.initializeModalEvents) { 162 | this.initializeModalEvents(); 163 | } 164 | 165 | // Setup mobile navigation 166 | if (this.setupMobileNavigation) { 167 | this.setupMobileNavigation(); 168 | } 169 | 170 | // Setup event listeners 171 | this.setupEventListeners(); 172 | }, 173 | 174 | // Load card template 175 | loadCardTemplate: function() { 176 | fetch('/static/templates/dashboard_card.html') 177 | .then(response => response.text()) 178 | .then(template => { 179 | this.cardTemplate = template; 180 | DashboardApp.debug("Card template loaded"); 181 | }) 182 | .catch(error => { 183 | console.error("Error loading card template:", error); 184 | }); 185 | }, 186 | 187 | // Setup global event listeners 188 | setupEventListeners: function() { 189 | DashboardApp.debug("Setting up event listeners..."); 190 | 191 | // Clear filters button 192 | const clearFiltersBtn = document.getElementById('filter-clear'); 193 | if (clearFiltersBtn) { 194 | const self = this; 195 | clearFiltersBtn.addEventListener('click', function() { 196 | // Reset all filters 197 | self.activeFilters = { 198 | models: [], 199 | formats: [], 200 | vulnerabilities: [], 201 | dateRange: null 202 | }; 203 | 204 | // Reset checkboxes 205 | document.querySelectorAll('.filter-checkbox').forEach(checkbox => { 206 | checkbox.checked = false; 207 | }); 208 | 209 | // Reset date fields 210 | const dateStart = document.getElementById('date-start'); 211 | const dateEnd = document.getElementById('date-end'); 212 | if (dateStart) { 213 | dateStart.value = ''; 214 | } 215 | if (dateEnd) { 216 | dateEnd.value = ''; 217 | } 218 | 219 | // Refresh data 220 | self.fetchReports(); 221 | self.fetchStats(); 222 | }); 223 | } 224 | 225 | // Reload/refresh links 226 | const self = this; 227 | document.addEventListener('click', function(e) { 228 | if (e.target.tagName === 'A' && 229 | (e.target.innerText.includes('Reload') || 230 | e.target.href?.includes('get_stats'))) { 231 | 232 | self.refreshDashboard(); 233 | e.preventDefault(); 234 | } 235 | }); 236 | }, 237 | }; 238 | 239 | // Initialize the application when DOM is ready 240 | document.addEventListener('DOMContentLoaded', function() { 241 | DashboardApp.init(); 242 | }); 243 | -------------------------------------------------------------------------------- /oasis/static/js/dashboard/api.js: -------------------------------------------------------------------------------- 1 | // API functions for fetching data 2 | DashboardApp.buildFilterParams = function() { 3 | // Create and return URLSearchParams object with active filters 4 | const params = new URLSearchParams(); 5 | 6 | if (DashboardApp.activeFilters.models && DashboardApp.activeFilters.models.length > 0) { 7 | params.append('model', DashboardApp.activeFilters.models.join(',')); 8 | } 9 | 10 | if (DashboardApp.activeFilters.formats && DashboardApp.activeFilters.formats.length > 0) { 11 | params.append('format', DashboardApp.activeFilters.formats.join(',')); 12 | } 13 | 14 | if (DashboardApp.activeFilters.vulnerabilities && DashboardApp.activeFilters.vulnerabilities.length > 0) { 15 | params.append('vulnerability', DashboardApp.activeFilters.vulnerabilities.join(',')); 16 | } 17 | 18 | if (DashboardApp.activeFilters.dateRange) { 19 | if (DashboardApp.activeFilters.dateRange.start) { 20 | params.append('start_date', DashboardApp.activeFilters.dateRange.start); 21 | } 22 | if (DashboardApp.activeFilters.dateRange.end) { 23 | params.append('end_date', DashboardApp.activeFilters.dateRange.end); 24 | } 25 | } 26 | 27 | return params; 28 | }; 29 | 30 | DashboardApp.fetchReports = function() { 31 | DashboardApp.debug("Fetching reports..."); 32 | DashboardApp.showLoading('reports-container'); 33 | 34 | // Use the utility function to build parameters 35 | const params = DashboardApp.buildFilterParams(); 36 | 37 | DashboardApp.debug("Filter params:", params.toString()); 38 | 39 | // Fetch reports 40 | fetch(`/api/reports?${params.toString()}`) 41 | .then(response => { 42 | if (!response.ok) { 43 | throw new Error('Network response was not ok'); 44 | } 45 | return response.json(); 46 | }) 47 | .then(data => { 48 | const reports = Array.isArray(data) ? data : (data.reports || []); 49 | DashboardApp.debug("Reports fetched:", reports.length); 50 | 51 | // Process and store the reports 52 | DashboardApp.reportData = DashboardApp.groupReportsByModelAndVuln(reports); 53 | 54 | // Render the reports in the current view 55 | DashboardApp.renderCurrentView(); 56 | }) 57 | .catch(error => { 58 | console.error('Error fetching reports:', error); 59 | document.getElementById('reports-container').innerHTML = 60 | `
Error fetching reports: ${error.message}
`; 61 | }); 62 | }; 63 | 64 | DashboardApp.fetchStats = function(forceRefresh = false) { 65 | DashboardApp.debug("Fetching stats..."); 66 | DashboardApp.showLoading('stats-container'); 67 | 68 | // Use the utility function to build parameters 69 | const params = DashboardApp.buildFilterParams(); 70 | 71 | // Add force parameter if requested 72 | if (forceRefresh) { 73 | params.append('force', '1'); 74 | } 75 | 76 | DashboardApp.debug("Stats filter params:", params.toString()); 77 | 78 | // Fetch stats 79 | fetch(`/api/stats?${params.toString()}`) 80 | .then(response => { 81 | if (!response.ok) { 82 | throw new Error('Network response was not ok'); 83 | } 84 | return response.json(); 85 | }) 86 | .then(data => { 87 | DashboardApp.stats = data; 88 | 89 | // Render the statistics 90 | DashboardApp.renderStats(); 91 | 92 | // Update filter counts but not change selected filters 93 | if (!DashboardApp.filtersPopulated) { 94 | DashboardApp.populateFilters(); 95 | DashboardApp.filtersPopulated = true; 96 | } else { 97 | DashboardApp.updateFilterCounts(); 98 | } 99 | }) 100 | .catch(error => { 101 | console.error('Error fetching stats:', error); 102 | document.getElementById('stats-container').innerHTML = 103 | `
Error fetching stats: ${error.message}
`; 104 | }); 105 | }; 106 | 107 | DashboardApp.refreshDashboard = function() { 108 | DashboardApp.debug("Refreshing dashboard..."); 109 | 110 | // Show loading indicators 111 | this.showLoading('stats-container'); 112 | this.showLoading('reports-container'); 113 | 114 | // Fetch fresh data 115 | Promise.all([ 116 | fetch('/api/stats?force=1').then(response => response.json()), 117 | fetch('/api/reports').then(response => response.json()) 118 | ]) 119 | .then(([statsData, reportsData]) => { 120 | // Update the state 121 | this.stats = statsData; 122 | this.reportData = this.groupReportsByModelAndVuln(reportsData); 123 | 124 | // Render the updated data 125 | this.renderStats(); 126 | this.renderCurrentView(); 127 | 128 | // Update filters if necessary 129 | if (!this.filtersPopulated) { 130 | this.populateFilters(); 131 | this.filtersPopulated = true; 132 | } else { 133 | this.updateFilterCounts(); 134 | } 135 | 136 | DashboardApp.debug('Dashboard refreshed successfully'); 137 | }) 138 | .catch(error => { 139 | console.error('Error refreshing dashboard:', error); 140 | document.getElementById('stats-container').innerHTML = 141 | '
Error refreshing dashboard. Please try again later.
'; 142 | }); 143 | }; 144 | 145 | DashboardApp.debug("API module loaded"); -------------------------------------------------------------------------------- /oasis/static/js/dashboard/filters.js: -------------------------------------------------------------------------------- 1 | // Filter management functions 2 | DashboardApp.renderFilters = function() { 3 | DashboardApp.debug("Rendering filters..."); 4 | const modelFiltersContainer = document.getElementById('model-filters-section'); 5 | const vulnFiltersContainer = document.getElementById('vulnerability-filters-section'); 6 | const formatFiltersContainer = document.getElementById('format-filters-section'); 7 | const dateFiltersContainer = document.getElementById('date-filters-section'); 8 | 9 | if (modelFiltersContainer) { 10 | modelFiltersContainer.innerHTML = ` 11 |
🤖 Filter by model
12 |
13 |
14 | `; 15 | } 16 | 17 | if (vulnFiltersContainer) { 18 | vulnFiltersContainer.innerHTML = ` 19 |
🛡️ Filter by vulnerability
20 |
21 | `; 22 | } 23 | 24 | if (dateFiltersContainer) { 25 | dateFiltersContainer.innerHTML = ` 26 |
📅 Filter by date range
27 |
28 | `; 29 | } 30 | }; 31 | 32 | DashboardApp.populateFilters = function() { 33 | DashboardApp.debug("Populating filters..."); 34 | // Populate model filters 35 | const modelFiltersContainer = document.getElementById('model-filters'); 36 | let modelFiltersHtml = ''; 37 | 38 | Object.keys(DashboardApp.stats.models || {}).sort().forEach(model => { 39 | const count = DashboardApp.stats.models[model]; 40 | const formattedModel = DashboardApp.formatDisplayName(model, 'model'); 41 | modelFiltersHtml += ` 42 |
43 | 47 |
48 | `; 49 | }); 50 | 51 | if (modelFiltersContainer) { 52 | modelFiltersContainer.innerHTML = modelFiltersHtml || '
No model available
'; 53 | } 54 | 55 | // Populate vulnerability filters 56 | const vulnFiltersContainer = document.getElementById('vulnerability-filters'); 57 | let vulnFiltersHtml = ''; 58 | 59 | Object.keys(DashboardApp.stats.vulnerabilities || {}).sort().forEach(vuln => { 60 | const count = DashboardApp.stats.vulnerabilities[vuln]; 61 | const formattedVuln = DashboardApp.formatDisplayName(vuln, 'vulnerability'); 62 | vulnFiltersHtml += ` 63 |
64 | 68 |
69 | `; 70 | }); 71 | 72 | if (vulnFiltersContainer) { 73 | vulnFiltersContainer.innerHTML = vulnFiltersHtml || '
No vulnerability type available
'; 74 | } 75 | 76 | // Populate format filters 77 | const formatFiltersContainer = document.getElementById('format-filters'); 78 | let formatFiltersHtml = ''; 79 | 80 | Object.keys(DashboardApp.stats.formats || {}).sort().forEach(format => { 81 | const count = DashboardApp.stats.formats[format]; 82 | const formattedFormat = DashboardApp.formatDisplayName(format, 'format'); 83 | formatFiltersHtml += ` 84 |
85 | 89 |
90 | `; 91 | }); 92 | 93 | if (formatFiltersContainer) { 94 | formatFiltersContainer.innerHTML = formatFiltersHtml || '
No format available
'; 95 | } 96 | 97 | // Re-add event listeners 98 | document.querySelectorAll('.filter-checkbox').forEach(checkbox => { 99 | checkbox.addEventListener('change', function() { 100 | const {type, value} = this.dataset; 101 | const isChecked = this.checked; 102 | 103 | // Map filter type to the corresponding property in activeFilters using object lookup 104 | const typeMapping = { 105 | 'model': 'models', 106 | 'vulnerability': 'vulnerabilities', 107 | 'format': 'formats' 108 | }; 109 | 110 | const filterType = typeMapping[type]; 111 | 112 | // Return early if unknown type 113 | if (!filterType) { 114 | return; 115 | } 116 | 117 | // Update active filters array 118 | if (isChecked) { 119 | // Add value if it's not already present 120 | if (!DashboardApp.activeFilters[filterType].includes(value)) { 121 | DashboardApp.activeFilters[filterType].push(value); 122 | } 123 | } else { 124 | // Remove value if it's present 125 | DashboardApp.activeFilters[filterType] = DashboardApp.activeFilters[filterType].filter(item => item !== value); 126 | } 127 | 128 | // Refresh reports and stats with new filters 129 | DashboardApp.fetchReports(); 130 | DashboardApp.fetchStats(); 131 | }); 132 | }); 133 | 134 | // Add date filter HTML 135 | const dateFiltersContainer = document.getElementById('date-filters'); 136 | if (dateFiltersContainer) { 137 | dateFiltersContainer.innerHTML = ` 138 |
139 |
140 | 141 | 142 |
143 |
144 | 145 | 146 |
147 | 148 |
149 | `; 150 | 151 | const dateFilterBtn = document.getElementById('date-filter-apply'); 152 | dateFilterBtn.addEventListener('click', function() { 153 | const startDate = document.getElementById('date-start').value; 154 | const endDate = document.getElementById('date-end').value; 155 | 156 | DashboardApp.debug("Date filter applied:", startDate, endDate); 157 | 158 | DashboardApp.activeFilters.dateRange = { 159 | start: startDate ? new Date(startDate).toISOString() : null, 160 | end: endDate ? new Date(endDate + 'T23:59:59').toISOString() : null 161 | }; 162 | 163 | DashboardApp.fetchReports(); 164 | DashboardApp.fetchStats(); 165 | }); 166 | } 167 | }; 168 | 169 | DashboardApp.updateFilterCounts = function() { 170 | DashboardApp.debug("Updating filter counts..."); 171 | // Save the current state of filters 172 | const checkedFilters = { 173 | model: {}, 174 | vulnerability: {}, 175 | format: {} 176 | }; 177 | 178 | document.querySelectorAll('.filter-checkbox').forEach(checkbox => { 179 | const {type, value} = checkbox.dataset; 180 | checkedFilters[type][value] = checkbox.checked; 181 | }); 182 | 183 | // Update model filters - always display all models 184 | const modelFilters = document.querySelectorAll('.filter-option[data-type="model"]'); 185 | modelFilters.forEach(option => { 186 | const modelValue = option.dataset.value; 187 | const countSpan = option.querySelector('.filter-count'); 188 | 189 | if (modelValue && countSpan && DashboardApp.stats.models) { 190 | countSpan.textContent = `(${DashboardApp.stats.models[modelValue] || 0})`; 191 | } 192 | }); 193 | 194 | // Update vulnerability filters 195 | // If models are selected, only show vulnerabilities associated with these models 196 | const vulnFilters = document.querySelectorAll('.filter-option[data-type="vulnerability"]'); 197 | vulnFilters.forEach(option => { 198 | const vulnValue = option.dataset.value; 199 | const countSpan = option.querySelector('.filter-count'); 200 | 201 | // Always display all vulnerabilities, but update the counts 202 | if (vulnValue && countSpan && DashboardApp.stats.vulnerabilities) { 203 | countSpan.textContent = `(${DashboardApp.stats.vulnerabilities[vulnValue] || 0})`; 204 | 205 | // If the count is 0, we can hide this option 206 | if (DashboardApp.stats.vulnerabilities[vulnValue] === 0) { 207 | option.classList.add('empty-filter'); 208 | } else { 209 | option.classList.remove('empty-filter'); 210 | } 211 | } 212 | }); 213 | 214 | // Update format filters 215 | const formatFilters = document.querySelectorAll('.filter-option[data-type="format"]'); 216 | formatFilters.forEach(option => { 217 | const formatValue = option.dataset.value; 218 | const countSpan = option.querySelector('.filter-count'); 219 | 220 | if (formatValue && countSpan && DashboardApp.stats.formats) { 221 | countSpan.textContent = `(${DashboardApp.stats.formats[formatValue] || 0})`; 222 | } 223 | }); 224 | }; 225 | 226 | DashboardApp.initializeFilters = function() { 227 | DashboardApp.debug("Initializing filters..."); 228 | this.renderFilters(); 229 | 230 | // Re-add event listeners for filter checkboxes 231 | document.querySelectorAll('.filter-checkbox').forEach(checkbox => { 232 | checkbox.addEventListener('change', function() { 233 | const {type, value} = this.dataset; 234 | const isChecked = this.checked; 235 | 236 | // Map filter type to the corresponding property in activeFilters using object lookup 237 | const typeMapping = { 238 | 'model': 'models', 239 | 'vulnerability': 'vulnerabilities', 240 | 'format': 'formats' 241 | }; 242 | 243 | const filterType = typeMapping[type]; 244 | 245 | // Return early if unknown type 246 | if (!filterType) { 247 | return; 248 | } 249 | 250 | // Update active filters array 251 | if (isChecked) { 252 | // Add value if it's not already present 253 | if (!DashboardApp.activeFilters[filterType].includes(value)) { 254 | DashboardApp.activeFilters[filterType].push(value); 255 | } 256 | } else { 257 | // Remove value if it's present 258 | DashboardApp.activeFilters[filterType] = DashboardApp.activeFilters[filterType].filter(item => item !== value); 259 | } 260 | 261 | // Pour le débogage 262 | DashboardApp.debug("Updated filters:", DashboardApp.activeFilters); 263 | 264 | // Refresh reports and stats with new filters 265 | DashboardApp.fetchReports(); 266 | DashboardApp.fetchStats(); 267 | }); 268 | }); 269 | 270 | // Initialize event listeners for the filter reset button 271 | const clearFiltersBtn = document.getElementById('filter-clear'); 272 | if (clearFiltersBtn) { 273 | clearFiltersBtn.addEventListener('click', function() { 274 | // Reset all filter arrays 275 | DashboardApp.activeFilters.models = []; 276 | DashboardApp.activeFilters.formats = []; 277 | DashboardApp.activeFilters.vulnerabilities = []; 278 | DashboardApp.activeFilters.dateRange = null; 279 | 280 | // Reset checkboxes 281 | document.querySelectorAll('.filter-checkbox').forEach(checkbox => { 282 | checkbox.checked = false; 283 | }); 284 | 285 | // Reset date fields 286 | document.getElementById('date-start').value = ''; 287 | document.getElementById('date-end').value = ''; 288 | 289 | // Refresh data 290 | DashboardApp.fetchReports(); 291 | DashboardApp.fetchStats(); 292 | }); 293 | } 294 | }; 295 | 296 | DashboardApp.clearFilters = function() { 297 | DashboardApp.debug("Clearing filters..."); 298 | // Uncheck all checkboxes 299 | document.querySelectorAll('.filter-checkbox').forEach(checkbox => { 300 | checkbox.checked = false; 301 | }); 302 | 303 | // Reset active filters 304 | DashboardApp.activeFilters = { 305 | models: [], 306 | formats: [], 307 | vulnerabilities: [], 308 | dateRange: null 309 | }; 310 | 311 | // Refresh reports only - stats will be calculated from reports 312 | DashboardApp.fetchReports(); 313 | }; 314 | 315 | // Function to set up mobile navigation 316 | DashboardApp.setupMobileNavigation = function() { 317 | DashboardApp.debug("Setting up mobile navigation..."); 318 | const toggleFiltersBtn = document.getElementById('toggle-filters'); 319 | const sidebar = document.querySelector('.sidebar'); 320 | 321 | if (!toggleFiltersBtn || !sidebar) { 322 | DashboardApp.debug("Toggle button or sidebar not found"); 323 | return; 324 | } 325 | 326 | // Create an overlay to facilitate the closing of the menu 327 | const overlay = document.createElement('div'); 328 | overlay.className = 'sidebar-overlay'; 329 | document.body.appendChild(overlay); 330 | 331 | // Restore the previous menu state if saved 332 | if (localStorage.getItem('filtersExpanded') === 'true') { 333 | sidebar.classList.add('expanded'); 334 | overlay.classList.add('active'); 335 | } 336 | 337 | // Function to toggle the menu state 338 | const toggleMenu = () => { 339 | const isExpanded = sidebar.classList.contains('expanded'); 340 | 341 | if (isExpanded) { 342 | sidebar.classList.remove('expanded'); 343 | overlay.classList.remove('active'); 344 | localStorage.setItem('filtersExpanded', 'false'); 345 | } else { 346 | sidebar.classList.add('expanded'); 347 | overlay.classList.add('active'); 348 | localStorage.setItem('filtersExpanded', 'true'); 349 | } 350 | }; 351 | 352 | // Click event on the button 353 | toggleFiltersBtn.addEventListener('click', function(e) { 354 | e.stopPropagation(); // Prevent propagation to the document 355 | toggleMenu(); 356 | }); 357 | 358 | // Close the menu when clicking on the overlay 359 | overlay.addEventListener('click', function() { 360 | sidebar.classList.remove('expanded'); 361 | overlay.classList.remove('active'); 362 | localStorage.setItem('filtersExpanded', 'false'); 363 | }); 364 | }; 365 | 366 | DashboardApp.debug("Filters module loaded"); -------------------------------------------------------------------------------- /oasis/static/js/dashboard/interactions.js: -------------------------------------------------------------------------------- 1 | // User interaction functions 2 | DashboardApp.toggleTreeSection = function(header) { 3 | DashboardApp.debug("Toggling tree section"); 4 | const section = header.parentElement; 5 | const content = section.querySelector('.tree-content'); 6 | const toggle = header.querySelector('.tree-toggle'); 7 | 8 | if (content.style.display === 'none') { 9 | content.style.display = 'block'; 10 | toggle.textContent = '▼'; 11 | } else { 12 | content.style.display = 'none'; 13 | toggle.textContent = '►'; 14 | } 15 | }; 16 | 17 | DashboardApp.toggleTreeNode = function(header) { 18 | DashboardApp.debug("Toggling tree node"); 19 | const item = header.parentElement; 20 | const content = item.querySelector('.tree-dates-list'); 21 | 22 | if (content.style.display === 'none') { 23 | content.style.display = 'block'; 24 | } else { 25 | content.style.display = 'none'; 26 | } 27 | }; 28 | 29 | DashboardApp.filterByModel = function(model) { 30 | DashboardApp.debug("Filtering by model:", model); 31 | 32 | // Reset all filters 33 | DashboardApp.activeFilters = { 34 | models: [model], 35 | formats: [], 36 | vulnerabilities: [], 37 | dateRange: null 38 | }; 39 | 40 | // Update UI to reflect the selected model 41 | document.querySelectorAll('.filter-checkbox[data-type="model"]').forEach(checkbox => { 42 | checkbox.checked = checkbox.dataset.value === model; 43 | }); 44 | 45 | // Reset other checkboxes 46 | document.querySelectorAll('.filter-checkbox:not([data-type="model"])').forEach(checkbox => { 47 | checkbox.checked = false; 48 | }); 49 | 50 | // Reset date inputs 51 | const startDateInput = document.getElementById('date-start'); 52 | const endDateInput = document.getElementById('date-end'); 53 | if (startDateInput) { 54 | startDateInput.value = ''; 55 | } 56 | if (endDateInput) { 57 | endDateInput.value = ''; 58 | } 59 | 60 | // Fetch new data 61 | DashboardApp.fetchReports(); 62 | DashboardApp.fetchStats(); 63 | }; 64 | 65 | DashboardApp.filterDatesByModel = function(modelElement) { 66 | DashboardApp.debug("Filtering dates by model"); 67 | const modelName = modelElement.dataset.model; 68 | const card = modelElement.closest('.report-card'); 69 | 70 | if (!card || !modelName) { 71 | console.error("Cannot filter dates: missing model or card"); 72 | return; 73 | } 74 | 75 | // Mark this model as selected within the card 76 | card.querySelectorAll('.model-tag').forEach(tag => { 77 | if (tag.dataset.model === modelName) { 78 | tag.classList.add('selected'); 79 | } else { 80 | tag.classList.remove('selected'); 81 | } 82 | }); 83 | 84 | // Get vulnerability type from the card 85 | const titleElement = card.querySelector('.report-title[data-vuln-type]'); 86 | 87 | if (!titleElement) { 88 | console.error("Cannot find title element in card"); 89 | DashboardApp.debug("Card structure:", card.innerHTML); 90 | return; 91 | } 92 | 93 | const {vulnType} = titleElement.dataset; 94 | DashboardApp.debug("Found vulnerability type:", vulnType); 95 | 96 | // Update dates for this model and vulnerability 97 | DashboardApp.updateDatesForModel(card, modelName, vulnType); 98 | }; 99 | 100 | DashboardApp.updateDatesForModel = function(card, modelName, vulnType) { 101 | DashboardApp.debug("Updating dates for model:", modelName, "vulnerability:", vulnType); 102 | 103 | // Show loading in the dates container 104 | const datesContainer = card.querySelector('.dates-list'); 105 | if (datesContainer) { 106 | datesContainer.innerHTML = '
'; 107 | } else { 108 | console.error("Cannot find dates container in card"); 109 | DashboardApp.debug("Card structure:", card.innerHTML); 110 | return; 111 | } 112 | 113 | // Fetch dates for the selected model and vulnerability 114 | fetch(`/api/dates?model=${encodeURIComponent(modelName)}&vulnerability=${encodeURIComponent(vulnType)}`) 115 | .then(response => { 116 | if (!response.ok) { 117 | throw new Error('Network response was not ok'); 118 | } 119 | return response.json(); 120 | }) 121 | .then(data => { 122 | // Rebuild dates with the received data 123 | if (datesContainer) { 124 | if (data.dates && data.dates.length > 0) { 125 | let datesHtml = ''; 126 | data.dates.forEach(dateInfo => { 127 | // Formatting the date and time 128 | if (dateInfo.date) { 129 | const dateObj = new Date(dateInfo.date); 130 | const formattedDate = dateObj.toLocaleDateString(); 131 | const formattedTime = dateObj.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 132 | 133 | // Get path and format 134 | const path = dateInfo.path || ''; 135 | const format = dateInfo.format || 'md'; 136 | 137 | datesHtml += ` 138 | 141 |
${formattedDate}
142 |
${formattedTime}
143 |
144 | `; 145 | } else { 146 | // Fallback 147 | const path = dateInfo.path || ''; 148 | const format = dateInfo.format || 'md'; 149 | 150 | datesHtml += ` 151 | 154 |
No date
155 |
156 | `; 157 | } 158 | }); 159 | datesContainer.innerHTML = datesHtml; 160 | } else { 161 | datesContainer.innerHTML = 'No dates available for this model'; 162 | } 163 | } 164 | }) 165 | .catch(error => { 166 | console.error('Error fetching dates:', error); 167 | if (datesContainer) { 168 | datesContainer.innerHTML = `Error loading dates: ${error.message}`; 169 | } 170 | }); 171 | }; 172 | 173 | DashboardApp.updateDatesForVulnerability = function(vulnElement, vulnType) { 174 | DashboardApp.debug("Updating dates for vulnerability:", vulnType); 175 | 176 | // Get the containing section 177 | const section = vulnElement.closest('.tree-section'); 178 | 179 | if (!section || !vulnType) { 180 | console.error("Cannot update dates: missing vulnerability or section"); 181 | return; 182 | } 183 | 184 | // Show loading in the dates container 185 | const datesContainer = section.querySelector('.tree-dates-list'); 186 | if (datesContainer) { 187 | datesContainer.innerHTML = '
'; 188 | } 189 | 190 | // Fetch dates for the selected vulnerability 191 | fetch(`/api/dates?vulnerability=${encodeURIComponent(vulnType)}`) 192 | .then(response => { 193 | if (!response.ok) { 194 | throw new Error('Network response was not ok'); 195 | } 196 | return response.json(); 197 | }) 198 | .then(data => { 199 | // Rebuild dates with the received data 200 | if (datesContainer) { 201 | if (data.dates && data.dates.length > 0) { 202 | let datesHtml = ''; 203 | data.dates.forEach(dateInfo => { 204 | // Formatting the date and time 205 | if (dateInfo.date) { 206 | const dateObj = new Date(dateInfo.date); 207 | const formattedDate = dateObj.toLocaleDateString(); 208 | const formattedTime = dateObj.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 209 | 210 | const modelName = dateInfo.model || 'Unknown'; 211 | const path = dateInfo.path || ''; 212 | const format = dateInfo.format || 'md'; // par défaut à md 213 | 214 | datesHtml += ` 215 | 218 |
${modelName}:
219 |
${formattedDate}
220 |
${formattedTime}
221 |
222 | `; 223 | } else { 224 | // Fallback 225 | const modelName = dateInfo.model || 'Unknown'; 226 | const path = dateInfo.path || ''; 227 | const format = dateInfo.format || 'md'; 228 | 229 | datesHtml += ` 230 | 233 |
${modelName}:
234 |
No date
235 |
236 | `; 237 | } 238 | }); 239 | datesContainer.innerHTML = datesHtml; 240 | } else { 241 | datesContainer.innerHTML = 'No dates available for this vulnerability'; 242 | } 243 | } 244 | }) 245 | .catch(error => { 246 | console.error('Error fetching dates:', error); 247 | if (datesContainer) { 248 | datesContainer.innerHTML = `Error loading dates: ${error.message}`; 249 | } 250 | }); 251 | }; 252 | 253 | DashboardApp.debug("Interactions module loaded"); -------------------------------------------------------------------------------- /oasis/static/js/dashboard/modal.js: -------------------------------------------------------------------------------- 1 | DashboardApp.ensureModalStyles = function() { 2 | // Check if styles already exist 3 | if (document.getElementById('modal-dynamic-styles')) { 4 | return; 5 | } 6 | 7 | // Create style element 8 | const style = document.createElement('style'); 9 | style.id = 'modal-dynamic-styles'; 10 | 11 | // Define basic modal styles 12 | style.textContent = ` 13 | `; 14 | 15 | // Add to document 16 | document.head.appendChild(style); 17 | DashboardApp.debug("Modal styles added dynamically"); 18 | }; 19 | 20 | DashboardApp.openReport = function(path, format) { 21 | DashboardApp.debug("Opening report:", path, format); 22 | if (!path) { 23 | console.error("No path provided for report"); 24 | return; 25 | } 26 | 27 | // Store current report info 28 | DashboardApp.currentReportPath = path; 29 | DashboardApp.currentReportFormat = format; 30 | 31 | // Get the modal elements from dashboard.html, using the IDs de l'ancien code 32 | const modal = document.getElementById('report-modal'); 33 | const modalTitle = document.getElementById('report-modal-title'); 34 | const modalContent = document.getElementById('report-modal-content'); 35 | const downloadOptions = document.getElementById('download-options'); 36 | 37 | if (!modal) { 38 | console.error("Modal element not found"); 39 | return; 40 | } 41 | 42 | if (!modalContent) { 43 | console.error("Modal content element not found"); 44 | return; 45 | } 46 | 47 | if (!modalTitle) { 48 | console.error("Modal title element not found"); 49 | return; 50 | } 51 | 52 | // Show loading indicator 53 | DashboardApp.showLoading('report-modal-content'); 54 | 55 | // Show the modal using CSS classes comme dans l'ancien code 56 | modal.classList.add('visible'); 57 | 58 | // Extract report name from path 59 | const pathParts = path.split('/'); 60 | const fileName = pathParts[pathParts.length - 1]; 61 | const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, ""); 62 | 63 | // Determine vulnerability type from filename 64 | const vulnType = fileNameWithoutExt.replace(/_/g, ' ') 65 | .split(' ') 66 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 67 | .join(' '); 68 | 69 | modalTitle.textContent = vulnType; 70 | 71 | // Fetch report content based on format 72 | if (format === 'md') { 73 | // Use the API endpoint for markdown content 74 | fetch(`/api/report-content/${encodeURIComponent(path)}`) 75 | .then(response => { 76 | if (!response.ok) { 77 | throw new Error(`HTTP error: ${response.status}`); 78 | } 79 | return response.json(); 80 | }) 81 | .then(data => { 82 | if (data.content) { 83 | // The content is already HTML, just insert it 84 | modalContent.innerHTML = data.content; 85 | } else { 86 | modalContent.innerHTML = '
Unable to load report content.
'; 87 | } 88 | DashboardApp.hideLoading('report-modal-content'); 89 | }) 90 | .catch(error => { 91 | console.error('Error fetching report content:', error); 92 | modalContent.innerHTML = `
Error loading report content: ${error.message}
`; 93 | DashboardApp.hideLoading('report-modal-content'); 94 | }); 95 | } else if (format === 'html') { 96 | // Load the HTML content via AJAX 97 | fetch(`/reports/${encodeURIComponent(path)}`) 98 | .then(response => { 99 | if (!response.ok) { 100 | throw new Error(`HTTP error: ${response.status}`); 101 | } 102 | return response.text(); 103 | }) 104 | .then(htmlContent => { 105 | // Create a div to contain the HTML content 106 | const htmlContainer = document.createElement('div'); 107 | htmlContainer.className = 'html-content-container'; 108 | htmlContainer.innerHTML = htmlContent; 109 | 110 | // Remove elements that could break the layout 111 | const elementsToRemove = htmlContainer.querySelectorAll('html, head, body, script'); 112 | elementsToRemove.forEach(el => { 113 | if (el.tagName.toLowerCase() === 'body') { 114 | // For body, we want to keep its content 115 | const bodyContent = el.innerHTML; 116 | el.parentNode.innerHTML = bodyContent; 117 | } else { 118 | el.remove(); 119 | } 120 | }); 121 | 122 | // Inject the content 123 | modalContent.innerHTML = ''; 124 | modalContent.appendChild(htmlContainer); 125 | 126 | // Add a style to ensure the content is displayed properly 127 | const style = document.createElement('style'); 128 | style.textContent = ` 129 | .html-content-container { 130 | width: 100%; 131 | max-height: calc(100vh - 200px); 132 | overflow-y: auto; 133 | } 134 | `; 135 | modalContent.appendChild(style); 136 | 137 | DashboardApp.hideLoading('report-modal-content'); 138 | }) 139 | .catch(error => { 140 | console.error('Error fetching HTML content:', error); 141 | modalContent.innerHTML = `
Error loading HTML content: ${error.message}
`; 142 | DashboardApp.hideLoading('report-modal-content'); 143 | }); 144 | } else if (format === 'pdf') { 145 | // Create a responsive container for the PDF 146 | const pdfContainer = document.createElement('div'); 147 | pdfContainer.className = 'pdf-container'; 148 | pdfContainer.style.cssText = 'width: 100%; height: calc(100vh - 200px); position: relative;'; 149 | 150 | const embed = document.createElement('embed'); 151 | embed.src = `/reports/${encodeURIComponent(path)}`; 152 | embed.type = 'application/pdf'; 153 | embed.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;'; 154 | 155 | pdfContainer.appendChild(embed); 156 | modalContent.innerHTML = ''; 157 | modalContent.appendChild(pdfContainer); 158 | 159 | // Ajustements pour l'Observateur de redimensionnement 160 | if (DashboardApp.currentResizeObserver) { 161 | DashboardApp.currentResizeObserver.disconnect(); 162 | } 163 | 164 | // Créer un nouvel observateur pour ajuster la taille 165 | if (typeof ResizeObserver !== 'undefined') { 166 | DashboardApp.currentResizeObserver = new ResizeObserver(() => { 167 | pdfContainer.style.height = 'calc(100vh - 200px)'; 168 | }); 169 | DashboardApp.currentResizeObserver.observe(modal); 170 | } 171 | 172 | DashboardApp.hideLoading('report-modal-content'); 173 | } else { 174 | modalContent.innerHTML = `
This format (${format.toUpperCase()}) cannot be displayed directly. Use the download option.
`; 175 | DashboardApp.hideLoading('report-modal-content'); 176 | } 177 | 178 | // Download options - restaurer le code pour les options de téléchargement 179 | if (downloadOptions) { 180 | const basePath = path.substring(0, path.lastIndexOf('.')); 181 | // Extract the base path without the current format 182 | const formatPattern = /\/(md|html|pdf)\//; 183 | const match = path.match(formatPattern); 184 | let currentFormat = 'md'; 185 | 186 | if (match) { 187 | currentFormat = match[1]; 188 | } 189 | 190 | // Create the buttons with the correct paths 191 | let downloadHtml = ''; 192 | const formats = { 193 | '📝': 'MD', 194 | '🌐': 'HTML', 195 | '📄': 'PDF' 196 | }; 197 | 198 | Object.keys(formats).forEach(fmt => { 199 | // Replace the format in the path 200 | const formattedPath = basePath.replace(`/${currentFormat}/`, `/${formats[fmt].toLowerCase()}/`) + `.${formats[fmt].toLowerCase()}`; 201 | downloadHtml += ``; 203 | }); 204 | 205 | downloadOptions.innerHTML = downloadHtml; 206 | } 207 | }; 208 | 209 | // Function to download a report 210 | DashboardApp.downloadReportFile = function(path, format) { 211 | window.open(`/api/download?path=${encodeURIComponent(path)}`, '_blank'); 212 | }; 213 | 214 | // Function to close the modal 215 | DashboardApp.closeReportModal = function() { 216 | DashboardApp.debug("Closing report modal"); 217 | // Get the modal 218 | const modal = document.getElementById('report-modal'); 219 | 220 | if (modal) { 221 | modal.classList.remove('visible'); 222 | } 223 | 224 | // Clean up any resize observer 225 | if (DashboardApp.currentResizeObserver) { 226 | DashboardApp.currentResizeObserver.disconnect(); 227 | DashboardApp.currentResizeObserver = null; 228 | } 229 | 230 | // Reset current report info 231 | DashboardApp.currentReportPath = ''; 232 | DashboardApp.currentReportFormat = ''; 233 | }; 234 | 235 | // Helper function to convert Markdown to HTML 236 | DashboardApp.convertMarkdownToHtml = function(markdown) { 237 | if (!markdown) { 238 | return '

Empty content

'; 239 | } 240 | 241 | try { 242 | // Check if marked is available 243 | if (typeof marked !== 'undefined') { 244 | return marked(markdown); 245 | } else { 246 | // Simple fallback if marked.js is not loaded 247 | return markdown 248 | .replace(/\n/g, '
') 249 | .replace(/\*\*(.*?)\*\*/g, '$1') 250 | .replace(/\*(.*?)\*/g, '$1'); 251 | } 252 | } catch (error) { 253 | console.error('Error converting Markdown:', error); 254 | return `
${markdown.replace(//g, '>')}
`; 255 | } 256 | }; 257 | 258 | DashboardApp.setupModalResize = function() { 259 | DashboardApp.debug("Setting up modal resize observer"); 260 | // Clean up any existing observer 261 | if (DashboardApp.currentResizeObserver) { 262 | DashboardApp.currentResizeObserver.disconnect(); 263 | } 264 | 265 | // Get the modal content element 266 | const modalContent = document.getElementById('report-modal-content'); 267 | 268 | if (!modalContent) { 269 | console.error("Modal content element not found"); 270 | return; 271 | } 272 | 273 | // Create a new resize observer if supported by the browser 274 | if (typeof ResizeObserver !== 'undefined') { 275 | DashboardApp.currentResizeObserver = new ResizeObserver(entries => { 276 | for (let entry of entries) { 277 | // Adjust content layout if needed based on width 278 | const {width} = entry.contentRect; 279 | 280 | // Add/remove responsive classes based on width 281 | if (width < 768) { 282 | modalContent.classList.add('modal-content-small'); 283 | } else { 284 | modalContent.classList.remove('modal-content-small'); 285 | } 286 | } 287 | }); 288 | 289 | // Start observing the modal content 290 | DashboardApp.currentResizeObserver.observe(modalContent); 291 | } 292 | }; 293 | 294 | // Initializing modal related event listeners 295 | DashboardApp.initializeModalEvents = function() { 296 | DashboardApp.debug("Initializing modal events"); 297 | 298 | // Add event listeners to close buttons in the modal 299 | const closeButtons = document.querySelectorAll('#report-modal .close'); 300 | closeButtons.forEach(button => { 301 | button.addEventListener('click', DashboardApp.closeReportModal); 302 | }); 303 | 304 | // Allow closing the modal by clicking outside 305 | const modal = document.getElementById('report-modal'); 306 | if (modal) { 307 | modal.addEventListener('click', function(event) { 308 | // Only close if clicking directly on the modal background 309 | if (event.target === modal) { 310 | DashboardApp.closeReportModal(); 311 | } 312 | }); 313 | } 314 | }; 315 | 316 | DashboardApp.debug("Modal module loaded"); -------------------------------------------------------------------------------- /oasis/static/js/dashboard/utils.js: -------------------------------------------------------------------------------- 1 | // Utility functions for the dashboard 2 | DashboardApp.groupReportsByModelAndVuln = function(reports) { 3 | DashboardApp.debug("Grouping reports by model and vulnerability"); 4 | return reports.map(report => { 5 | // Extraction of important properties 6 | const { model, vulnerability_type, path, date, format, stats, alternative_formats, date_visible } = report; 7 | 8 | // Construction of a simplified report 9 | return { 10 | model, 11 | vulnerability_type, 12 | path, 13 | date, 14 | format, 15 | date_visible: date_visible !== undefined ? date_visible : true, 16 | stats: stats || { high_risk: 0, medium_risk: 0, low_risk: 0, total: 0 }, 17 | alternative_formats: alternative_formats || {} 18 | }; 19 | }); 20 | }; 21 | 22 | DashboardApp.formatDisplayName = function(name, type, emoji = true) { 23 | if (!name) { 24 | return 'Unknown'; 25 | } 26 | 27 | if (type === 'format') { 28 | return name.toUpperCase(); 29 | } 30 | 31 | let formattedName = name; 32 | if (type === 'model') { 33 | if (emoji) { 34 | formattedName = DashboardApp.getModelEmoji(name) + ' ' + name; 35 | } else { 36 | formattedName = name; 37 | } 38 | } 39 | 40 | if (type === 'vulnerability') { 41 | if (emoji) { 42 | const lowered_name = name.toLowerCase().replace(/ /g, '_'); 43 | formattedName = DashboardApp.getVulnerabilityEmoji(lowered_name) + ' ' + name; 44 | } else { 45 | formattedName = name; 46 | } 47 | } 48 | 49 | // For vulnerability types and models 50 | return formattedName 51 | .replace(/_/g, ' ') 52 | .split(' ') 53 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 54 | .join(' '); 55 | }; 56 | 57 | DashboardApp.getModelEmoji = function(model) { 58 | // Try to match by prefix 59 | for (const [key, emoji] of Object.entries(modelEmojis)) { 60 | // Check if model starts with key or key starts with model 61 | if (model.toLowerCase().startsWith(key.toLowerCase()) || 62 | key.toLowerCase().startsWith(model.toLowerCase())) { 63 | return emoji + ' '; 64 | } 65 | } 66 | 67 | // Default emoji if no match found 68 | return '🤖 '; 69 | }; 70 | 71 | DashboardApp.getVulnerabilityEmoji = function(vulnerability) { 72 | // Try to match by prefix 73 | for (const [key, emoji] of Object.entries(vulnEmojis)) { 74 | if (vulnerability.toLowerCase().startsWith(key.toLowerCase()) || 75 | key.toLowerCase().startsWith(vulnerability.toLowerCase())) { 76 | return emoji + ' '; 77 | } 78 | } 79 | 80 | // Default emoji if no match found 81 | return '🔒 '; 82 | }; 83 | 84 | DashboardApp.debug("Utils module loaded"); -------------------------------------------------------------------------------- /oasis/static/js/dashboard/views.js: -------------------------------------------------------------------------------- 1 | // View rendering functions 2 | DashboardApp.renderCurrentView = function() { 3 | DashboardApp.debug("Rendering current view..."); 4 | DashboardApp.debug("Reports data:", DashboardApp.reportData.length); 5 | 6 | // This function renders the current view based on the view mode 7 | switch (DashboardApp.currentViewMode) { 8 | case 'tree-model': 9 | DashboardApp.renderTreeView('model'); 10 | break; 11 | case 'tree-vuln': 12 | DashboardApp.renderTreeView('vulnerability_type'); 13 | break; 14 | case 'list': 15 | default: 16 | DashboardApp.renderListView(); 17 | break; 18 | } 19 | 20 | // Update view mode buttons 21 | document.querySelectorAll('.view-tab').forEach(tab => { 22 | if (tab.dataset.mode === DashboardApp.currentViewMode) { 23 | tab.classList.add('active'); 24 | } else { 25 | tab.classList.remove('active'); 26 | } 27 | }); 28 | }; 29 | 30 | DashboardApp.renderTreeView = function(groupBy) { 31 | DashboardApp.debug("Rendering tree view..."); 32 | const container = document.getElementById('reports-container'); 33 | 34 | if (!container) { 35 | console.error("Reports container not found"); 36 | return; 37 | } 38 | 39 | if (DashboardApp.reportData.length === 0) { 40 | container.innerHTML = '
No reports match your filters. Please adjust your criteria.
'; 41 | return; 42 | } 43 | 44 | // Group reports based on the grouping criterion 45 | const grouped = {}; 46 | DashboardApp.reportData.forEach(report => { 47 | const key = report[groupBy]; 48 | if (!grouped[key]) { 49 | grouped[key] = []; 50 | } 51 | grouped[key].push(report); 52 | }); 53 | 54 | // Sort keys 55 | const sortedKeys = Object.keys(grouped).sort((a, b) => { 56 | // Put Executive Summary and Audit Report first for vulnerabilities 57 | if (groupBy === 'vulnerability_type') { 58 | if (a === 'Executive Summary') { 59 | return -1; 60 | } 61 | if (b === 'Executive Summary') { 62 | return 1; 63 | } 64 | if (a === 'Audit Report') { 65 | return -1; 66 | } 67 | if (b === 'Audit Report') { 68 | return 1; 69 | } 70 | } 71 | return a.localeCompare(b); 72 | }); 73 | 74 | let html = '
'; 75 | 76 | // For each group (model or vulnerability type) 77 | sortedKeys.forEach(key => { 78 | const reportsInGroup = grouped[key]; 79 | const formattedKey = DashboardApp.formatDisplayName(key, groupBy === 'model' ? 'model' : 'vulnerability'); 80 | 81 | html += ` 82 |
83 |
84 | ${formattedKey} (${reportsInGroup.length}) 85 |
86 |
87 | `; 88 | 89 | // For vulnerability-based tree, group by model within each vulnerability type 90 | if (groupBy === 'vulnerability_type') { 91 | // Group by model within this vulnerability 92 | const modelGroups = {}; 93 | reportsInGroup.forEach(report => { 94 | if (!modelGroups[report.model]) { 95 | modelGroups[report.model] = []; 96 | } 97 | modelGroups[report.model].push(report); 98 | }); 99 | 100 | // Sort models 101 | const sortedModels = Object.keys(modelGroups).sort(); 102 | 103 | sortedModels.forEach(model => { 104 | const reportsForModel = modelGroups[model]; 105 | const formattedModel = DashboardApp.formatDisplayName(model, 'model'); 106 | 107 | html += ` 108 |
109 |
110 |
${formattedModel}
111 |
112 |
113 | `; 114 | 115 | // Sort reports by date 116 | reportsForModel.sort((a, b) => new Date(b.date) - new Date(a.date)); 117 | 118 | // Add date entries 119 | reportsForModel.forEach(report => { 120 | const reportDate = report.date ? new Date(report.date) : null; 121 | const formattedDate = reportDate ? reportDate.toLocaleDateString() : 'No date'; 122 | const formattedTime = reportDate ? reportDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''; 123 | 124 | html += ` 125 | 129 |
${formattedDate}
130 |
${formattedTime}
131 |
132 | `; 133 | }); 134 | 135 | html += ` 136 |
137 |
138 | `; 139 | }); 140 | } 141 | // For model-based tree, group by vulnerability within each model 142 | else if (groupBy === 'model') { 143 | // Group by vulnerability within this model 144 | const vulnGroups = {}; 145 | reportsInGroup.forEach(report => { 146 | if (!vulnGroups[report.vulnerability_type]) { 147 | vulnGroups[report.vulnerability_type] = []; 148 | } 149 | vulnGroups[report.vulnerability_type].push(report); 150 | }); 151 | 152 | // Sort vulnerabilities - put Executive Summary and Audit Report first 153 | const sortedVulns = Object.keys(vulnGroups).sort((a, b) => { 154 | if (a === 'Executive Summary') { 155 | return -1; 156 | } 157 | if (b === 'Executive Summary') { 158 | return 1; 159 | } 160 | if (a === 'Audit Report') { 161 | return -1; 162 | } 163 | if (b === 'Audit Report') { 164 | return 1; 165 | } 166 | return a.localeCompare(b); 167 | }); 168 | 169 | sortedVulns.forEach(vuln => { 170 | const reportsForVuln = vulnGroups[vuln]; 171 | const formattedVuln = DashboardApp.formatDisplayName(vuln, 'vulnerability'); 172 | 173 | html += ` 174 |
175 |
176 |
${formattedVuln}
177 |
178 |
179 | `; 180 | 181 | // Sort reports by date 182 | reportsForVuln.sort((a, b) => new Date(b.date) - new Date(a.date)); 183 | 184 | // Add date entries 185 | reportsForVuln.forEach(report => { 186 | const reportDate = report.date ? new Date(report.date) : null; 187 | const formattedDate = reportDate ? reportDate.toLocaleDateString() : 'No date'; 188 | const formattedTime = reportDate ? reportDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''; 189 | 190 | html += ` 191 | 195 |
${formattedDate}
196 |
${formattedTime}
197 |
198 | `; 199 | }); 200 | 201 | html += ` 202 |
203 |
204 | `; 205 | }); 206 | } 207 | 208 | html += ` 209 |
210 |
211 | `; 212 | }); 213 | 214 | html += '
'; 215 | container.innerHTML = html; 216 | }; 217 | 218 | DashboardApp.renderListView = function() { 219 | DashboardApp.debug("Rendering list view..."); 220 | const container = document.getElementById('reports-container'); 221 | 222 | if (!container) { 223 | console.error("Reports container not found"); 224 | return; 225 | } 226 | 227 | if (DashboardApp.reportData.length === 0) { 228 | container.innerHTML = '
No reports match your filters. Please adjust your criteria.
'; 229 | return; 230 | } 231 | 232 | // Load the template if it's not already loaded 233 | if (!DashboardApp.cardTemplate) { 234 | fetch('/static/templates/dashboard_card.html') 235 | .then(response => response.text()) 236 | .then(template => { 237 | DashboardApp.cardTemplate = template; 238 | DashboardApp.renderListViewWithTemplate(); 239 | }) 240 | .catch(error => { 241 | console.error('Error loading template:', error); 242 | container.innerHTML = '
Error loading template. Please refresh the page.
'; 243 | }); 244 | } else { 245 | DashboardApp.renderListViewWithTemplate(); 246 | } 247 | }; 248 | 249 | DashboardApp.renderListViewWithTemplate = function() { 250 | DashboardApp.debug("Rendering list view with template..."); 251 | const container = document.getElementById('reports-container'); 252 | 253 | // Group by vulnerability type 254 | const vulnGroups = {}; 255 | DashboardApp.reportData.forEach(report => { 256 | if (!vulnGroups[report.vulnerability_type]) { 257 | vulnGroups[report.vulnerability_type] = []; 258 | } 259 | vulnGroups[report.vulnerability_type].push(report); 260 | }); 261 | 262 | // Sort vulnerability types - Ensure Executive Summary and Audit Report come first 263 | const sortedVulns = Object.keys(vulnGroups).sort((a, b) => { 264 | if (a === 'Executive Summary') { 265 | return -1; 266 | } 267 | if (b === 'Executive Summary') { 268 | return 1; 269 | } 270 | if (a === 'Audit Report') { 271 | return -1; 272 | } 273 | if (b === 'Audit Report') { 274 | return 1; 275 | } 276 | return a.localeCompare(b); 277 | }); 278 | 279 | let html = '
'; 280 | 281 | sortedVulns.forEach(vuln => { 282 | const reportsForVuln = vulnGroups[vuln]; 283 | const formattedVulnEmoji = DashboardApp.formatDisplayName(vuln, 'vulnerability'); 284 | const formattedVuln = DashboardApp.formatDisplayName(vuln, 'vulnerability', false); 285 | 286 | // Group models for this vulnerability 287 | const models = [...new Set(reportsForVuln.map(report => report.model))]; 288 | 289 | // Get report statistics 290 | const totalFindings = reportsForVuln.reduce((sum, r) => sum + (r.stats?.total || 0), 0); 291 | const highRisk = reportsForVuln.reduce((sum, r) => sum + (r.stats?.high_risk || 0), 0); 292 | const mediumRisk = reportsForVuln.reduce((sum, r) => sum + (r.stats?.medium_risk || 0), 0); 293 | const lowRisk = reportsForVuln.reduce((sum, r) => sum + (r.stats?.low_risk || 0), 0); 294 | 295 | // Determine format paths for buttons 296 | let mdPath = ''; 297 | let htmlPath = ''; 298 | let pdfPath = ''; 299 | 300 | // Get the latest report for each format 301 | reportsForVuln.sort((a, b) => new Date(b.date) - new Date(a.date)); 302 | const latestReport = reportsForVuln[0]; 303 | 304 | if (latestReport) { 305 | // Format paths 306 | mdPath = latestReport.format === 'md' ? latestReport.path : 307 | (latestReport.alternative_formats && latestReport.alternative_formats.md ? 308 | latestReport.alternative_formats.md : ''); 309 | 310 | htmlPath = latestReport.format === 'html' ? latestReport.path : 311 | (latestReport.alternative_formats && latestReport.alternative_formats.html ? 312 | latestReport.alternative_formats.html : ''); 313 | 314 | pdfPath = latestReport.format === 'pdf' ? latestReport.path : 315 | (latestReport.alternative_formats && latestReport.alternative_formats.pdf ? 316 | latestReport.alternative_formats.pdf : ''); 317 | } 318 | 319 | // Generate models HTML 320 | let modelsHTML = ''; 321 | models.forEach(model => { 322 | const formattedModel = DashboardApp.formatDisplayName(model, 'model'); 323 | 324 | modelsHTML += ` 325 | 328 | ${formattedModel} 329 | 330 | `; 331 | }); 332 | 333 | // Generate dates HTML 334 | let datesHTML = ''; 335 | reportsForVuln.sort((a, b) => new Date(b.date) - new Date(a.date)).forEach(report => { 336 | // Only show dates for MD reports 337 | if (report['date_visible']) { 338 | const reportDate = report.date ? new Date(report.date) : null; 339 | const formattedDate = reportDate ? reportDate.toLocaleDateString() : 'No date'; 340 | const formattedTime = reportDate ? reportDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''; 341 | 342 | datesHTML += ` 343 | 346 |
${formattedDate}
347 |
${formattedTime}
348 |
349 | `; 350 | } 351 | }); 352 | 353 | // Generate format buttons HTML 354 | let formatButtons = ''; 355 | if (pdfPath) { 356 | formatButtons += ``; 357 | } 358 | if (mdPath) { 359 | formatButtons += ``; 360 | } 361 | if (htmlPath) { 362 | formatButtons += ``; 363 | } 364 | 365 | // Use the template and replace placeholders 366 | let cardHTML = DashboardApp.cardTemplate 367 | .replace('${formattedVulnTypeEmoji}', formattedVulnEmoji) 368 | .replace('${formattedVulnType}', formattedVuln) 369 | .replace('${modelsHTML}', modelsHTML) 370 | .replace('${datesHTML}', datesHTML) 371 | .replace('${totalFindings}', totalFindings) 372 | .replace('${highRisk}', highRisk) 373 | .replace('${mediumRisk}', mediumRisk) 374 | .replace('${lowRisk}', lowRisk) 375 | .replace('${formatButtons}', formatButtons); 376 | 377 | html += cardHTML; 378 | }); 379 | 380 | html += '
'; 381 | container.innerHTML = html; 382 | }; 383 | 384 | DashboardApp.renderStats = function() { 385 | DashboardApp.debug("Rendering stats..."); 386 | const statsContainer = document.getElementById('stats-container'); 387 | 388 | if (!statsContainer) { 389 | console.error("Stats container not found"); 390 | return; 391 | } 392 | 393 | if (!DashboardApp.stats || !DashboardApp.stats.risk_summary) { 394 | console.error("Stats data not available"); 395 | return; 396 | } 397 | 398 | // Preparing risk data for display 399 | const totalRisks = DashboardApp.stats.risk_summary.high + DashboardApp.stats.risk_summary.medium + DashboardApp.stats.risk_summary.low || 1; 400 | const highPct = (DashboardApp.stats.risk_summary.high / totalRisks * 100) || 0; 401 | const mediumPct = (DashboardApp.stats.risk_summary.medium / totalRisks * 100) || 0; 402 | const lowPct = (DashboardApp.stats.risk_summary.low / totalRisks * 100) || 0; 403 | 404 | statsContainer.innerHTML = ` 405 |
406 |
407 |
📊 Reports
408 |
${DashboardApp.stats.total_reports || 0}
409 |
Reports generated
410 |
411 |
412 |
🤖 Models
413 |
${Object.keys(DashboardApp.stats.models || {}).length}
414 |
AI models used
415 |
416 |
417 |
🛡️ Vulnerability types
418 |
${Object.keys(DashboardApp.stats.vulnerabilities || {}).length}
419 |
Vulnerabilities analyzed
420 |
421 |
422 |
📈 Risk summary
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 | 🚨 ${DashboardApp.stats.risk_summary.high || 0} High 432 | ⚠️ ${DashboardApp.stats.risk_summary.medium || 0} Medium 433 | 📌 ${DashboardApp.stats.risk_summary.low || 0} Low 434 |
435 |
436 |
437 | `; 438 | }; 439 | 440 | DashboardApp.switchView = function(viewMode) { 441 | DashboardApp.debug("Switching view to:", viewMode); 442 | // Update active tab 443 | document.querySelectorAll('.view-tab').forEach(tab => { 444 | tab.classList.remove('active'); 445 | }); 446 | 447 | const viewTab = document.getElementById(`view-${viewMode}`); 448 | if (viewTab) { 449 | viewTab.classList.add('active'); 450 | } 451 | 452 | // Update view mode and render 453 | DashboardApp.currentViewMode = viewMode; 454 | DashboardApp.renderCurrentView(); 455 | }; 456 | 457 | DashboardApp.debug("Views module loaded"); -------------------------------------------------------------------------------- /oasis/static/templates/dashboard_card.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

${formattedVulnTypeEmoji}

4 |
5 |
6 |
7 |
Models:
8 |
9 | ${modelsHTML} 10 |
11 |
12 | 13 |
14 |
Dates:
15 |
16 | ${datesHTML} 17 |
18 |
19 | 20 |
21 |
22 | ${totalFindings} 23 | 🔍 Total 24 |
25 |
26 | ${highRisk} 27 | 🚨 High 28 |
29 |
30 | ${mediumRisk} 31 | ⚠️ Medium 32 |
33 |
34 | ${lowRisk} 35 | 📌 Low 36 |
37 |
38 | 39 |
40 | ${formatButtons} 41 |
42 |
43 |
-------------------------------------------------------------------------------- /oasis/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}OASIS{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% block head_extra %}{% endblock %} 15 | 16 | 17 | {% include "header.html" %} 18 | 19 | {% block content %}{% endblock %} 20 | 21 | {% include "footer.html" %} 22 | 23 | {% block scripts %}{% endblock %} 24 | 25 | -------------------------------------------------------------------------------- /oasis/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}OASIS - Security reports dashboard{% endblock %} 4 | 5 | {% block header_title %}OASIS Dashboard{% endblock %} 6 | 7 | {% block head_extra %} 8 | 13 | {% endblock %} 14 | 15 | {% block content %} 16 |
17 | 21 | 22 | 56 | 57 |
58 |
59 |
60 |
61 | 62 | 68 | 71 |
72 |
73 |
74 |
75 |
76 | 77 | 94 | {% endblock %} 95 | 96 | {% block scripts %} 97 | 98 | 99 | {% endblock %} -------------------------------------------------------------------------------- /oasis/templates/footer.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /oasis/templates/header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | OASIS Logo 5 |
6 |

{% block header_title %}OASIS{% endblock %}

7 |
8 | {% if session.get('logged_in') %} 9 |
10 | 🔄 Reload 11 | 🔑 Logout 12 |
13 | {% endif %} 14 |
-------------------------------------------------------------------------------- /oasis/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}OASIS - Login{% endblock %} 4 | 5 | {% block header_title %}Login{% endblock %} 6 | 7 | {% block content %} 8 |
9 | 25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /oasis/templates/report_styles.css: -------------------------------------------------------------------------------- 1 | /* Color palette based on logo */ 2 | :root { 3 | --dark-blue: #0e1f2b; 4 | --sand: #e6e0cc; 5 | --light-sand: #f5f2e9; 6 | --turquoise: #40b4c4; 7 | --light-turquoise: #7cd1de; 8 | --medium-gray: #7f8c8d; 9 | --light-gray: #f5f5f5; 10 | --border-color: #ddd; 11 | } 12 | 13 | @page { 14 | margin: 1cm; 15 | size: A4; 16 | @top-right { 17 | content: counter(page); 18 | } 19 | } 20 | 21 | /* Force page break - multiple approaches */ 22 | div[style*="page-break-after: always"], 23 | div.page-break { 24 | page-break-after: always !important; 25 | break-after: page !important; 26 | margin: 0 !important; 27 | padding: 0 !important; 28 | height: 0 !important; 29 | visibility: hidden !important; 30 | } 31 | 32 | body { 33 | font-family: Arial, sans-serif; 34 | font-size: 11pt; 35 | line-height: 1.5; 36 | max-width: none; 37 | margin: 0; 38 | padding: 30px; 39 | background-color: var(--light-sand); 40 | color: var(--dark-blue); 41 | } 42 | 43 | /* Header styles */ 44 | .report-header { 45 | display: flex; 46 | align-items: center; 47 | padding-bottom: 5px; 48 | border-bottom: 2px solid var(--turquoise); 49 | margin-bottom: 1cm; 50 | } 51 | 52 | .logo-container { 53 | margin-right: 20px; 54 | } 55 | 56 | .logo-circle { 57 | width: 80px; 58 | height: 80px; 59 | border-radius: 50%; 60 | background-color: #e1dcc8; 61 | display: flex; 62 | justify-content: center; 63 | align-items: center; 64 | position: relative; 65 | overflow: hidden; 66 | } 67 | 68 | .oasis-logo-img { 69 | width: 60px; 70 | height: 60px; 71 | object-fit: contain; 72 | margin-top: 4px; 73 | } 74 | 75 | .oasis-logo { 76 | width: 40px; 77 | height: 40px; 78 | background: linear-gradient(to bottom, var(--sand) 60%, var(--turquoise) 60%); 79 | clip-path: polygon(0 0, 100% 30%, 100% 100%, 0% 100%); 80 | position: relative; 81 | } 82 | 83 | .oasis-logo::after { 84 | content: ""; 85 | position: absolute; 86 | right: 0; 87 | top: 0; 88 | width: 15px; 89 | height: 15px; 90 | background-color: var(--dark-blue); 91 | clip-path: polygon(0 0, 100% 0, 100% 100%); 92 | } 93 | 94 | .report-title { 95 | font-size: 24pt; 96 | font-weight: bold; 97 | color: var(--dark-blue); 98 | } 99 | 100 | /* Main content */ 101 | .report-content { 102 | min-height: 400px; 103 | padding-bottom: 2cm; 104 | } 105 | 106 | /* Footer styles */ 107 | .report-footer { 108 | position: running(footer); 109 | display: flex; 110 | justify-content: space-between; 111 | padding-top: 10px; 112 | border-top: 1px solid var(--border-color); 113 | font-size: 9pt; 114 | color: var(--medium-gray); 115 | } 116 | 117 | .page-number::after { 118 | content: counter(page); 119 | } 120 | 121 | @page { 122 | @bottom-center { 123 | content: element(footer); 124 | } 125 | } 126 | 127 | /* Typography */ 128 | code { 129 | background-color: var(--light-gray); 130 | padding: 2px 4px; 131 | border-radius: 4px; 132 | font-family: monospace; 133 | font-size: 9pt; 134 | word-wrap: break-word; 135 | white-space: pre-wrap; 136 | border: 1px solid var(--border-color); 137 | } 138 | 139 | pre { 140 | background-color: var(--light-gray); 141 | padding: 1em; 142 | border-radius: 4px; 143 | margin: 1em 0; 144 | white-space: pre-wrap; 145 | word-wrap: break-word; 146 | font-size: 9pt; 147 | border: 1px solid var(--border-color); 148 | box-shadow: 0 1px 3px rgba(0,0,0,0.1); 149 | } 150 | 151 | h1 { 152 | color: var(--dark-blue); 153 | font-size: 20pt; 154 | margin-top: 0; 155 | padding-bottom: 0.2em; 156 | border-bottom: 1px solid var(--turquoise); 157 | } 158 | 159 | h2 { 160 | color: var(--dark-blue); 161 | font-size: 16pt; 162 | margin-top: 1.2em; 163 | padding-left: 0.3em; 164 | border-left: 4px solid var(--turquoise); 165 | } 166 | 167 | h3 { 168 | color: var(--dark-blue); 169 | font-size: 14pt; 170 | margin-top: 1em; 171 | } 172 | 173 | p { 174 | margin: 0.7em 0; 175 | } 176 | 177 | ul, ol { 178 | margin: 0.7em 0; 179 | padding-left: 2em; 180 | } 181 | 182 | /* Tables */ 183 | table { 184 | border-collapse: collapse; 185 | width: 100%; 186 | margin: 1.2em 0; 187 | box-shadow: 0 1px 3px rgba(0,0,0,0.1); 188 | background-color: white; 189 | } 190 | 191 | th, td { 192 | border: 1px solid var(--border-color); 193 | padding: 10px; 194 | text-align: left; 195 | } 196 | 197 | th { 198 | background-color: var(--dark-blue); 199 | color: white; 200 | font-weight: bold; 201 | } 202 | 203 | tr:nth-child(even) { 204 | background-color: var(--light-sand); 205 | } 206 | 207 | /* Risk colors */ 208 | .risk-high { color: #d73a49; font-weight: bold; } 209 | .risk-medium { color: #e36209; } 210 | .risk-low { color: #2cbe4e; } 211 | 212 | /* Summary boxes */ 213 | .summary-box { 214 | background-color: var(--light-turquoise); 215 | border-left: 4px solid var(--turquoise); 216 | padding: 15px; 217 | margin: 1em 0; 218 | border-radius: 0 4px 4px 0; 219 | } 220 | 221 | /* Detail sections */ 222 | .detail-section { 223 | background-color: white; 224 | border: 1px solid var(--border-color); 225 | border-radius: 4px; 226 | padding: 15px; 227 | margin: 1em 0; 228 | box-shadow: 0 1px 3px rgba(0,0,0,0.1); 229 | } 230 | -------------------------------------------------------------------------------- /oasis/templates/report_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 |
11 |
12 |
13 | OASIS Logo 14 |
15 |
16 |
OASIS Security Report
17 |
18 | 19 |
20 | {{ content|safe }} 21 |
22 | 23 | 26 | 27 | -------------------------------------------------------------------------------- /oasis/tools.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import logging 3 | from pathlib import Path 4 | import re 5 | import numpy as np 6 | from typing import List, Dict 7 | from weasyprint.logger import LOGGER as weasyprint_logger 8 | 9 | # Import configuration 10 | from .config import KEYWORD_LISTS, MODEL_EMOJIS, VULNERABILITY_MAPPING 11 | 12 | # Initialize logger with module name 13 | logger = logging.getLogger('oasis') 14 | 15 | # Suppress weasyprint warnings 16 | logging.getLogger('weasyprint').setLevel(logging.ERROR) 17 | 18 | class EmojiFormatter(logging.Formatter): 19 | """ 20 | Custom formatter that adds contextual emojis to log messages. 21 | 22 | Handles: 23 | - Automatic emoji detection 24 | - Level-based icons 25 | - Context-aware icons 26 | - Newline preservation 27 | """ 28 | 29 | @staticmethod 30 | def has_emoji_prefix(text: str) -> bool: 31 | emoji_ranges = [ 32 | (0x1F300, 0x1F9FF), # Misc Symbols & Pictographs 33 | (0x2600, 0x26FF), # Misc Symbols 34 | (0x2700, 0x27BF), # Dingbats 35 | (0x1F600, 0x1F64F), # Emoticons 36 | (0x1F680, 0x1F6FF), # Transport & Map Symbols 37 | ] 38 | if not text: 39 | return False 40 | first_char = text.strip()[0] 41 | code = ord(first_char) 42 | return any(start <= code <= end for start, end in emoji_ranges) 43 | 44 | def determine_icon(self, record) -> str: 45 | # Early returns for non-string messages or messages with emoji prefixes 46 | if not isinstance(record.msg, str) or self.has_emoji_prefix(record.msg.strip()): 47 | return '' 48 | 49 | msg_lower = record.msg.lower() 50 | 51 | # Level-based icons 52 | if record.levelno == logging.DEBUG: 53 | return '🪲 ' 54 | if record.levelno == logging.WARNING: 55 | return '⚠️ ' 56 | if record.levelno == logging.ERROR: 57 | return '💥 ' if any(word in msg_lower for word in KEYWORD_LISTS['FAIL_WORDS']) else '❌ ' 58 | if record.levelno == logging.CRITICAL: 59 | return '🚨 ' 60 | 61 | # INFO level processing - check for model names first 62 | if record.levelno == logging.INFO: 63 | # Check for model names first 64 | for model_name in MODEL_EMOJIS: 65 | if model_name.lower() in msg_lower: 66 | return '' 67 | 68 | # Map keyword categories to icons 69 | keyword_to_icon = { 70 | 'INSTALL_WORDS': '📥 ', 71 | 'START_WORDS': '🚀 ', 72 | 'FINISH_WORDS': '🏁 ', 73 | 'STOPPED_WORDS': '🛑 ', 74 | 'DELETE_WORDS': '🗑️ ', 75 | 'SUCCESS_WORDS': '✅ ', 76 | 'GENERATION_WORDS': '⚙️ ', 77 | 'REPORT_WORDS': '📄 ', 78 | 'MODEL_WORDS': '🤖 ', 79 | 'CACHE_WORDS': '💾 ', 80 | 'SAVE_WORDS': '💾 ', 81 | 'LOAD_WORDS': '📂 ', 82 | 'STATISTICS_WORDS': '📊 ', 83 | 'TOP_WORDS': '🏆 ', 84 | 'VULNERABILITY_WORDS': '🚨 ', 85 | 'ANALYSIS_WORDS': '🔎 ', 86 | } 87 | 88 | # Check each category and return the first matching icon 89 | for category, icon in keyword_to_icon.items(): 90 | if any(word in msg_lower for word in KEYWORD_LISTS[category]): 91 | return icon 92 | 93 | # Default: no icon 94 | return '' 95 | 96 | def format(self, record): 97 | if hasattr(record, 'emoji') and not record.emoji: 98 | return record.msg 99 | if not hasattr(record, 'formatted_message'): 100 | icon = self.determine_icon(record) 101 | if record.msg.startswith('\n'): 102 | record.formatted_message = record.msg.replace('\n', f'\n{icon}', 1) 103 | else: 104 | record.formatted_message = f"{icon}{record.msg}" 105 | return record.formatted_message 106 | 107 | def setup_logging(debug=False, silent=False, error_log_file=None): 108 | """ 109 | Setup all loggers with proper configuration 110 | 111 | Args: 112 | debug: Enable debug logging 113 | silent: Disable all output 114 | error_log_file: Path to error log file (used only in silent mode) 115 | """ 116 | # Set root logger level 117 | root_logger = logging.getLogger() 118 | 119 | # Avoid adding duplicate handlers if they already exist. 120 | if root_logger.handlers: 121 | return 122 | 123 | if debug: 124 | root_logger.setLevel(logging.DEBUG) 125 | else: 126 | root_logger.setLevel(logging.INFO) 127 | 128 | # Configure handlers based on silent mode 129 | if not silent: 130 | console_handler = logging.StreamHandler() 131 | console_handler.setFormatter(EmojiFormatter()) 132 | logger.addHandler(console_handler) 133 | 134 | # Add file handler for errors in silent mode 135 | if silent and error_log_file: 136 | file_handler = logging.FileHandler(error_log_file) 137 | file_handler.setLevel(logging.ERROR) # Only log errors and above 138 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 139 | file_handler.setFormatter(formatter) 140 | logger.addHandler(file_handler) 141 | 142 | logger.propagate = False # Prevent duplicate logging 143 | 144 | # Set OASIS logger level based on mode 145 | if silent and not error_log_file: 146 | logger.setLevel(logging.CRITICAL + 1) # Above all levels (complete silence) 147 | elif silent: 148 | logger.setLevel(logging.ERROR) # Only errors and above if logging to file 149 | elif debug: 150 | logger.setLevel(logging.DEBUG) # Show all messages 151 | else: 152 | logger.setLevel(logging.INFO) # Show info, warning, error 153 | 154 | # Configure other loggers 155 | fonttools_logger = logging.getLogger('fontTools') 156 | fonttools_logger.setLevel(logging.ERROR) 157 | 158 | weasyprint_logger.setLevel(logging.ERROR) 159 | 160 | # Disable other verbose loggers 161 | logging.getLogger('PIL').setLevel(logging.WARNING) 162 | logging.getLogger('markdown').setLevel(logging.WARNING) 163 | 164 | def chunk_content(content: str, max_length: int = 2048) -> List[str]: 165 | """ 166 | Split content into chunks of maximum length while preserving line integrity 167 | 168 | Args: 169 | content: Text content to split 170 | max_length: Maximum length of each chunk (reduced to 2048 to be safe) 171 | Returns: 172 | List of content chunks 173 | """ 174 | if len(content) <= max_length: 175 | return [content] 176 | 177 | chunks = [] 178 | lines = content.splitlines() 179 | current_chunk = [] 180 | current_length = 0 181 | 182 | for line in lines: 183 | line_length = len(line) + 1 # +1 for newline 184 | if current_length + line_length > max_length: 185 | if current_chunk: 186 | chunks.append('\n'.join(current_chunk)) 187 | current_chunk = [line] 188 | current_length = line_length 189 | else: 190 | current_chunk.append(line) 191 | current_length += line_length 192 | 193 | if current_chunk: 194 | chunks.append('\n'.join(current_chunk)) 195 | 196 | logger.debug(f"Split content of {len(content)} chars into {len(chunks)} chunks") 197 | 198 | return chunks 199 | 200 | def extract_clean_path(input_path: str | Path) -> Path: 201 | """ 202 | Extract a clean path from input that might contain additional arguments 203 | 204 | Args: 205 | input_path: Path string or Path object potentially containing additional arguments 206 | 207 | Returns: 208 | Clean path in the same format as input (Path object or string) 209 | """ 210 | # Determine input type to preserve it for output 211 | is_path_object = isinstance(input_path, Path) 212 | 213 | # Convert to string for processing 214 | input_path_str = str(input_path) 215 | 216 | # Extract the actual path before any arguments 217 | path_parts = input_path_str.split() 218 | actual_path = path_parts[0] if path_parts else input_path_str 219 | 220 | # Handle quoted paths (remove quotes if present) 221 | if actual_path.startswith('"') and actual_path.endswith('"'): 222 | actual_path = actual_path[1:-1] 223 | elif actual_path.startswith("'") and actual_path.endswith("'"): 224 | actual_path = actual_path[1:-1] 225 | 226 | logger.debug(f"Extracted clean path: {actual_path} from input: {input_path_str}") 227 | 228 | # Return in the same format as input 229 | return Path(actual_path) if is_path_object else actual_path 230 | 231 | def parse_input(input_path: str | Path) -> List[Path]: 232 | """ 233 | Parse input path and return list of files to analyze 234 | 235 | Args: 236 | input_path: Path to file, directory, or file containing paths 237 | Returns: 238 | List of Path objects to analyze 239 | """ 240 | # Get clean path without arguments, and ensure it's a Path object 241 | clean_path_str = extract_clean_path(input_path) 242 | input_path = Path(clean_path_str) # Convert to Path object for processing 243 | 244 | files_to_analyze = [] 245 | 246 | # Case 1: Input is a file containing paths 247 | if input_path.suffix == '.txt': 248 | try: 249 | with open(input_path, 'r') as f: 250 | paths = [line.strip() for line in f if line.strip()] 251 | for path in paths: 252 | p = Path(path) 253 | if p.is_file(): 254 | files_to_analyze.append(p) 255 | elif p.is_dir(): 256 | files_to_analyze.extend( 257 | f for f in p.rglob('*') 258 | if f.is_file() 259 | ) 260 | except Exception as e: 261 | logger.exception(f"Error reading paths file: {str(e)}") 262 | return [] 263 | 264 | # Case 2: Input is a single file 265 | elif input_path.is_file(): 266 | files_to_analyze.append(input_path) 267 | 268 | # Case 3: Input is a directory 269 | elif input_path.is_dir(): 270 | files_to_analyze.extend( 271 | f for f in input_path.rglob('*') 272 | if f.is_file() 273 | ) 274 | 275 | else: 276 | logger.error(f"Invalid input path: {input_path}") 277 | return [] 278 | 279 | return files_to_analyze 280 | 281 | def sanitize_name(string: str) -> str: 282 | """ 283 | Sanitize string for file name creation 284 | 285 | Args: 286 | string: Original string to be sanitized for file naming 287 | """ 288 | # Get the last part after the last slash (if any) 289 | base_name = string.split('/')[-1] 290 | return re.sub(r'[^a-zA-Z0-9]', '_', base_name) 291 | 292 | def display_logo(): 293 | """ 294 | Display the OASIS logo 295 | """ 296 | logo = """ 297 | .d88b. db .d8888. _\\\\|//_ .d8888. 298 | .8P Y8. d88b 88' YP \\\\// 88' YP 299 | 88 88 d8'`8b `8bo. || `8bo. 300 | 88 88 88ooo88 `Y8b. || `Y8b. 301 | `8b d8' 88~~~88 db 8D /||\\ db 8D 302 | `Y88P' YP YP `8888Y' __/_||_\\_ `8888Y' 303 | 304 | ╔════════════════════════════════════════════════╗ 305 | ║ Ollama Automated Security Intelligence Scanner ║ 306 | ╚════════════════════════════════════════════════╝ 307 | """ 308 | logger.info(logo) 309 | 310 | def calculate_similarity(embedding1: List[float], embedding2: List[float]) -> float: 311 | """ 312 | Calculate cosine similarity between two embeddings 313 | 314 | Args: 315 | embedding1: First embedding vector 316 | embedding2: Second embedding vector 317 | Returns: 318 | Cosine similarity score (0-1) 319 | """ 320 | # Convert to numpy arrays for efficient computation 321 | vec1 = np.array(embedding1) 322 | vec2 = np.array(embedding2) 323 | 324 | # Calculate cosine similarity 325 | dot_product = np.dot(vec1, vec2) 326 | norm1 = np.linalg.norm(vec1) 327 | norm2 = np.linalg.norm(vec2) 328 | 329 | if norm1 == 0 or norm2 == 0: 330 | return 0.0 331 | 332 | return float(dot_product / (norm1 * norm2)) 333 | 334 | def open_file(file_path: str) -> str: 335 | """ 336 | Open a file and return its content 337 | 338 | Args: 339 | file_path: Path to the file 340 | Returns: 341 | Content of the file 342 | """ 343 | # Try different encodings 344 | encodings = ['utf-8', 'latin-1', 'cp1252', 'iso-8859-1'] 345 | content = None 346 | 347 | errors = [] 348 | for encoding in encodings: 349 | try: 350 | with open(file_path, 'r', encoding=encoding) as f: 351 | content = f.read() 352 | break 353 | except UnicodeDecodeError: 354 | errors.append(f"Failed to decode with {encoding}") 355 | continue 356 | except Exception as e: 357 | error_msg = f"Error reading {file_path} with {encoding}: {e.__class__.__name__}: {str(e)}" 358 | logger.exception(error_msg) 359 | errors.append(error_msg) 360 | continue 361 | 362 | if content is None: 363 | error_details = "; ".join(errors) 364 | logger.error(f"Failed to read {file_path}: Tried encodings {', '.join(encodings)}. Errors: {error_details}") 365 | return None 366 | 367 | return content 368 | 369 | def get_vulnerability_mapping() -> Dict[str, Dict[str, any]]: 370 | """ 371 | Return the vulnerability mapping 372 | 373 | Returns: 374 | Vulnerability mapping 375 | """ 376 | return VULNERABILITY_MAPPING 377 | 378 | def generate_timestamp(for_file: bool = False) -> str: 379 | """ 380 | Generate a timestamp in the format YYYY-MM-DD HH:MM:SS 381 | """ 382 | if for_file: 383 | return datetime.now().strftime("%Y%m%d_%H%M%S") 384 | else: 385 | return datetime.now().strftime("%Y-%m-%d %H:%M:%S") 386 | 387 | def parse_iso_date(date_string): 388 | """ 389 | Parse ISO format date string with error handling 390 | Returns datetime object with UTC timezone or None if parsing fails 391 | """ 392 | if not date_string: 393 | return None 394 | 395 | try: 396 | # Handle 'Z' UTC indicator in ISO format 397 | if date_string.endswith('Z'): 398 | date_string = date_string.replace('Z', '+00:00') 399 | 400 | # Parse ISO format date string 401 | return datetime.fromisoformat(date_string) 402 | except (ValueError, TypeError) as e: 403 | print(f"Error parsing date '{date_string}': {e}") 404 | return None 405 | 406 | def parse_report_date(date_string): 407 | """ 408 | Parse report date string with error handling 409 | Returns datetime object with UTC timezone or None if parsing fails 410 | """ 411 | if not date_string: 412 | return None 413 | 414 | try: 415 | # Parse date in format used by reports 416 | dt = datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S') 417 | # Add UTC timezone if not present 418 | if dt.tzinfo is None: 419 | from datetime import timezone 420 | dt = dt.replace(tzinfo=timezone.utc) 421 | return dt 422 | except (ValueError, TypeError) as e: 423 | print(f"Error parsing report date '{date_string}': {e}") 424 | return None 425 | 426 | def create_cache_dir(input_path: str | Path) -> Path: 427 | """ 428 | Create a cache directory for the input path 429 | """ 430 | # Create base cache directory 431 | input_path = Path(input_path).resolve() # Get absolute path 432 | base_cache_dir = input_path.parent / '.oasis_cache' 433 | base_cache_dir.mkdir(exist_ok=True) 434 | 435 | # Create project-specific cache directory using the final folder name 436 | project_name = sanitize_name(input_path.name) 437 | cache_dir = base_cache_dir / project_name 438 | cache_dir.mkdir(exist_ok=True) 439 | return cache_dir -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "oasis" 7 | version = "0.4.0" 8 | description = "Ollama Automated Security Intelligence Scanner" 9 | readme = "README.md" 10 | requires-python = ">=3.9" 11 | license = "GPL-3.0-only" 12 | authors = [ 13 | { name = "psyray" } 14 | ] 15 | dependencies = [ 16 | "ollama>=0.4.7", 17 | "weasyprint>=60.1", 18 | "markdown>=3.7", 19 | "tqdm>=4.67.1", 20 | "numpy>=2.0.2", 21 | "fonttools>=4.56.0", 22 | "httpx>=0.28.1", 23 | "jinja2>=3.1.6", 24 | "beautifulsoup4>=4.13.3", 25 | "flask>=3.1.0", 26 | ] 27 | 28 | [project.scripts] 29 | oasis = "oasis:main" 30 | 31 | [tool.hatch.build.targets.wheel] 32 | packages = ["oasis"] -------------------------------------------------------------------------------- /test_files/Vulnerable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.SqlClient; 3 | using System.Web; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using System.IO; 7 | using System.Net; 8 | using System.Diagnostics; 9 | using System.Xml; 10 | using System.Reflection; 11 | 12 | public class VulnerableCode 13 | { 14 | // SQL Injection vulnerability 15 | public string GetUserData(string username) 16 | { 17 | using (SqlConnection conn = new SqlConnection("connection_string")) 18 | { 19 | // Vulnerable: String concatenation in SQL 20 | string query = "SELECT * FROM Users WHERE Username = '" + username + "'"; 21 | SqlCommand cmd = new SqlCommand(query, conn); 22 | return cmd.ExecuteScalar()?.ToString(); 23 | } 24 | } 25 | 26 | // XSS vulnerability 27 | public string RenderUserInput(string input) 28 | { 29 | // Vulnerable: Direct HTML output 30 | return $"
{input}
"; 31 | } 32 | 33 | // Insufficient Input Validation 34 | public int ProcessAge(string age) 35 | { 36 | // Vulnerable: No validation 37 | return int.Parse(age) * 12; 38 | } 39 | 40 | // Sensitive Data Exposure 41 | public void LogUserData(string username, string password) 42 | { 43 | // Vulnerable: Logging sensitive data 44 | Console.WriteLine($"User created - Username: {username}, Password: {password}"); 45 | } 46 | 47 | // Session Management Issues 48 | private static Dictionary sessions = new Dictionary(); 49 | public string CreateSession(string userId) 50 | { 51 | // Vulnerable: Predictable session token 52 | string token = userId + DateTime.Now.Ticks; 53 | sessions[userId] = token; 54 | return token; 55 | } 56 | 57 | // Security Misconfiguration 58 | private const string AdminPassword = "admin123"; // Vulnerable: Hardcoded credential 59 | 60 | // Sensitive Data Logging 61 | public void ProcessPayment(string creditCard) 62 | { 63 | // Vulnerable: Logging sensitive data 64 | Console.WriteLine($"Processing payment with card: {creditCard}"); 65 | } 66 | 67 | // Insecure Cryptographic Usage 68 | public string HashPassword(string password) 69 | { 70 | // Vulnerable: Using weak hashing 71 | using (MD5 md5 = MD5.Create()) 72 | { 73 | byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(password)); 74 | return Convert.ToBase64String(hash); 75 | } 76 | } 77 | 78 | // Remote Code Execution (RCE) vulnerability 79 | public object ExecuteCode(string code) 80 | { 81 | // Vulnerable: Dynamic code execution 82 | return Eval(code); 83 | } 84 | 85 | private object Eval(string code) 86 | { 87 | // Mock implementation of code evaluation 88 | Microsoft.CSharp.CSharpCodeProvider provider = new Microsoft.CSharp.CSharpCodeProvider(); 89 | System.CodeDom.Compiler.CompilerParameters parameters = new System.CodeDom.Compiler.CompilerParameters(); 90 | // Vulnerable implementation 91 | return null; // Simplified for example 92 | } 93 | 94 | public string RunCommand(string command) 95 | { 96 | // Vulnerable: Direct command execution 97 | Process process = new Process(); 98 | process.StartInfo.FileName = "cmd.exe"; 99 | process.StartInfo.Arguments = "/c " + command; 100 | process.StartInfo.RedirectStandardOutput = true; 101 | process.StartInfo.UseShellExecute = false; 102 | process.Start(); 103 | string output = process.StandardOutput.ReadToEnd(); 104 | process.WaitForExit(); 105 | return output; 106 | } 107 | 108 | // Server-Side Request Forgery (SSRF) vulnerability 109 | public string FetchUrl(string url) 110 | { 111 | // Vulnerable: No URL validation 112 | using (WebClient client = new WebClient()) 113 | { 114 | return client.DownloadString(url); 115 | } 116 | } 117 | 118 | // XML External Entity (XXE) vulnerability 119 | public XmlDocument ParseXml(string xml) 120 | { 121 | // Vulnerable: No protection against XXE 122 | XmlDocument doc = new XmlDocument(); 123 | doc.XmlResolver = new XmlUrlResolver(); // Allows XXE 124 | doc.LoadXml(xml); 125 | return doc; 126 | } 127 | 128 | // Path Traversal vulnerability 129 | public string ReadFile(string fileName) 130 | { 131 | // Vulnerable: No path validation 132 | return File.ReadAllText(fileName); 133 | } 134 | 135 | // Insecure Direct Object Reference (IDOR) vulnerability 136 | private string[] UserRecords = {"admin:secret", "user:password"}; 137 | 138 | public string GetUserRecord(int id) 139 | { 140 | // Vulnerable: No access control 141 | return UserRecords[id]; 142 | } 143 | 144 | // Authentication Issues 145 | public bool Login(string username, string password) 146 | { 147 | // Vulnerable: Weak authentication 148 | if (username == "admin" && password == "admin123") 149 | { 150 | return true; 151 | } 152 | return false; 153 | } 154 | 155 | // Cross-Site Request Forgery (CSRF) vulnerability 156 | public bool TransferMoney(string fromAccount, string toAccount, int amount) 157 | { 158 | // Vulnerable: No CSRF protection 159 | Console.WriteLine($"Transferring {amount} from {fromAccount} to {toAccount}"); 160 | // Process transfer without validating request origin 161 | return true; 162 | } 163 | } -------------------------------------------------------------------------------- /test_files/Vulnerable.java: -------------------------------------------------------------------------------- 1 | import java.sql.Connection; 2 | import java.sql.DriverManager; 3 | import java.sql.Statement; 4 | import java.util.Base64; 5 | import java.util.logging.Logger; 6 | import java.io.*; 7 | import java.net.URL; 8 | import javax.xml.parsers.DocumentBuilderFactory; 9 | import javax.xml.parsers.DocumentBuilder; 10 | import org.w3c.dom.Document; 11 | import org.xml.sax.InputSource; 12 | 13 | public class Vulnerable { 14 | private static final Logger LOGGER = Logger.getLogger(Vulnerable.class.getName()); 15 | 16 | // SQL Injection vulnerability 17 | public void getUserData(String username) { 18 | try { 19 | Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/test", "root", ""); 20 | Statement stmt = conn.createStatement(); 21 | // Vulnerable: Direct string concatenation 22 | stmt.executeQuery("SELECT * FROM users WHERE username = '" + username + "'"); 23 | } catch (Exception e) { 24 | LOGGER.severe("Database error: " + e.getMessage()); 25 | } 26 | } 27 | 28 | // XSS vulnerability 29 | public String displayUserInput(String input) { 30 | // Vulnerable: Direct HTML output 31 | return "
" + input + "
"; 32 | } 33 | 34 | // Insufficient Input Validation 35 | public int calculateUserAge(String age) { 36 | // Vulnerable: No validation 37 | return Integer.parseInt(age) * 12; 38 | } 39 | 40 | // Sensitive Data Exposure 41 | public void saveUserCredentials(String username, String password) { 42 | // Vulnerable: Logging sensitive data 43 | LOGGER.info("New user - Username: " + username + ", Password: " + password); 44 | } 45 | 46 | // Session Management Issues 47 | private static java.util.Map sessions = new java.util.HashMap<>(); 48 | public String createSession(String userId) { 49 | // Vulnerable: Predictable session token 50 | String token = userId + System.currentTimeMillis(); 51 | sessions.put(userId, token); 52 | return token; 53 | } 54 | 55 | // Security Misconfiguration 56 | private static final String ADMIN_PASSWORD = "admin123"; // Vulnerable: Hardcoded credential 57 | private static final boolean DEBUG_MODE = true; // Vulnerable: Debug enabled in production 58 | 59 | // Sensitive Data Logging 60 | public void processPayment(String creditCard) { 61 | // Vulnerable: Logging sensitive data 62 | LOGGER.info("Processing payment with card: " + creditCard); 63 | } 64 | 65 | // Insecure Cryptographic Usage 66 | public String encryptPassword(String password) { 67 | // Vulnerable: Using base64 as "encryption" 68 | return Base64.getEncoder().encodeToString(password.getBytes()); 69 | } 70 | 71 | // Remote Code Execution (RCE) vulnerability 72 | public Object executeCode(String code) throws Exception { 73 | // Vulnerable: Dynamic code execution 74 | return Class.forName("javax.script.ScriptEngineManager") 75 | .newInstance() 76 | .getClass() 77 | .getMethod("getEngineByName", String.class) 78 | .invoke(Class.forName("javax.script.ScriptEngineManager").newInstance(), "js") 79 | .getClass() 80 | .getMethod("eval", String.class) 81 | .invoke(Class.forName("javax.script.ScriptEngineManager") 82 | .newInstance() 83 | .getClass() 84 | .getMethod("getEngineByName", String.class) 85 | .invoke(Class.forName("javax.script.ScriptEngineManager").newInstance(), "js"), code); 86 | } 87 | 88 | public Process runCommand(String command) throws Exception { 89 | // Vulnerable: Direct command execution 90 | return Runtime.getRuntime().exec(command); 91 | } 92 | 93 | // Server-Side Request Forgery (SSRF) vulnerability 94 | public String fetchUrl(String url) throws Exception { 95 | // Vulnerable: No URL validation 96 | BufferedReader reader = new BufferedReader( 97 | new InputStreamReader(new URL(url).openStream()) 98 | ); 99 | StringBuilder content = new StringBuilder(); 100 | String line; 101 | while ((line = reader.readLine()) != null) { 102 | content.append(line); 103 | } 104 | reader.close(); 105 | return content.toString(); 106 | } 107 | 108 | // XML External Entity (XXE) vulnerability 109 | public Document parseXml(String xml) throws Exception { 110 | // Vulnerable: No protection against XXE 111 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 112 | DocumentBuilder builder = factory.newDocumentBuilder(); 113 | return builder.parse(new InputSource(new StringReader(xml))); 114 | } 115 | 116 | // Path Traversal vulnerability 117 | public String readFile(String fileName) throws Exception { 118 | // Vulnerable: No path validation 119 | BufferedReader reader = new BufferedReader(new FileReader(fileName)); 120 | StringBuilder content = new StringBuilder(); 121 | String line; 122 | while ((line = reader.readLine()) != null) { 123 | content.append(line); 124 | } 125 | reader.close(); 126 | return content.toString(); 127 | } 128 | 129 | // Insecure Direct Object Reference (IDOR) vulnerability 130 | private String[] userRecords = {"admin:secret", "user:password"}; 131 | 132 | public String getUserRecord(int id) { 133 | // Vulnerable: No access control 134 | return userRecords[id]; 135 | } 136 | 137 | // Authentication Issues 138 | public boolean login(String username, String password) { 139 | // Vulnerable: Weak authentication 140 | if (username.equals("admin") && password.equals("admin123")) { 141 | return true; 142 | } 143 | return false; 144 | } 145 | 146 | // Cross-Site Request Forgery (CSRF) vulnerability 147 | public boolean transferMoney(String fromAccount, String toAccount, int amount) { 148 | // Vulnerable: No CSRF protection 149 | LOGGER.info("Transferring " + amount + " from " + fromAccount + " to " + toAccount); 150 | // Process transfer without validating request origin 151 | return true; 152 | } 153 | } -------------------------------------------------------------------------------- /test_files/vulnerable.php: -------------------------------------------------------------------------------- 1 | query($query); 8 | } 9 | 10 | // XSS vulnerability 11 | function displayUserInput($message) { 12 | // Vulnerable: Direct output without escaping 13 | echo "
$message
"; 14 | } 15 | 16 | // Insufficient Input Validation 17 | function processUserAge($age) { 18 | // Vulnerable: No validation 19 | return $age * 12; 20 | } 21 | 22 | // Sensitive Data Exposure 23 | function saveUserCredentials($username, $password) { 24 | // Vulnerable: Storing plain text password 25 | file_put_contents('credentials.txt', "$username:$password\n", FILE_APPEND); 26 | } 27 | 28 | // Session Management Issues 29 | function createUserSession($userId) { 30 | // Vulnerable: Weak session management 31 | session_start(); 32 | $_SESSION['user'] = $userId; 33 | // No session regeneration, no secure flags 34 | } 35 | 36 | // Security Misconfiguration 37 | ini_set('display_errors', 1); // Vulnerable: Exposing errors 38 | error_reporting(E_ALL); 39 | 40 | // Sensitive Data Logging 41 | function logPayment($creditCard) { 42 | // Vulnerable: Logging sensitive data 43 | error_log("Processing payment for card: $creditCard"); 44 | } 45 | 46 | // Insecure Cryptographic Usage 47 | function encryptPassword($password) { 48 | // Vulnerable: Using weak hashing 49 | return md5($password); 50 | } 51 | 52 | // Remote Code Execution (RCE) vulnerability 53 | function runCommand($cmd) { 54 | // Vulnerable: Direct command execution 55 | return shell_exec($cmd); 56 | } 57 | 58 | function evaluateCode($code) { 59 | // Vulnerable: Direct eval of user input 60 | return eval($code); 61 | } 62 | 63 | // Server-Side Request Forgery (SSRF) vulnerability 64 | function fetchExternalResource($url) { 65 | // Vulnerable: No URL validation 66 | return file_get_contents($url); 67 | } 68 | 69 | // XML External Entity (XXE) vulnerability 70 | function parseXmlDocument($xml) { 71 | // Vulnerable: No protection against XXE 72 | $doc = new DOMDocument(); 73 | $doc->loadXML($xml, LIBXML_NOENT); 74 | return $doc; 75 | } 76 | 77 | // Path Traversal vulnerability 78 | function getFileContents($fileName) { 79 | // Vulnerable: No path validation 80 | return file_get_contents($fileName); 81 | } 82 | 83 | function saveUploadedFile($fileName) { 84 | // Vulnerable: Path traversal 85 | $filePath = "uploads/" . $fileName; 86 | return $filePath; 87 | } 88 | 89 | // Insecure Direct Object Reference (IDOR) vulnerability 90 | function getUserProfile($userId) { 91 | // Vulnerable: No access control checks 92 | $filePath = "users/" . $userId . ".json"; 93 | return json_decode(file_get_contents($filePath), true); 94 | } 95 | 96 | // Authentication Issues 97 | function loginUser($username, $password) { 98 | // Vulnerable: Weak password policy, no MFA 99 | $users = [ 100 | "admin" => "password123", 101 | "user" => "123456" 102 | ]; 103 | 104 | return isset($users[$username]) && $users[$username] === $password; 105 | } 106 | 107 | // Cross-Site Request Forgery (CSRF) vulnerability 108 | function processMoneyTransfer($fromAccount, $toAccount, $amount) { 109 | // Vulnerable: No CSRF token validation 110 | // Just process the transfer based on parameters 111 | return "Transferred $amount from $fromAccount to $toAccount"; 112 | } 113 | 114 | ?> -------------------------------------------------------------------------------- /test_files/vulnerable.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import logging 3 | import base64 4 | import subprocess 5 | import urllib.request 6 | import xml.etree.ElementTree as ET 7 | from flask import Flask, request 8 | 9 | # SQL Injection vulnerability 10 | def get_user(username): 11 | conn = sqlite3.connect('users.db') 12 | cursor = conn.cursor() 13 | # Vulnerable: Direct string concatenation 14 | cursor.execute("SELECT * FROM users WHERE username = '" + username + "'") 15 | return cursor.fetchone() 16 | 17 | # XSS vulnerability 18 | def render_comment(comment): 19 | # Vulnerable: Direct HTML rendering without escaping 20 | return f"
{comment}
" 21 | 22 | # Insufficient Input Validation 23 | def process_age(age): 24 | # Vulnerable: No proper validation 25 | return int(age) * 12 26 | 27 | # Sensitive Data Exposure 28 | def save_user_data(user): 29 | # Vulnerable: Logging sensitive data 30 | logging.info(f"New user created - Username: {user['username']}, Password: {user['password']}, SSN: {user['ssn']}") 31 | 32 | # Insecure Session Management 33 | session_tokens = {} 34 | def create_session(user_id): 35 | # Vulnerable: Predictable session token 36 | token = str(user_id) + "_session" 37 | session_tokens[user_id] = token 38 | return token 39 | 40 | # Security Misconfiguration 41 | DEBUG = True 42 | ADMIN_PASSWORD = "admin123" # Vulnerable: Hardcoded credentials 43 | 44 | # Sensitive Data Logging 45 | def log_transaction(credit_card): 46 | # Vulnerable: Logging sensitive data 47 | logging.info(f"Processing payment with card: {credit_card}") 48 | 49 | # Insecure Cryptographic Usage 50 | def encrypt_password(password): 51 | # Vulnerable: Using base64 for "encryption" 52 | return base64.b64encode(password.encode()).decode() 53 | 54 | # Remote Code Execution (RCE) vulnerability 55 | def run_command(command): 56 | # Vulnerable: Direct command execution 57 | return subprocess.check_output(command, shell=True) 58 | 59 | def process_template(template_str, context): 60 | # Vulnerable: Template injection 61 | template_code = f"def render_template(): return f'''{template_str}'''" 62 | exec(template_code, context) 63 | return context["render_template"]() 64 | 65 | # Server-Side Request Forgery (SSRF) vulnerability 66 | def fetch_url(url): 67 | # Vulnerable: No validation of URL 68 | return urllib.request.urlopen(url).read() 69 | 70 | def webhook_callback(webhook_url, data): 71 | # Vulnerable: No URL validation 72 | import requests 73 | return requests.post(webhook_url, json=data) 74 | 75 | # XML External Entity (XXE) vulnerability 76 | def parse_xml(xml_string): 77 | # Vulnerable: No protection against XXE 78 | return ET.fromstring(xml_string) 79 | 80 | # Path Traversal vulnerability 81 | def read_file(filename): 82 | # Vulnerable: No path validation 83 | with open(filename, 'r') as file: 84 | return file.read() 85 | 86 | def save_profile_picture(user_id, filename): 87 | # Vulnerable: Path traversal 88 | path = f"uploads/{filename}" 89 | return path 90 | 91 | # Insecure Direct Object Reference (IDOR) vulnerability 92 | users_data = { 93 | "1": {"name": "Admin", "role": "admin", "salary": 100000}, 94 | "2": {"name": "User", "role": "user", "salary": 50000} 95 | } 96 | 97 | def get_user_data(user_id): 98 | # Vulnerable: No access control check 99 | return users_data.get(user_id) 100 | 101 | # Cross-Site Request Forgery (CSRF) vulnerability 102 | app = Flask(__name__) 103 | 104 | @app.route('/transfer') 105 | def transfer_money(): 106 | # Vulnerable: No CSRF protection 107 | from_account = request.args.get('from') 108 | to_account = request.args.get('to') 109 | amount = request.args.get('amount') 110 | # Process the transfer without CSRF token 111 | return f"Transferred ${amount} from {from_account} to {to_account}" 112 | 113 | # Authentication Issues 114 | def login(username, password): 115 | # Vulnerable: No rate limiting, no MFA 116 | if username == "admin" and password == "password": 117 | return True 118 | return False -------------------------------------------------------------------------------- /test_files/vulnerable.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SQL Injection vulnerability 4 | function query_database() { 5 | # Vulnerable: Direct variable interpolation 6 | mysql -u root -e "SELECT * FROM users WHERE username = '$1'" 7 | } 8 | 9 | # Insufficient Input Validation 10 | function process_input() { 11 | # Vulnerable: No input validation 12 | eval "$1" # Executing user input directly 13 | } 14 | 15 | # Sensitive Data Exposure 16 | function backup_credentials() { 17 | # Vulnerable: Storing sensitive data in plain text 18 | echo "admin:password123" > /tmp/backup.txt 19 | chmod 644 /tmp/backup.txt 20 | } 21 | 22 | # Security Misconfiguration 23 | # Vulnerable: Weak permissions 24 | chmod 777 /var/www/html 25 | chmod 777 /etc/passwd 26 | 27 | # Sensitive Data Logging 28 | function process_payment() { 29 | # Vulnerable: Logging sensitive data 30 | echo "Processing payment with card: $1" >> /var/log/payments.log 31 | } 32 | 33 | # Insecure Cryptographic Usage 34 | function encrypt_password() { 35 | # Vulnerable: Using weak encryption 36 | echo "$1" | base64 37 | } 38 | 39 | # Session Management Issues 40 | function create_session() { 41 | # Vulnerable: Predictable session file 42 | echo "user_session" > "/tmp/session_$1" 43 | } 44 | 45 | # Remote Code Execution (RCE) vulnerability 46 | function execute_command() { 47 | # Vulnerable: Direct command execution 48 | eval $1 49 | } 50 | 51 | function process_template() { 52 | # Vulnerable: Template injection through eval 53 | template="$1" 54 | data="$2" 55 | eval "echo \"$template\"" 56 | } 57 | 58 | # Server-Side Request Forgery (SSRF) vulnerability 59 | function fetch_url() { 60 | # Vulnerable: No URL validation 61 | curl -s "$1" 62 | } 63 | 64 | function webhook_callback() { 65 | # Vulnerable: No URL validation 66 | curl -X POST -H "Content-Type: application/json" -d "$2" "$1" 67 | } 68 | 69 | # XML External Entity (XXE) vulnerability - shell script example 70 | function parse_xml() { 71 | # Vulnerable: Using external entity processing 72 | xmllint --noent "$1" 73 | } 74 | 75 | # Path Traversal vulnerability 76 | function read_file() { 77 | # Vulnerable: No path validation 78 | cat "$1" 79 | } 80 | 81 | function save_file() { 82 | # Vulnerable: Path traversal 83 | echo "$2" > "uploads/$1" 84 | } 85 | 86 | # Insecure Direct Object Reference (IDOR) vulnerability 87 | function get_user_data() { 88 | # Vulnerable: Direct reference to objects without access control 89 | cat "users/$1.json" 90 | } 91 | 92 | # Authentication Issues 93 | function login() { 94 | # Vulnerable: Weak authentication, hardcoded credentials 95 | if [ "$1" == "admin" ] && [ "$2" == "admin123" ]; then 96 | echo "Login successful" 97 | return 0 98 | fi 99 | return 1 100 | } 101 | 102 | # Cross-Site Request Forgery (CSRF) vulnerability - shell script server example 103 | function handle_transfer() { 104 | # Vulnerable: No CSRF protection 105 | from_account="$1" 106 | to_account="$2" 107 | amount="$3" 108 | echo "Transferring $amount from $from_account to $to_account" 109 | # Process transfer without validating request origin 110 | } 111 | 112 | # Example usage 113 | query_database "user' OR '1'='1" 114 | process_input "rm -rf /" 115 | backup_credentials 116 | process_payment "4111-1111-1111-1111" 117 | encrypt_password "secret123" 118 | create_session "admin" 119 | execute_command "cat /etc/passwd" 120 | fetch_url "http://internal-server/admin" 121 | read_file "../../../etc/passwd" 122 | handle_transfer "account1" "hacker_account" "1000" --------------------------------------------------------------------------------