├── .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 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
20 |
OASIS
21 |
22 |
23 | 🏝️ O llama A utomated S ecurity I ntelligence S canner
24 |
25 |
26 |
27 |
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 |
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 |
44 |
45 | ${formattedModel} (${count})
46 |
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 |
65 |
66 | ${formattedVuln} (${count})
67 |
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 |
86 |
87 | ${formattedFormat} (${count})
88 |
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 |
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 += `
202 | ${fmt} ${formats[fmt]} `;
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 |
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 |
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 |
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 += `
📄 PDF `;
357 | }
358 | if (mdPath) {
359 | formatButtons += `
📝 MD `;
360 | }
361 | if (htmlPath) {
362 | formatButtons += `
🌐 HTML `;
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 |
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 |
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 |
18 | 🔍
19 | Filters
20 |
21 |
22 |
56 |
57 |
58 |
61 |
62 |
68 |
71 |
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 |
--------------------------------------------------------------------------------
/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 |
10 |
11 |
12 |
13 |
🏝️ OASIS
14 |
O llama A utomated S ecurity I ntelligence S canner
15 | {% if error %}
16 |
{{ error }}
17 | {% endif %}
18 |
24 |
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 |
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""
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"
--------------------------------------------------------------------------------