Spaces:
Running
Running
Shravan Pandala commited on
Commit ·
13fb9f7
1
Parent(s): 2f1e795
refactor: restructure project - move server files to root, merge package.json, rewrite .gitignore
Browse files- .env.example +2 -0
- .gitignore +23 -199
- CHANGELOG.md +9 -0
- TESTING.md +125 -0
- app.py +0 -10
- cerberus_api/__init__.py +0 -35
- cerberus_api/config.py +0 -9
- cerberus_api/n8n_client.py +0 -55
- cerberus_api/routes.py +0 -67
- eslint.config.mjs +27 -0
- n8nClient.js +103 -0
- package-lock.json +0 -0
- package.json +115 -0
- requirements.txt +0 -4
- routes.js +293 -0
- server.js +64 -0
- src/extension.ts +181 -0
- src/test/extension.test.ts +15 -0
- src/vulnerabilityTree.ts +155 -0
- test_endpoints.py +0 -101
- tsconfig.json +17 -0
- vsc-extension-quickstart.md +44 -0
.env.example
CHANGED
|
@@ -1,2 +1,4 @@
|
|
| 1 |
N8N_WEBHOOK_URL=https://your-n8n-instance/webhook/scan
|
| 2 |
N8N_TIMEOUT_SECONDS=20
|
|
|
|
|
|
|
|
|
| 1 |
N8N_WEBHOOK_URL=https://your-n8n-instance/webhook/scan
|
| 2 |
N8N_TIMEOUT_SECONDS=20
|
| 3 |
+
PORT=5000
|
| 4 |
+
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
|
.gitignore
CHANGED
|
@@ -1,208 +1,32 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
*.py[codz]
|
| 4 |
-
*$py.class
|
| 5 |
|
| 6 |
-
#
|
| 7 |
-
|
|
|
|
| 8 |
|
| 9 |
-
#
|
| 10 |
-
.
|
| 11 |
-
build/
|
| 12 |
-
develop-eggs/
|
| 13 |
-
dist/
|
| 14 |
-
downloads/
|
| 15 |
-
eggs/
|
| 16 |
-
.eggs/
|
| 17 |
-
lib/
|
| 18 |
-
lib64/
|
| 19 |
-
parts/
|
| 20 |
-
sdist/
|
| 21 |
-
var/
|
| 22 |
-
wheels/
|
| 23 |
-
share/python-wheels/
|
| 24 |
-
*.egg-info/
|
| 25 |
-
.installed.cfg
|
| 26 |
-
*.egg
|
| 27 |
-
MANIFEST
|
| 28 |
|
| 29 |
-
#
|
| 30 |
-
# Usually these files are written by a python script from a template
|
| 31 |
-
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
-
*.manifest
|
| 33 |
-
*.spec
|
| 34 |
-
|
| 35 |
-
# Installer logs
|
| 36 |
-
pip-log.txt
|
| 37 |
-
pip-delete-this-directory.txt
|
| 38 |
-
|
| 39 |
-
# Unit test / coverage reports
|
| 40 |
-
htmlcov/
|
| 41 |
-
.tox/
|
| 42 |
-
.nox/
|
| 43 |
-
.coverage
|
| 44 |
-
.coverage.*
|
| 45 |
-
.cache
|
| 46 |
-
nosetests.xml
|
| 47 |
-
coverage.xml
|
| 48 |
-
*.cover
|
| 49 |
-
*.py.cover
|
| 50 |
-
.hypothesis/
|
| 51 |
-
.pytest_cache/
|
| 52 |
-
cover/
|
| 53 |
-
|
| 54 |
-
# Translations
|
| 55 |
-
*.mo
|
| 56 |
-
*.pot
|
| 57 |
-
|
| 58 |
-
# Django stuff:
|
| 59 |
-
*.log
|
| 60 |
-
local_settings.py
|
| 61 |
-
db.sqlite3
|
| 62 |
-
db.sqlite3-journal
|
| 63 |
-
|
| 64 |
-
# Flask stuff:
|
| 65 |
-
instance/
|
| 66 |
-
.webassets-cache
|
| 67 |
-
|
| 68 |
-
# Scrapy stuff:
|
| 69 |
-
.scrapy
|
| 70 |
-
|
| 71 |
-
# Sphinx documentation
|
| 72 |
-
docs/_build/
|
| 73 |
-
|
| 74 |
-
# PyBuilder
|
| 75 |
-
.pybuilder/
|
| 76 |
-
target/
|
| 77 |
-
|
| 78 |
-
# Jupyter Notebook
|
| 79 |
-
.ipynb_checkpoints
|
| 80 |
-
|
| 81 |
-
# IPython
|
| 82 |
-
profile_default/
|
| 83 |
-
ipython_config.py
|
| 84 |
-
|
| 85 |
-
# pyenv
|
| 86 |
-
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
-
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
-
# .python-version
|
| 89 |
-
|
| 90 |
-
# pipenv
|
| 91 |
-
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
-
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
-
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
-
# install all needed dependencies.
|
| 95 |
-
#Pipfile.lock
|
| 96 |
-
|
| 97 |
-
# UV
|
| 98 |
-
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 99 |
-
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
-
# commonly ignored for libraries.
|
| 101 |
-
#uv.lock
|
| 102 |
-
|
| 103 |
-
# poetry
|
| 104 |
-
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 105 |
-
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 106 |
-
# commonly ignored for libraries.
|
| 107 |
-
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 108 |
-
#poetry.lock
|
| 109 |
-
#poetry.toml
|
| 110 |
-
|
| 111 |
-
# pdm
|
| 112 |
-
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 113 |
-
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 114 |
-
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 115 |
-
#pdm.lock
|
| 116 |
-
#pdm.toml
|
| 117 |
-
.pdm-python
|
| 118 |
-
.pdm-build/
|
| 119 |
-
|
| 120 |
-
# pixi
|
| 121 |
-
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 122 |
-
#pixi.lock
|
| 123 |
-
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 124 |
-
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 125 |
-
.pixi
|
| 126 |
-
|
| 127 |
-
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 128 |
-
__pypackages__/
|
| 129 |
-
|
| 130 |
-
# Celery stuff
|
| 131 |
-
celerybeat-schedule
|
| 132 |
-
celerybeat.pid
|
| 133 |
-
|
| 134 |
-
# SageMath parsed files
|
| 135 |
-
*.sage.py
|
| 136 |
-
|
| 137 |
-
# Environments
|
| 138 |
.env
|
| 139 |
.envrc
|
| 140 |
-
.venv
|
| 141 |
-
env/
|
| 142 |
-
venv/
|
| 143 |
-
ENV/
|
| 144 |
-
env.bak/
|
| 145 |
-
venv.bak/
|
| 146 |
-
|
| 147 |
-
# Spyder project settings
|
| 148 |
-
.spyderproject
|
| 149 |
-
.spyproject
|
| 150 |
-
|
| 151 |
-
# Rope project settings
|
| 152 |
-
.ropeproject
|
| 153 |
-
|
| 154 |
-
# mkdocs documentation
|
| 155 |
-
/site
|
| 156 |
-
|
| 157 |
-
# mypy
|
| 158 |
-
.mypy_cache/
|
| 159 |
-
.dmypy.json
|
| 160 |
-
dmypy.json
|
| 161 |
-
|
| 162 |
-
# Pyre type checker
|
| 163 |
-
.pyre/
|
| 164 |
|
| 165 |
-
#
|
| 166 |
-
.
|
| 167 |
-
|
| 168 |
-
# Cython debug symbols
|
| 169 |
-
cython_debug/
|
| 170 |
-
|
| 171 |
-
# PyCharm
|
| 172 |
-
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 173 |
-
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 174 |
-
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 175 |
-
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 176 |
-
#.idea/
|
| 177 |
-
|
| 178 |
-
# Abstra
|
| 179 |
-
# Abstra is an AI-powered process automation framework.
|
| 180 |
-
# Ignore directories containing user credentials, local state, and settings.
|
| 181 |
-
# Learn more at https://abstra.io/docs
|
| 182 |
-
.abstra/
|
| 183 |
-
|
| 184 |
-
# Visual Studio Code
|
| 185 |
-
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 186 |
-
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 187 |
-
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 188 |
-
# you could uncomment the following to ignore the entire vscode folder
|
| 189 |
-
# .vscode/
|
| 190 |
-
|
| 191 |
-
# Ruff stuff:
|
| 192 |
-
.ruff_cache/
|
| 193 |
-
|
| 194 |
-
# PyPI configuration file
|
| 195 |
-
.pypirc
|
| 196 |
|
| 197 |
-
#
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
# refer to https://docs.cursor.com/context/ignore-files
|
| 201 |
-
.cursorignore
|
| 202 |
-
.cursorindexingignore
|
| 203 |
|
| 204 |
-
#
|
| 205 |
-
marimo/_static/
|
| 206 |
-
marimo/_lsp/
|
| 207 |
-
__marimo__/
|
| 208 |
.DS_Store
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ─── Dependencies ────────────────────────────────────────────────────────────
|
| 2 |
+
node_modules/
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
# ─── Build output ─────────────────────────────────────────────────────────────
|
| 5 |
+
out/
|
| 6 |
+
*.js.map
|
| 7 |
|
| 8 |
+
# ─── VS Code Extension package ────────────────────────────────────────────────
|
| 9 |
+
*.vsix
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
# ─── Environment / Secrets ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
.env
|
| 13 |
.envrc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
# ─── Logs ─────────────────────────────────────────────────────────────────────
|
| 16 |
+
*.log
|
| 17 |
+
npm-debug.log*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
# ─── Test coverage ────────────────────────────────────────────────────────────
|
| 20 |
+
coverage/
|
| 21 |
+
.nyc_output/
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
+
# ─── macOS ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
| 24 |
.DS_Store
|
| 25 |
+
|
| 26 |
+
# ─── Editor / IDE ─────────────────────────────────────────────────────────────
|
| 27 |
+
.idea/
|
| 28 |
+
*.suo
|
| 29 |
+
*.ntvs*
|
| 30 |
+
*.njsproj
|
| 31 |
+
*.sln
|
| 32 |
+
*.sw?
|
CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Change Log
|
| 2 |
+
|
| 3 |
+
All notable changes to the "cerberus" extension will be documented in this file.
|
| 4 |
+
|
| 5 |
+
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
| 6 |
+
|
| 7 |
+
## [Unreleased]
|
| 8 |
+
|
| 9 |
+
- Initial release
|
TESTING.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Testing the Cerberus VS Code Extension
|
| 2 |
+
|
| 3 |
+
## Prerequisites
|
| 4 |
+
1. Node.js installed
|
| 5 |
+
2. VS Code installed
|
| 6 |
+
3. Backend server dependencies installed
|
| 7 |
+
|
| 8 |
+
## Setup
|
| 9 |
+
|
| 10 |
+
### 1. Install Extension Dependencies
|
| 11 |
+
```bash
|
| 12 |
+
npm install
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
### 2. Install and Start Backend Server
|
| 16 |
+
```bash
|
| 17 |
+
cd server
|
| 18 |
+
npm install
|
| 19 |
+
npm start
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
The server will run on http://localhost:5000
|
| 23 |
+
|
| 24 |
+
### 3. Compile Extension
|
| 25 |
+
```bash
|
| 26 |
+
npm run compile
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
Or watch for changes:
|
| 30 |
+
```bash
|
| 31 |
+
npm run watch
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## Running the Extension
|
| 35 |
+
|
| 36 |
+
### Option 1: Press F5 (Debug Mode)
|
| 37 |
+
1. Open the project in VS Code
|
| 38 |
+
2. Press `F5` to launch the Extension Development Host
|
| 39 |
+
3. A new VS Code window will open with the extension loaded
|
| 40 |
+
|
| 41 |
+
### Option 2: Install Locally
|
| 42 |
+
```bash
|
| 43 |
+
vsce package
|
| 44 |
+
```
|
| 45 |
+
Then install the `.vsix` file in VS Code.
|
| 46 |
+
|
| 47 |
+
## Testing
|
| 48 |
+
|
| 49 |
+
### Create a Test File
|
| 50 |
+
Create a file called `test_vulnerable.py` with this content:
|
| 51 |
+
|
| 52 |
+
```python
|
| 53 |
+
def get_user(user_id):
|
| 54 |
+
import sqlite3
|
| 55 |
+
conn = sqlite3.connect('users.db')
|
| 56 |
+
cursor = conn.cursor()
|
| 57 |
+
# SQL Injection vulnerability - unsafe string concatenation
|
| 58 |
+
query = 'SELECT * FROM users WHERE id = ' + user_id
|
| 59 |
+
cursor.execute(query)
|
| 60 |
+
return cursor.fetchone()
|
| 61 |
+
|
| 62 |
+
def login(username, password):
|
| 63 |
+
import sqlite3
|
| 64 |
+
conn = sqlite3.connect('users.db')
|
| 65 |
+
cursor = conn.cursor()
|
| 66 |
+
# Another SQL injection
|
| 67 |
+
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
|
| 68 |
+
cursor.execute(query)
|
| 69 |
+
return cursor.fetchone()
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
### Using the Extension
|
| 73 |
+
|
| 74 |
+
1. **Open the Cerberus Panel**
|
| 75 |
+
- Click the shield icon (🛡️) in the Activity Bar on the left
|
| 76 |
+
- Or run command: `Cerberus: View Results`
|
| 77 |
+
|
| 78 |
+
2. **Run a Scan**
|
| 79 |
+
- Click the shield icon in the panel title bar
|
| 80 |
+
- Or run command: `Cerberus: Scan for Vulnerabilities`
|
| 81 |
+
- Or press `Ctrl+Shift+P` → type "Cerberus: Scan"
|
| 82 |
+
|
| 83 |
+
3. **View Results**
|
| 84 |
+
- Vulnerabilities will appear in the tree view
|
| 85 |
+
- Expand files to see individual vulnerabilities
|
| 86 |
+
- Click a vulnerability to see the fix preview
|
| 87 |
+
|
| 88 |
+
4. **Apply Fixes**
|
| 89 |
+
- Right-click a vulnerability → "Apply Fix"
|
| 90 |
+
- Or right-click a file → "Fix All in File"
|
| 91 |
+
- The file will be automatically updated and saved
|
| 92 |
+
|
| 93 |
+
## Features
|
| 94 |
+
|
| 95 |
+
- 🔍 **Scan Workspace**: Scans all code files for vulnerabilities
|
| 96 |
+
- 🛠️ **Apply Fix**: Automatically applies AI-suggested fixes
|
| 97 |
+
- 📁 **File Tree**: Hierarchical view of vulnerabilities by file
|
| 98 |
+
- ⚡ **Real-time**: Instant feedback on scan progress
|
| 99 |
+
- 🎯 **Context Menu**: Right-click actions for quick fixes
|
| 100 |
+
|
| 101 |
+
## Commands
|
| 102 |
+
|
| 103 |
+
| Command | Description |
|
| 104 |
+
|---------|-------------|
|
| 105 |
+
| `Cerberus: Scan for Vulnerabilities` | Scan current workspace |
|
| 106 |
+
| `Cerberus: Apply Fix` | Apply fix to selected vulnerability |
|
| 107 |
+
| `Cerberus: Fix All in File` | Apply all fixes in a file |
|
| 108 |
+
| `Cerberus: View Results` | Focus the Cerberus panel |
|
| 109 |
+
|
| 110 |
+
## Troubleshooting
|
| 111 |
+
|
| 112 |
+
### "Failed to connect to backend server"
|
| 113 |
+
- Make sure the server is running: `cd server && npm start`
|
| 114 |
+
- Check if port 5000 is available
|
| 115 |
+
- Verify the server is accessible: `curl http://localhost:5000/api/health`
|
| 116 |
+
|
| 117 |
+
### Extension not loading
|
| 118 |
+
- Run `npm run compile` to build the extension
|
| 119 |
+
- Check the Debug Console for errors
|
| 120 |
+
- Make sure `out/extension.js` exists
|
| 121 |
+
|
| 122 |
+
### Scan hangs or times out
|
| 123 |
+
- Large projects may take several minutes
|
| 124 |
+
- The timeout is set to 5 minutes
|
| 125 |
+
- Try scanning a smaller folder first
|
app.py
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
from dotenv import load_dotenv
|
| 2 |
-
|
| 3 |
-
from cerberus_api import create_app
|
| 4 |
-
|
| 5 |
-
load_dotenv()
|
| 6 |
-
app = create_app()
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
if __name__ == "__main__":
|
| 10 |
-
app.run(host="0.0.0.0", port=5000, debug=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cerberus_api/__init__.py
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
import logging
|
| 2 |
-
|
| 3 |
-
from flask import Flask, request
|
| 4 |
-
from flask_cors import CORS
|
| 5 |
-
|
| 6 |
-
from .config import Config
|
| 7 |
-
from .routes import api_bp
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def create_app() -> Flask:
|
| 11 |
-
app = Flask(__name__)
|
| 12 |
-
app.config.from_object(Config)
|
| 13 |
-
|
| 14 |
-
_configure_logging(app)
|
| 15 |
-
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
| 16 |
-
app.register_blueprint(api_bp)
|
| 17 |
-
|
| 18 |
-
@app.before_request
|
| 19 |
-
def log_incoming_request() -> None:
|
| 20 |
-
app.logger.info(
|
| 21 |
-
"Incoming request method=%s path=%s remote=%s",
|
| 22 |
-
request.method,
|
| 23 |
-
request.path,
|
| 24 |
-
request.remote_addr,
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
-
return app
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def _configure_logging(app: Flask) -> None:
|
| 31 |
-
logging.basicConfig(
|
| 32 |
-
level=logging.INFO,
|
| 33 |
-
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
| 34 |
-
)
|
| 35 |
-
app.logger.setLevel(logging.INFO)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cerberus_api/config.py
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
class Config:
|
| 5 |
-
N8N_WEBHOOK_URL = os.getenv(
|
| 6 |
-
"N8N_WEBHOOK_URL",
|
| 7 |
-
"https://n8n.shravanpandala.me/webhook/scan",
|
| 8 |
-
)
|
| 9 |
-
N8N_TIMEOUT_SECONDS = float(os.getenv("N8N_TIMEOUT_SECONDS", "20"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cerberus_api/n8n_client.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import requests
|
| 4 |
-
from requests.exceptions import RequestException, Timeout
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
class N8NWebhookError(Exception):
|
| 8 |
-
pass
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
class N8NWebhookTimeoutError(N8NWebhookError):
|
| 12 |
-
pass
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class N8NWebhookUpstreamError(N8NWebhookError):
|
| 16 |
-
pass
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class N8NWebhookResponseError(N8NWebhookError):
|
| 20 |
-
pass
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def patch_code_via_n8n(*, code: str, webhook_url: str, timeout_seconds: float) -> str:
|
| 24 |
-
try:
|
| 25 |
-
response = requests.post(
|
| 26 |
-
webhook_url,
|
| 27 |
-
json={"code": code},
|
| 28 |
-
timeout=timeout_seconds,
|
| 29 |
-
)
|
| 30 |
-
except Timeout as exc:
|
| 31 |
-
raise N8NWebhookTimeoutError("n8n webhook request timed out") from exc
|
| 32 |
-
except RequestException as exc:
|
| 33 |
-
raise N8NWebhookUpstreamError("Failed to call n8n webhook") from exc
|
| 34 |
-
|
| 35 |
-
if response.status_code >= 500:
|
| 36 |
-
raise N8NWebhookUpstreamError(
|
| 37 |
-
f"n8n webhook returned server error {response.status_code}"
|
| 38 |
-
)
|
| 39 |
-
if response.status_code >= 400:
|
| 40 |
-
raise N8NWebhookUpstreamError(
|
| 41 |
-
f"n8n webhook returned unexpected status {response.status_code}"
|
| 42 |
-
)
|
| 43 |
-
|
| 44 |
-
try:
|
| 45 |
-
payload = response.json()
|
| 46 |
-
except ValueError as exc:
|
| 47 |
-
raise N8NWebhookResponseError("n8n webhook response was not valid JSON") from exc
|
| 48 |
-
|
| 49 |
-
corrected_code = payload.get("corrected_code")
|
| 50 |
-
if not isinstance(corrected_code, str):
|
| 51 |
-
raise N8NWebhookResponseError(
|
| 52 |
-
"n8n webhook response missing 'corrected_code' string"
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
return corrected_code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cerberus_api/routes.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
from flask import Blueprint, current_app, jsonify, request
|
| 4 |
-
|
| 5 |
-
from .n8n_client import (
|
| 6 |
-
N8NWebhookResponseError,
|
| 7 |
-
N8NWebhookTimeoutError,
|
| 8 |
-
N8NWebhookUpstreamError,
|
| 9 |
-
patch_code_via_n8n,
|
| 10 |
-
)
|
| 11 |
-
|
| 12 |
-
api_bp = Blueprint("api", __name__)
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@api_bp.get("/api/health")
|
| 16 |
-
def health():
|
| 17 |
-
return jsonify({"status": "ok"}), 200
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
@api_bp.post("/api/patch-code")
|
| 21 |
-
def patch_code():
|
| 22 |
-
if not request.is_json:
|
| 23 |
-
return (
|
| 24 |
-
jsonify(
|
| 25 |
-
{
|
| 26 |
-
"error": "Invalid payload",
|
| 27 |
-
"message": "Request must be JSON: {'code': 'raw python string'}",
|
| 28 |
-
}
|
| 29 |
-
),
|
| 30 |
-
400,
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
payload = request.get_json(silent=True) or {}
|
| 34 |
-
code = payload.get("code")
|
| 35 |
-
|
| 36 |
-
if not isinstance(code, str):
|
| 37 |
-
return (
|
| 38 |
-
jsonify(
|
| 39 |
-
{
|
| 40 |
-
"error": "Invalid payload",
|
| 41 |
-
"message": "Field 'code' is required and must be a string.",
|
| 42 |
-
}
|
| 43 |
-
),
|
| 44 |
-
400,
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
current_app.logger.info("Processing patch request code_length=%s", len(code))
|
| 48 |
-
|
| 49 |
-
try:
|
| 50 |
-
corrected_code = patch_code_via_n8n(
|
| 51 |
-
code=code,
|
| 52 |
-
webhook_url=current_app.config["N8N_WEBHOOK_URL"],
|
| 53 |
-
timeout_seconds=current_app.config["N8N_TIMEOUT_SECONDS"],
|
| 54 |
-
)
|
| 55 |
-
except (N8NWebhookTimeoutError, N8NWebhookUpstreamError, N8NWebhookResponseError):
|
| 56 |
-
current_app.logger.exception("n8n webhook call failed")
|
| 57 |
-
return (
|
| 58 |
-
jsonify(
|
| 59 |
-
{
|
| 60 |
-
"error": "Bad Gateway",
|
| 61 |
-
"message": "Unable to retrieve corrected code from n8n webhook.",
|
| 62 |
-
}
|
| 63 |
-
),
|
| 64 |
-
502,
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
return jsonify({"corrected_code": corrected_code}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eslint.config.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import typescriptEslint from "typescript-eslint";
|
| 2 |
+
|
| 3 |
+
export default [{
|
| 4 |
+
files: ["**/*.ts"],
|
| 5 |
+
}, {
|
| 6 |
+
plugins: {
|
| 7 |
+
"@typescript-eslint": typescriptEslint.plugin,
|
| 8 |
+
},
|
| 9 |
+
|
| 10 |
+
languageOptions: {
|
| 11 |
+
parser: typescriptEslint.parser,
|
| 12 |
+
ecmaVersion: 2022,
|
| 13 |
+
sourceType: "module",
|
| 14 |
+
},
|
| 15 |
+
|
| 16 |
+
rules: {
|
| 17 |
+
"@typescript-eslint/naming-convention": ["warn", {
|
| 18 |
+
selector: "import",
|
| 19 |
+
format: ["camelCase", "PascalCase"],
|
| 20 |
+
}],
|
| 21 |
+
|
| 22 |
+
curly: "warn",
|
| 23 |
+
eqeqeq: "warn",
|
| 24 |
+
"no-throw-literal": "warn",
|
| 25 |
+
semi: "warn",
|
| 26 |
+
},
|
| 27 |
+
}];
|
n8nClient.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* n8n Webhook Client
|
| 3 |
+
* Handles communication with the n8n workflow for code analysis and patching
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
const axios = require('axios');
|
| 7 |
+
|
| 8 |
+
class N8NWebhookError extends Error {
|
| 9 |
+
constructor(message) {
|
| 10 |
+
super(message);
|
| 11 |
+
this.name = 'N8NWebhookError';
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
class N8NWebhookTimeoutError extends N8NWebhookError {
|
| 16 |
+
constructor(message) {
|
| 17 |
+
super(message);
|
| 18 |
+
this.name = 'N8NWebhookTimeoutError';
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
class N8NWebhookUpstreamError extends N8NWebhookError {
|
| 23 |
+
constructor(message) {
|
| 24 |
+
super(message);
|
| 25 |
+
this.name = 'N8NWebhookUpstreamError';
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
class N8NWebhookResponseError extends N8NWebhookError {
|
| 30 |
+
constructor(message) {
|
| 31 |
+
super(message);
|
| 32 |
+
this.name = 'N8NWebhookResponseError';
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Send code to n8n webhook for vulnerability analysis and correction
|
| 38 |
+
* @param {Object} params
|
| 39 |
+
* @param {string} params.code - The code to analyze
|
| 40 |
+
* @param {string} params.webhookUrl - The n8n webhook URL
|
| 41 |
+
* @param {number} params.timeoutSeconds - Request timeout in seconds
|
| 42 |
+
* @returns {Promise<string>} - The corrected code
|
| 43 |
+
*/
|
| 44 |
+
async function patchCodeViaN8n({ code, webhookUrl, timeoutSeconds }) {
|
| 45 |
+
try {
|
| 46 |
+
console.log(`[DEBUG] Sending code to n8n (length: ${code.length} chars)`);
|
| 47 |
+
console.log(`[DEBUG] First 100 chars: ${code.substring(0, 100).replace(/\n/g, '\\n')}...`);
|
| 48 |
+
|
| 49 |
+
const response = await axios.post(
|
| 50 |
+
webhookUrl,
|
| 51 |
+
{ code },
|
| 52 |
+
{
|
| 53 |
+
timeout: timeoutSeconds * 1000,
|
| 54 |
+
headers: {
|
| 55 |
+
'Content-Type': 'application/json'
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
);
|
| 59 |
+
|
| 60 |
+
console.log(`[DEBUG] n8n response status: ${response.status}`);
|
| 61 |
+
console.log(`[DEBUG] Response preview: ${JSON.stringify(response.data).substring(0, 100)}...`);
|
| 62 |
+
|
| 63 |
+
if (response.status >= 500) {
|
| 64 |
+
throw new N8NWebhookUpstreamError(`n8n webhook returned server error ${response.status}`);
|
| 65 |
+
}
|
| 66 |
+
if (response.status >= 400) {
|
| 67 |
+
throw new N8NWebhookUpstreamError(`n8n webhook returned unexpected status ${response.status}`);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
const payload = response.data;
|
| 71 |
+
const correctedCode = payload.corrected_code;
|
| 72 |
+
|
| 73 |
+
if (typeof correctedCode !== 'string') {
|
| 74 |
+
throw new N8NWebhookResponseError("n8n webhook response missing 'corrected_code' string");
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
return correctedCode;
|
| 78 |
+
} catch (error) {
|
| 79 |
+
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
| 80 |
+
throw new N8NWebhookTimeoutError('n8n webhook request timed out');
|
| 81 |
+
}
|
| 82 |
+
if (error.response) {
|
| 83 |
+
if (error.response.status >= 500) {
|
| 84 |
+
throw new N8NWebhookUpstreamError(`n8n webhook returned server error ${error.response.status}`);
|
| 85 |
+
}
|
| 86 |
+
if (error.response.status >= 400) {
|
| 87 |
+
throw new N8NWebhookUpstreamError(`n8n webhook returned unexpected status ${error.response.status}`);
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
if (error instanceof N8NWebhookError) {
|
| 91 |
+
throw error;
|
| 92 |
+
}
|
| 93 |
+
throw new N8NWebhookUpstreamError(`Failed to call n8n webhook: ${error.message}`);
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
module.exports = {
|
| 98 |
+
N8NWebhookError,
|
| 99 |
+
N8NWebhookTimeoutError,
|
| 100 |
+
N8NWebhookUpstreamError,
|
| 101 |
+
N8NWebhookResponseError,
|
| 102 |
+
patchCodeViaN8n
|
| 103 |
+
};
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "cerberus",
|
| 3 |
+
"displayName": "cerberus",
|
| 4 |
+
"description": "Your AI-powered security co-pilot that does not just find vulnerabilities, it fixes them.",
|
| 5 |
+
"version": "0.0.1",
|
| 6 |
+
"engines": {
|
| 7 |
+
"vscode": "^1.109.0"
|
| 8 |
+
},
|
| 9 |
+
"categories": [
|
| 10 |
+
"Other"
|
| 11 |
+
],
|
| 12 |
+
"activationEvents": [
|
| 13 |
+
"onStartupFinished"
|
| 14 |
+
],
|
| 15 |
+
"main": "./out/extension.js",
|
| 16 |
+
"contributes": {
|
| 17 |
+
"commands": [
|
| 18 |
+
{
|
| 19 |
+
"command": "cerberus.scan",
|
| 20 |
+
"title": "Scan for Vulnerabilities",
|
| 21 |
+
"category": "Cerberus",
|
| 22 |
+
"icon": "$(shield)"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"command": "cerberus.fixVulnerability",
|
| 26 |
+
"title": "Apply Fix",
|
| 27 |
+
"category": "Cerberus",
|
| 28 |
+
"icon": "$(wrench)"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"command": "cerberus.fixFile",
|
| 32 |
+
"title": "Fix All in File",
|
| 33 |
+
"category": "Cerberus",
|
| 34 |
+
"icon": "$(replace-all)"
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"command": "cerberus.viewResults",
|
| 38 |
+
"title": "View Results",
|
| 39 |
+
"category": "Cerberus"
|
| 40 |
+
}
|
| 41 |
+
],
|
| 42 |
+
"menus": {
|
| 43 |
+
"view/title": [
|
| 44 |
+
{
|
| 45 |
+
"command": "cerberus.scan",
|
| 46 |
+
"when": "view == cerberus.vulnerabilityView",
|
| 47 |
+
"group": "navigation"
|
| 48 |
+
}
|
| 49 |
+
],
|
| 50 |
+
"view/item/context": [
|
| 51 |
+
{
|
| 52 |
+
"command": "cerberus.fixVulnerability",
|
| 53 |
+
"when": "view == cerberus.vulnerabilityView && viewItem == vulnerability",
|
| 54 |
+
"group": "inline"
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"command": "cerberus.fixFile",
|
| 58 |
+
"when": "view == cerberus.vulnerabilityView && viewItem == file",
|
| 59 |
+
"group": "inline"
|
| 60 |
+
}
|
| 61 |
+
]
|
| 62 |
+
},
|
| 63 |
+
"views": {
|
| 64 |
+
"cerberus-explorer": [
|
| 65 |
+
{
|
| 66 |
+
"id": "cerberus.vulnerabilityView",
|
| 67 |
+
"name": "Vulnerabilities"
|
| 68 |
+
}
|
| 69 |
+
]
|
| 70 |
+
},
|
| 71 |
+
"viewsContainers": {
|
| 72 |
+
"activitybar": [
|
| 73 |
+
{
|
| 74 |
+
"id": "cerberus-explorer",
|
| 75 |
+
"title": "Cerberus",
|
| 76 |
+
"icon": "$(shield)"
|
| 77 |
+
}
|
| 78 |
+
]
|
| 79 |
+
}
|
| 80 |
+
},
|
| 81 |
+
"scripts": {
|
| 82 |
+
"vscode:prepublish": "npm run compile",
|
| 83 |
+
"compile": "tsc -p ./",
|
| 84 |
+
"watch": "tsc -watch -p ./",
|
| 85 |
+
"pretest": "npm run compile && npm run lint",
|
| 86 |
+
"lint": "eslint src",
|
| 87 |
+
"test": "vscode-test",
|
| 88 |
+
"server:start": "node server.js",
|
| 89 |
+
"server:dev": "nodemon server.js"
|
| 90 |
+
},
|
| 91 |
+
"devDependencies": {
|
| 92 |
+
"@types/mocha": "^10.0.10",
|
| 93 |
+
"@types/node": "22.x",
|
| 94 |
+
"@types/vscode": "^1.109.0",
|
| 95 |
+
"@vscode/test-cli": "^0.0.12",
|
| 96 |
+
"@vscode/test-electron": "^2.5.2",
|
| 97 |
+
"eslint": "^9.39.2",
|
| 98 |
+
"jest": "^29.7.0",
|
| 99 |
+
"nodemon": "^3.0.2",
|
| 100 |
+
"typescript": "^5.9.3",
|
| 101 |
+
"typescript-eslint": "^8.54.0"
|
| 102 |
+
},
|
| 103 |
+
"dependencies": {
|
| 104 |
+
"axios": "^1.13.5",
|
| 105 |
+
"cors": "^2.8.5",
|
| 106 |
+
"dotenv": "^16.3.1",
|
| 107 |
+
"express": "^4.18.2",
|
| 108 |
+
"express-rate-limit": "^7.1.5",
|
| 109 |
+
"glob": "^10.3.0"
|
| 110 |
+
},
|
| 111 |
+
"overrides": {
|
| 112 |
+
"diff": "^8.0.3",
|
| 113 |
+
"serialize-javascript": "^7.0.3"
|
| 114 |
+
}
|
| 115 |
+
}
|
requirements.txt
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
Flask>=3.0.0,<4.0.0
|
| 2 |
-
flask-cors>=4.0.0,<5.0.0
|
| 3 |
-
requests>=2.31.0,<3.0.0
|
| 4 |
-
python-dotenv>=1.0.0,<2.0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
routes.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* API Routes
|
| 3 |
+
* Express routes for the Cerberus security scanner API
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
const express = require('express');
|
| 7 |
+
const fs = require('fs').promises;
|
| 8 |
+
const path = require('path');
|
| 9 |
+
const { glob } = require('glob');
|
| 10 |
+
const rateLimit = require('express-rate-limit');
|
| 11 |
+
const {
|
| 12 |
+
patchCodeViaN8n,
|
| 13 |
+
N8NWebhookTimeoutError,
|
| 14 |
+
N8NWebhookUpstreamError,
|
| 15 |
+
N8NWebhookResponseError
|
| 16 |
+
} = require('./n8nClient');
|
| 17 |
+
|
| 18 |
+
const router = express.Router();
|
| 19 |
+
|
| 20 |
+
// Rate limiting for scan endpoint (10 requests per 15 minutes per IP)
|
| 21 |
+
const scanLimiter = rateLimit({
|
| 22 |
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
| 23 |
+
max: 10, // limit each IP to 10 requests per windowMs
|
| 24 |
+
message: {
|
| 25 |
+
error: 'Too many requests',
|
| 26 |
+
message: 'Too many scan requests from this IP, please try again after 15 minutes'
|
| 27 |
+
},
|
| 28 |
+
standardHeaders: true,
|
| 29 |
+
legacyHeaders: false
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Validate and sanitize folder path to prevent directory traversal
|
| 34 |
+
* @param {string} inputPath - The user-provided path
|
| 35 |
+
* @returns {Object} - { isValid: boolean, resolvedPath?: string, error?: string }
|
| 36 |
+
*/
|
| 37 |
+
function validateScanPath(inputPath) {
|
| 38 |
+
// Check for null bytes and path traversal attempts
|
| 39 |
+
if (inputPath.includes('\0')) {
|
| 40 |
+
return { isValid: false, error: 'Path contains null bytes' };
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Resolve the absolute path
|
| 44 |
+
const resolvedPath = path.resolve(inputPath);
|
| 45 |
+
|
| 46 |
+
// Define allowed base directory (default to current working directory)
|
| 47 |
+
const allowedBase = path.resolve(process.env.SCAN_BASE_PATH || process.cwd());
|
| 48 |
+
|
| 49 |
+
// Ensure the resolved path is within the allowed base directory
|
| 50 |
+
if (!resolvedPath.startsWith(allowedBase)) {
|
| 51 |
+
return { isValid: false, error: 'Path is outside allowed directory' };
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
return { isValid: true, resolvedPath };
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* GET /api/health
|
| 59 |
+
* Health check endpoint
|
| 60 |
+
*/
|
| 61 |
+
router.get('/api/health', (req, res) => {
|
| 62 |
+
res.json({ status: 'ok' });
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
/**
|
| 66 |
+
* POST /api/patch-code
|
| 67 |
+
* Send code to n8n for vulnerability analysis and correction
|
| 68 |
+
*/
|
| 69 |
+
router.post('/api/patch-code', async (req, res) => {
|
| 70 |
+
// Validate request is JSON
|
| 71 |
+
if (!req.is('application/json')) {
|
| 72 |
+
return res.status(400).json({
|
| 73 |
+
error: 'Invalid payload',
|
| 74 |
+
message: "Request must be JSON: {'code': 'raw python string'}"
|
| 75 |
+
});
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
const { code } = req.body;
|
| 79 |
+
|
| 80 |
+
// Validate code field
|
| 81 |
+
if (typeof code !== 'string') {
|
| 82 |
+
return res.status(400).json({
|
| 83 |
+
error: 'Invalid payload',
|
| 84 |
+
message: "Field 'code' is required and must be a string."
|
| 85 |
+
});
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
console.log(`Processing patch request code_length=${code.length}`);
|
| 89 |
+
|
| 90 |
+
try {
|
| 91 |
+
const correctedCode = await patchCodeViaN8n({
|
| 92 |
+
code,
|
| 93 |
+
webhookUrl: process.env.N8N_WEBHOOK_URL,
|
| 94 |
+
timeoutSeconds: parseFloat(process.env.N8N_TIMEOUT_SECONDS || '20')
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
return res.json({ corrected_code: correctedCode });
|
| 98 |
+
} catch (error) {
|
| 99 |
+
console.error('n8n webhook call failed:', error);
|
| 100 |
+
|
| 101 |
+
if (error instanceof N8NWebhookTimeoutError ||
|
| 102 |
+
error instanceof N8NWebhookUpstreamError ||
|
| 103 |
+
error instanceof N8NWebhookResponseError) {
|
| 104 |
+
return res.status(502).json({
|
| 105 |
+
error: 'Bad Gateway',
|
| 106 |
+
message: 'Unable to retrieve corrected code from n8n webhook.'
|
| 107 |
+
});
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
return res.status(500).json({
|
| 111 |
+
error: 'Internal Server Error',
|
| 112 |
+
message: 'An unexpected error occurred.'
|
| 113 |
+
});
|
| 114 |
+
}
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
/**
|
| 118 |
+
* POST /api/scan
|
| 119 |
+
* Scan a folder for vulnerabilities
|
| 120 |
+
*/
|
| 121 |
+
router.post('/api/scan', scanLimiter, async (req, res) => {
|
| 122 |
+
// Validate request is JSON
|
| 123 |
+
if (!req.is('application/json')) {
|
| 124 |
+
return res.status(400).json({
|
| 125 |
+
error: 'Invalid payload',
|
| 126 |
+
message: "Request must be JSON: {'path': 'folder path'}"
|
| 127 |
+
});
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
const { path: folderPath } = req.body;
|
| 131 |
+
|
| 132 |
+
// Validate path field
|
| 133 |
+
if (typeof folderPath !== 'string') {
|
| 134 |
+
return res.status(400).json({
|
| 135 |
+
error: 'Invalid payload',
|
| 136 |
+
message: "Field 'path' is required and must be a string."
|
| 137 |
+
});
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// Resolve the path (allow any directory for local testing)
|
| 141 |
+
const sanitizedPath = path.resolve(folderPath);
|
| 142 |
+
|
| 143 |
+
// Check if path exists and is a directory
|
| 144 |
+
try {
|
| 145 |
+
const stats = await fs.stat(sanitizedPath);
|
| 146 |
+
if (!stats.isDirectory()) {
|
| 147 |
+
return res.status(400).json({
|
| 148 |
+
error: 'Invalid path',
|
| 149 |
+
message: `Path is not a directory: ${folderPath}`
|
| 150 |
+
});
|
| 151 |
+
}
|
| 152 |
+
} catch (error) {
|
| 153 |
+
return res.status(400).json({
|
| 154 |
+
error: 'Invalid path',
|
| 155 |
+
message: `Path does not exist or is not a directory: ${folderPath}`
|
| 156 |
+
});
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
console.log(`Scanning folder: ${sanitizedPath}`);
|
| 160 |
+
|
| 161 |
+
// Collect all code files
|
| 162 |
+
const extensions = ['*.py', '*.js', '*.ts', '*.tsx', '*.jsx', '*.java', '*.go', '*.rb', '*.php'];
|
| 163 |
+
const codeFiles = [];
|
| 164 |
+
|
| 165 |
+
for (const ext of extensions) {
|
| 166 |
+
const pattern = path.join(sanitizedPath, '**', ext).replace(/\\/g, '/');
|
| 167 |
+
const files = await glob(pattern, { nodir: true });
|
| 168 |
+
codeFiles.push(...files);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
const vulnerabilities = [];
|
| 172 |
+
|
| 173 |
+
// Scan each file
|
| 174 |
+
for (const filePath of codeFiles) {
|
| 175 |
+
try {
|
| 176 |
+
const code = await fs.readFile(filePath, 'utf-8');
|
| 177 |
+
console.log(`Analyzing file: ${filePath}`);
|
| 178 |
+
|
| 179 |
+
try {
|
| 180 |
+
const result = await patchCodeViaN8n({
|
| 181 |
+
code,
|
| 182 |
+
webhookUrl: process.env.N8N_WEBHOOK_URL,
|
| 183 |
+
timeoutSeconds: parseFloat(process.env.N8N_TIMEOUT_SECONDS || '20')
|
| 184 |
+
});
|
| 185 |
+
|
| 186 |
+
vulnerabilities.push({
|
| 187 |
+
file: filePath,
|
| 188 |
+
status: 'analyzed',
|
| 189 |
+
result
|
| 190 |
+
});
|
| 191 |
+
} catch (error) {
|
| 192 |
+
if (error instanceof N8NWebhookTimeoutError ||
|
| 193 |
+
error instanceof N8NWebhookUpstreamError ||
|
| 194 |
+
error instanceof N8NWebhookResponseError) {
|
| 195 |
+
vulnerabilities.push({
|
| 196 |
+
file: filePath,
|
| 197 |
+
status: 'error',
|
| 198 |
+
error: error.message
|
| 199 |
+
});
|
| 200 |
+
} else {
|
| 201 |
+
throw error;
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
} catch (error) {
|
| 205 |
+
console.error(`Error reading file ${filePath}:`, error.message);
|
| 206 |
+
vulnerabilities.push({
|
| 207 |
+
file: filePath,
|
| 208 |
+
status: 'error',
|
| 209 |
+
error: error.message
|
| 210 |
+
});
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
return res.json({
|
| 215 |
+
scan_complete: true,
|
| 216 |
+
files_scanned: codeFiles.length,
|
| 217 |
+
vulnerabilities
|
| 218 |
+
});
|
| 219 |
+
});
|
| 220 |
+
|
| 221 |
+
/**
|
| 222 |
+
* POST /api/scan-file
|
| 223 |
+
* Scan a single file for vulnerabilities
|
| 224 |
+
*/
|
| 225 |
+
router.post('/api/scan-file', async (req, res) => {
|
| 226 |
+
// Validate request is JSON
|
| 227 |
+
if (!req.is('application/json')) {
|
| 228 |
+
return res.status(400).json({
|
| 229 |
+
error: 'Invalid payload',
|
| 230 |
+
message: "Request must be JSON: {'path': 'file path', 'code': 'file content'}"
|
| 231 |
+
});
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
const { path: filePath, code } = req.body;
|
| 235 |
+
|
| 236 |
+
// Validate required fields
|
| 237 |
+
if (typeof filePath !== 'string' || typeof code !== 'string') {
|
| 238 |
+
return res.status(400).json({
|
| 239 |
+
error: 'Invalid payload',
|
| 240 |
+
message: "Fields 'path' and 'code' are required and must be strings."
|
| 241 |
+
});
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
console.log(`Scanning single file: ${filePath} (${code.length} chars)`);
|
| 245 |
+
|
| 246 |
+
const vulnerabilities = [];
|
| 247 |
+
|
| 248 |
+
try {
|
| 249 |
+
const result = await patchCodeViaN8n({
|
| 250 |
+
code,
|
| 251 |
+
webhookUrl: process.env.N8N_WEBHOOK_URL,
|
| 252 |
+
timeoutSeconds: parseFloat(process.env.N8N_TIMEOUT_SECONDS || '20')
|
| 253 |
+
});
|
| 254 |
+
|
| 255 |
+
vulnerabilities.push({
|
| 256 |
+
file: filePath,
|
| 257 |
+
status: 'analyzed',
|
| 258 |
+
result
|
| 259 |
+
});
|
| 260 |
+
|
| 261 |
+
console.log(`✅ File analyzed successfully: ${filePath}`);
|
| 262 |
+
} catch (error) {
|
| 263 |
+
console.error(`❌ Error analyzing file ${filePath}:`, error.message);
|
| 264 |
+
|
| 265 |
+
if (error instanceof N8NWebhookTimeoutError) {
|
| 266 |
+
vulnerabilities.push({
|
| 267 |
+
file: filePath,
|
| 268 |
+
status: 'error',
|
| 269 |
+
error: 'Analysis timed out'
|
| 270 |
+
});
|
| 271 |
+
} else if (error instanceof N8NWebhookUpstreamError ||
|
| 272 |
+
error instanceof N8NWebhookResponseError) {
|
| 273 |
+
vulnerabilities.push({
|
| 274 |
+
file: filePath,
|
| 275 |
+
status: 'error',
|
| 276 |
+
error: error.message
|
| 277 |
+
});
|
| 278 |
+
} else {
|
| 279 |
+
vulnerabilities.push({
|
| 280 |
+
file: filePath,
|
| 281 |
+
status: 'error',
|
| 282 |
+
error: 'Unexpected error during analysis'
|
| 283 |
+
});
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
res.json({
|
| 288 |
+
files_scanned: 1,
|
| 289 |
+
vulnerabilities
|
| 290 |
+
});
|
| 291 |
+
});
|
| 292 |
+
|
| 293 |
+
module.exports = router;
|
server.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Cerberus Security Scanner Server
|
| 3 |
+
* Express.js backend that handles vulnerability scanning via n8n webhook
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
require('dotenv').config();
|
| 7 |
+
const express = require('express');
|
| 8 |
+
const cors = require('cors');
|
| 9 |
+
const routes = require('./routes');
|
| 10 |
+
|
| 11 |
+
const app = express();
|
| 12 |
+
const PORT = process.env.PORT || 5000;
|
| 13 |
+
|
| 14 |
+
// Middleware
|
| 15 |
+
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
| 16 |
+
? process.env.ALLOWED_ORIGINS.split(',')
|
| 17 |
+
: ['http://localhost:3000', 'http://localhost:5173'];
|
| 18 |
+
|
| 19 |
+
app.use(cors({
|
| 20 |
+
origin: (origin, callback) => {
|
| 21 |
+
// Allow requests with no origin (like mobile apps, curl, VS Code extensions, etc.)
|
| 22 |
+
if (!origin) return callback(null, true);
|
| 23 |
+
// Allow VS Code extension hosts
|
| 24 |
+
if (origin.startsWith('vscode-webview://') || origin.startsWith('vscode-file://')) {
|
| 25 |
+
return callback(null, true);
|
| 26 |
+
}
|
| 27 |
+
if (allowedOrigins.includes(origin)) {
|
| 28 |
+
return callback(null, true);
|
| 29 |
+
}
|
| 30 |
+
// Log blocked origins for debugging
|
| 31 |
+
console.log(`CORS blocked origin: ${origin}`);
|
| 32 |
+
callback(new Error('Not allowed by CORS'));
|
| 33 |
+
},
|
| 34 |
+
methods: ['GET', 'POST'],
|
| 35 |
+
allowedHeaders: ['Content-Type']
|
| 36 |
+
}));
|
| 37 |
+
app.use(express.json());
|
| 38 |
+
|
| 39 |
+
// Request logging middleware
|
| 40 |
+
app.use((req, res, next) => {
|
| 41 |
+
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} - ${req.ip}`);
|
| 42 |
+
next();
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
// Register routes
|
| 46 |
+
app.use(routes);
|
| 47 |
+
|
| 48 |
+
// Error handling middleware
|
| 49 |
+
app.use((err, req, res, next) => {
|
| 50 |
+
console.error('Unhandled error:', err);
|
| 51 |
+
res.status(500).json({
|
| 52 |
+
error: 'Internal Server Error',
|
| 53 |
+
message: 'An unexpected error occurred'
|
| 54 |
+
});
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
// Start server
|
| 58 |
+
app.listen(PORT, '0.0.0.0', () => {
|
| 59 |
+
console.log(`🚀 Cerberus server running on http://0.0.0.0:${PORT}`);
|
| 60 |
+
console.log(`📡 n8n webhook: ${process.env.N8N_WEBHOOK_URL || 'https://n8n.shravanpandala.me/webhook/scan'}`);
|
| 61 |
+
console.log(`⏱️ timeout: ${process.env.N8N_TIMEOUT_SECONDS || '20'}s`);
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
module.exports = app;
|
src/extension.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as vscode from 'vscode';
|
| 2 |
+
import * as fs from 'fs';
|
| 3 |
+
import * as path from 'path';
|
| 4 |
+
import axios from 'axios';
|
| 5 |
+
import { VulnerabilityTreeDataProvider, Vulnerability, VulnerabilityItem } from './vulnerabilityTree';
|
| 6 |
+
|
| 7 |
+
const BACKEND_URL = 'http://localhost:5000';
|
| 8 |
+
|
| 9 |
+
// Global reference to the tree provider
|
| 10 |
+
let vulnerabilityProvider: VulnerabilityTreeDataProvider;
|
| 11 |
+
|
| 12 |
+
// This method is called when your extension is activated
|
| 13 |
+
export function activate(context: vscode.ExtensionContext) {
|
| 14 |
+
console.log('Cerberus security extension is now active!');
|
| 15 |
+
|
| 16 |
+
// Create and register the tree data provider
|
| 17 |
+
vulnerabilityProvider = new VulnerabilityTreeDataProvider();
|
| 18 |
+
vscode.window.createTreeView('cerberus.vulnerabilityView', {
|
| 19 |
+
treeDataProvider: vulnerabilityProvider,
|
| 20 |
+
showCollapseAll: true,
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
// Register the scan command - scans the currently active file
|
| 24 |
+
const scanDisposable = vscode.commands.registerCommand('cerberus.scan', async () => {
|
| 25 |
+
// Get the active text editor
|
| 26 |
+
const activeEditor = vscode.window.activeTextEditor;
|
| 27 |
+
if (!activeEditor) {
|
| 28 |
+
vscode.window.showErrorMessage('Please open a file to scan.');
|
| 29 |
+
return;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const filePath = activeEditor.document.uri.fsPath;
|
| 33 |
+
const fileName = path.basename(filePath);
|
| 34 |
+
|
| 35 |
+
vscode.window.showInformationMessage(`🔍 Cerberus: Scanning ${fileName}...`);
|
| 36 |
+
|
| 37 |
+
try {
|
| 38 |
+
// Read the file content
|
| 39 |
+
const code = activeEditor.document.getText();
|
| 40 |
+
|
| 41 |
+
// Call the backend server to scan this specific file
|
| 42 |
+
const response = await axios.post(`${BACKEND_URL}/api/scan-file`, {
|
| 43 |
+
path: filePath,
|
| 44 |
+
code: code
|
| 45 |
+
}, {
|
| 46 |
+
timeout: 60000 // 1 minute timeout for single file
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
// Extract vulnerabilities from response
|
| 50 |
+
const data = response.data;
|
| 51 |
+
const vulnerabilities: Vulnerability[] = data.vulnerabilities || [];
|
| 52 |
+
|
| 53 |
+
// Update the tree view with results
|
| 54 |
+
vulnerabilityProvider.setVulnerabilities(vulnerabilities);
|
| 55 |
+
|
| 56 |
+
const analyzedCount = vulnerabilities.filter((v: Vulnerability) => v.status === 'analyzed').length;
|
| 57 |
+
const errorCount = vulnerabilities.filter((v: Vulnerability) => v.status === 'error').length;
|
| 58 |
+
|
| 59 |
+
if (analyzedCount > 0) {
|
| 60 |
+
vscode.window.showInformationMessage(
|
| 61 |
+
`✅ Cerberus: Found ${analyzedCount} issues in ${fileName}.`
|
| 62 |
+
);
|
| 63 |
+
} else if (errorCount > 0) {
|
| 64 |
+
vscode.window.showWarningMessage(
|
| 65 |
+
`⚠️ Cerberus: ${errorCount} errors while scanning ${fileName}.`
|
| 66 |
+
);
|
| 67 |
+
} else {
|
| 68 |
+
vscode.window.showInformationMessage(
|
| 69 |
+
`✅ Cerberus: No vulnerabilities found in ${fileName}.`
|
| 70 |
+
);
|
| 71 |
+
}
|
| 72 |
+
} catch (error: any) {
|
| 73 |
+
let errorMessage = `❌ Cerberus: Failed to connect to backend server at ${BACKEND_URL}.`;
|
| 74 |
+
|
| 75 |
+
if (error.code === 'ECONNREFUSED') {
|
| 76 |
+
errorMessage += ' Is the server running? (npm run server:start)';
|
| 77 |
+
} else if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
|
| 78 |
+
errorMessage += ' Request timed out. The scan may be taking too long.';
|
| 79 |
+
} else if (error.response) {
|
| 80 |
+
// Server responded with error status
|
| 81 |
+
errorMessage += ` Server error: ${error.response.status} - ${JSON.stringify(error.response.data)}`;
|
| 82 |
+
} else if (error.request) {
|
| 83 |
+
// Request was made but no response
|
| 84 |
+
errorMessage += ' No response from server.';
|
| 85 |
+
} else {
|
| 86 |
+
errorMessage += ` ${error.message}`;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
vscode.window.showErrorMessage(errorMessage);
|
| 90 |
+
console.error('Scan error details:', error);
|
| 91 |
+
}
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
// Register the fix command - applies fix to currently active file
|
| 95 |
+
const fixDisposable = vscode.commands.registerCommand('cerberus.fixVulnerability', async (item?: VulnerabilityItem) => {
|
| 96 |
+
if (!item || !item.vulnerability) {
|
| 97 |
+
vscode.window.showWarningMessage('Please select a vulnerability from the Cerberus panel to fix.');
|
| 98 |
+
return;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
const vulnerability = item.vulnerability;
|
| 102 |
+
|
| 103 |
+
if (vulnerability.status !== 'analyzed' || !vulnerability.result) {
|
| 104 |
+
vscode.window.showErrorMessage('No fix available for this vulnerability.');
|
| 105 |
+
return;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
await applyFix(vulnerability.file, vulnerability.result);
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
// Register command to fix all vulnerabilities in a file
|
| 112 |
+
const fixFileDisposable = vscode.commands.registerCommand('cerberus.fixFile', async (item?: VulnerabilityItem) => {
|
| 113 |
+
if (!item) {
|
| 114 |
+
vscode.window.showWarningMessage('Please select a file from the Cerberus panel.');
|
| 115 |
+
return;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Get all vulnerabilities for this file
|
| 119 |
+
const fileVulns = vulnerabilityProvider.getVulnerabilitiesForFile(item.label);
|
| 120 |
+
|
| 121 |
+
if (fileVulns.length === 0) {
|
| 122 |
+
vscode.window.showWarningMessage('No vulnerabilities found for this file.');
|
| 123 |
+
return;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// Apply the first available fix (or we could merge multiple fixes)
|
| 127 |
+
const fixableVuln = fileVulns.find(v => v.status === 'analyzed' && v.result);
|
| 128 |
+
if (!fixableVuln) {
|
| 129 |
+
vscode.window.showErrorMessage('No fixes available for this file.');
|
| 130 |
+
return;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
await applyFix(fixableVuln.file, fixableVuln.result!);
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
// Register the view results command
|
| 137 |
+
const viewDisposable = vscode.commands.registerCommand('cerberus.viewResults', () => {
|
| 138 |
+
vscode.commands.executeCommand('cerberus.vulnerabilityView.focus');
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
context.subscriptions.push(scanDisposable, fixDisposable, fixFileDisposable, viewDisposable);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
async function applyFix(filePath: string, correctedCode: string) {
|
| 145 |
+
try {
|
| 146 |
+
// Check if file exists
|
| 147 |
+
if (!fs.existsSync(filePath)) {
|
| 148 |
+
vscode.window.showErrorMessage(`File not found: ${filePath}`);
|
| 149 |
+
return;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// Open the document
|
| 153 |
+
const document = await vscode.workspace.openTextDocument(filePath);
|
| 154 |
+
const editor = await vscode.window.showTextDocument(document);
|
| 155 |
+
|
| 156 |
+
// Create a workspace edit to replace the entire file content
|
| 157 |
+
const edit = new vscode.WorkspaceEdit();
|
| 158 |
+
const fullRange = new vscode.Range(
|
| 159 |
+
document.positionAt(0),
|
| 160 |
+
document.positionAt(document.getText().length)
|
| 161 |
+
);
|
| 162 |
+
|
| 163 |
+
edit.replace(document.uri, fullRange, correctedCode);
|
| 164 |
+
|
| 165 |
+
// Apply the edit
|
| 166 |
+
const success = await vscode.workspace.applyEdit(edit);
|
| 167 |
+
|
| 168 |
+
if (success) {
|
| 169 |
+
// Save the document
|
| 170 |
+
await document.save();
|
| 171 |
+
vscode.window.showInformationMessage(`✅ Fixed applied and saved: ${path.basename(filePath)}`);
|
| 172 |
+
} else {
|
| 173 |
+
vscode.window.showErrorMessage('Failed to apply fix.');
|
| 174 |
+
}
|
| 175 |
+
} catch (error) {
|
| 176 |
+
vscode.window.showErrorMessage(`Error applying fix: ${error}`);
|
| 177 |
+
console.error('Fix error:', error);
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
export function deactivate() { }
|
src/test/extension.test.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as assert from 'assert';
|
| 2 |
+
|
| 3 |
+
// You can import and use all API from the 'vscode' module
|
| 4 |
+
// as well as import your extension to test it
|
| 5 |
+
import * as vscode from 'vscode';
|
| 6 |
+
// import * as myExtension from '../../extension';
|
| 7 |
+
|
| 8 |
+
suite('Extension Test Suite', () => {
|
| 9 |
+
vscode.window.showInformationMessage('Start all tests.');
|
| 10 |
+
|
| 11 |
+
test('Sample test', () => {
|
| 12 |
+
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
|
| 13 |
+
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
|
| 14 |
+
});
|
| 15 |
+
});
|
src/vulnerabilityTree.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as vscode from 'vscode';
|
| 2 |
+
|
| 3 |
+
export interface Vulnerability {
|
| 4 |
+
file: string;
|
| 5 |
+
status: string;
|
| 6 |
+
error?: string;
|
| 7 |
+
result?: string;
|
| 8 |
+
line?: number;
|
| 9 |
+
severity?: string;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export class VulnerabilityItem extends vscode.TreeItem {
|
| 13 |
+
constructor(
|
| 14 |
+
public readonly label: string,
|
| 15 |
+
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
| 16 |
+
public readonly vulnerability?: Vulnerability,
|
| 17 |
+
public readonly command?: vscode.Command,
|
| 18 |
+
contextValue?: string
|
| 19 |
+
) {
|
| 20 |
+
super(label, collapsibleState);
|
| 21 |
+
|
| 22 |
+
if (contextValue) {
|
| 23 |
+
this.contextValue = contextValue;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
if (vulnerability?.status === 'analyzed' && vulnerability?.result) {
|
| 27 |
+
this.description = 'Click to apply fix';
|
| 28 |
+
this.iconPath = new vscode.ThemeIcon('error', new vscode.ThemeColor('errorForeground'));
|
| 29 |
+
this.tooltip = vulnerability.result.substring(0, 500);
|
| 30 |
+
} else if (vulnerability?.status === 'error') {
|
| 31 |
+
this.description = 'Analysis error';
|
| 32 |
+
this.iconPath = new vscode.ThemeIcon('warning', new vscode.ThemeColor('warningForeground'));
|
| 33 |
+
this.tooltip = vulnerability.error || 'Unknown error';
|
| 34 |
+
} else {
|
| 35 |
+
this.iconPath = new vscode.ThemeIcon('file-code');
|
| 36 |
+
this.tooltip = 'Click to open file';
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export class VulnerabilityTreeDataProvider implements vscode.TreeDataProvider<VulnerabilityItem> {
|
| 42 |
+
private _onDidChangeTreeData: vscode.EventEmitter<VulnerabilityItem | undefined | null | void> =
|
| 43 |
+
new vscode.EventEmitter<VulnerabilityItem | undefined | null | void>();
|
| 44 |
+
readonly onDidChangeTreeData: vscode.Event<VulnerabilityItem | undefined | null | void> =
|
| 45 |
+
this._onDidChangeTreeData.event;
|
| 46 |
+
|
| 47 |
+
private vulnerabilities: Vulnerability[] = [];
|
| 48 |
+
|
| 49 |
+
setVulnerabilities(vulnerabilities: Vulnerability[]): void {
|
| 50 |
+
this.vulnerabilities = vulnerabilities;
|
| 51 |
+
this._onDidChangeTreeData.fire(null);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
getVulnerabilitiesForFile(fileName: string): Vulnerability[] {
|
| 55 |
+
return this.vulnerabilities.filter((v: Vulnerability) => {
|
| 56 |
+
const vulnFileName = v.file.split('\\').pop()?.split('/').pop() || v.file;
|
| 57 |
+
return vulnFileName === fileName;
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
getTreeItem(element: VulnerabilityItem): vscode.TreeItem {
|
| 62 |
+
return element;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
getChildren(element?: VulnerabilityItem): Thenable<VulnerabilityItem[]> {
|
| 66 |
+
if (!element) {
|
| 67 |
+
// Root level - show list of files with vulnerabilities
|
| 68 |
+
const fileMap = new Map<string, Vulnerability[]>();
|
| 69 |
+
|
| 70 |
+
this.vulnerabilities.forEach((vuln: Vulnerability) => {
|
| 71 |
+
if (!fileMap.has(vuln.file)) {
|
| 72 |
+
fileMap.set(vuln.file, []);
|
| 73 |
+
}
|
| 74 |
+
fileMap.get(vuln.file)!.push(vuln);
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
const items = Array.from(fileMap.entries()).map(
|
| 78 |
+
([file, vulns]) =>
|
| 79 |
+
new VulnerabilityItem(
|
| 80 |
+
this.getFileName(file),
|
| 81 |
+
vscode.TreeItemCollapsibleState.Collapsed,
|
| 82 |
+
undefined,
|
| 83 |
+
{
|
| 84 |
+
command: 'vscode.open',
|
| 85 |
+
title: 'Open file',
|
| 86 |
+
arguments: [vscode.Uri.file(file)],
|
| 87 |
+
},
|
| 88 |
+
'file'
|
| 89 |
+
)
|
| 90 |
+
);
|
| 91 |
+
|
| 92 |
+
return Promise.resolve(items);
|
| 93 |
+
} else if (element.vulnerability === undefined) {
|
| 94 |
+
// File level - show vulnerabilities in this file
|
| 95 |
+
const fileName = element.label;
|
| 96 |
+
const fileVulns = this.vulnerabilities.filter((v: Vulnerability) =>
|
| 97 |
+
this.getFileName(v.file) === fileName
|
| 98 |
+
);
|
| 99 |
+
|
| 100 |
+
const items = fileVulns.map(
|
| 101 |
+
(vuln, index) =>
|
| 102 |
+
new VulnerabilityItem(
|
| 103 |
+
`Issue ${index + 1}${vuln.status === 'error' ? ' (Error)' : ''}`,
|
| 104 |
+
vscode.TreeItemCollapsibleState.Collapsed,
|
| 105 |
+
vuln,
|
| 106 |
+
undefined,
|
| 107 |
+
vuln.status === 'analyzed' ? 'vulnerability' : undefined
|
| 108 |
+
)
|
| 109 |
+
);
|
| 110 |
+
|
| 111 |
+
return Promise.resolve(items);
|
| 112 |
+
} else {
|
| 113 |
+
// Vulnerability level - show details
|
| 114 |
+
const vuln = element.vulnerability;
|
| 115 |
+
const items: VulnerabilityItem[] = [];
|
| 116 |
+
|
| 117 |
+
if (vuln.status === 'analyzed' && vuln.result) {
|
| 118 |
+
// Show a preview of the fix
|
| 119 |
+
const lines = vuln.result.split('\n');
|
| 120 |
+
const previewLines = lines.slice(0, 10);
|
| 121 |
+
const preview = previewLines.join('\n') + (lines.length > 10 ? '\n...' : '');
|
| 122 |
+
|
| 123 |
+
items.push(
|
| 124 |
+
new VulnerabilityItem(
|
| 125 |
+
'Fixed Code Preview:',
|
| 126 |
+
vscode.TreeItemCollapsibleState.None,
|
| 127 |
+
undefined
|
| 128 |
+
)
|
| 129 |
+
);
|
| 130 |
+
|
| 131 |
+
items.push(
|
| 132 |
+
new VulnerabilityItem(
|
| 133 |
+
preview,
|
| 134 |
+
vscode.TreeItemCollapsibleState.None,
|
| 135 |
+
undefined
|
| 136 |
+
)
|
| 137 |
+
);
|
| 138 |
+
} else if (vuln.error) {
|
| 139 |
+
items.push(
|
| 140 |
+
new VulnerabilityItem(
|
| 141 |
+
`Error: ${vuln.error}`,
|
| 142 |
+
vscode.TreeItemCollapsibleState.None,
|
| 143 |
+
undefined
|
| 144 |
+
)
|
| 145 |
+
);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
return Promise.resolve(items);
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
private getFileName(filePath: string): string {
|
| 153 |
+
return filePath.split('\\').pop()?.split('/').pop() || filePath;
|
| 154 |
+
}
|
| 155 |
+
}
|
test_endpoints.py
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Tests for Cerberus API endpoints.
|
| 3 |
-
|
| 4 |
-
Uses Flask's built-in test client — no running server needed.
|
| 5 |
-
n8n webhook calls are mocked with unittest.mock.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import unittest
|
| 9 |
-
from unittest.mock import patch, MagicMock
|
| 10 |
-
|
| 11 |
-
from app import app
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class TestHealthEndpoint(unittest.TestCase):
|
| 15 |
-
"""Tests for GET /api/health."""
|
| 16 |
-
|
| 17 |
-
def setUp(self):
|
| 18 |
-
self.client = app.test_client()
|
| 19 |
-
|
| 20 |
-
def test_health_returns_ok(self):
|
| 21 |
-
resp = self.client.get("/api/health")
|
| 22 |
-
self.assertEqual(resp.status_code, 200)
|
| 23 |
-
self.assertEqual(resp.get_json(), {"status": "ok"})
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
class TestPatchCodeEndpoint(unittest.TestCase):
|
| 27 |
-
"""Tests for POST /api/patch-code."""
|
| 28 |
-
|
| 29 |
-
def setUp(self):
|
| 30 |
-
self.client = app.test_client()
|
| 31 |
-
|
| 32 |
-
# ── Validation tests (no mocking needed) ──────────────────────
|
| 33 |
-
|
| 34 |
-
def test_rejects_non_json_request(self):
|
| 35 |
-
resp = self.client.post("/api/patch-code", data="not json")
|
| 36 |
-
self.assertEqual(resp.status_code, 400)
|
| 37 |
-
self.assertIn("Invalid payload", resp.get_json()["error"])
|
| 38 |
-
|
| 39 |
-
def test_rejects_missing_code_field(self):
|
| 40 |
-
resp = self.client.post("/api/patch-code", json={"foo": "bar"})
|
| 41 |
-
self.assertEqual(resp.status_code, 400)
|
| 42 |
-
self.assertIn("code", resp.get_json()["message"])
|
| 43 |
-
|
| 44 |
-
def test_rejects_non_string_code(self):
|
| 45 |
-
resp = self.client.post("/api/patch-code", json={"code": 123})
|
| 46 |
-
self.assertEqual(resp.status_code, 400)
|
| 47 |
-
|
| 48 |
-
# ── Success test (mock the n8n webhook) ───────────────────────
|
| 49 |
-
|
| 50 |
-
@patch("cerberus_api.n8n_client.requests.post")
|
| 51 |
-
def test_success_returns_corrected_code(self, mock_post):
|
| 52 |
-
# Simulate a successful n8n webhook response
|
| 53 |
-
mock_response = MagicMock()
|
| 54 |
-
mock_response.status_code = 200
|
| 55 |
-
mock_response.json.return_value = {
|
| 56 |
-
"corrected_code": "import os\nprint(os.getenv('SECRET'))"
|
| 57 |
-
}
|
| 58 |
-
mock_post.return_value = mock_response
|
| 59 |
-
|
| 60 |
-
resp = self.client.post(
|
| 61 |
-
"/api/patch-code",
|
| 62 |
-
json={"code": "print('hello')"},
|
| 63 |
-
)
|
| 64 |
-
|
| 65 |
-
self.assertEqual(resp.status_code, 200)
|
| 66 |
-
data = resp.get_json()
|
| 67 |
-
self.assertIn("corrected_code", data)
|
| 68 |
-
self.assertIsInstance(data["corrected_code"], str)
|
| 69 |
-
|
| 70 |
-
# ── Error tests (mock failures) ───────────────────────────────
|
| 71 |
-
|
| 72 |
-
@patch("cerberus_api.n8n_client.requests.post")
|
| 73 |
-
def test_webhook_timeout_returns_502(self, mock_post):
|
| 74 |
-
from requests.exceptions import Timeout
|
| 75 |
-
|
| 76 |
-
mock_post.side_effect = Timeout("timed out")
|
| 77 |
-
|
| 78 |
-
resp = self.client.post(
|
| 79 |
-
"/api/patch-code",
|
| 80 |
-
json={"code": "x = 1"},
|
| 81 |
-
)
|
| 82 |
-
|
| 83 |
-
self.assertEqual(resp.status_code, 502)
|
| 84 |
-
self.assertIn("Bad Gateway", resp.get_json()["error"])
|
| 85 |
-
|
| 86 |
-
@patch("cerberus_api.n8n_client.requests.post")
|
| 87 |
-
def test_webhook_server_error_returns_502(self, mock_post):
|
| 88 |
-
mock_response = MagicMock()
|
| 89 |
-
mock_response.status_code = 500
|
| 90 |
-
mock_post.return_value = mock_response
|
| 91 |
-
|
| 92 |
-
resp = self.client.post(
|
| 93 |
-
"/api/patch-code",
|
| 94 |
-
json={"code": "x = 1"},
|
| 95 |
-
)
|
| 96 |
-
|
| 97 |
-
self.assertEqual(resp.status_code, 502)
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
if __name__ == "__main__":
|
| 101 |
-
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"module": "Node16",
|
| 4 |
+
"target": "ES2022",
|
| 5 |
+
"outDir": "out",
|
| 6 |
+
"lib": [
|
| 7 |
+
"ES2022"
|
| 8 |
+
],
|
| 9 |
+
"sourceMap": true,
|
| 10 |
+
"rootDir": "src",
|
| 11 |
+
"strict": true, /* enable all strict type-checking options */
|
| 12 |
+
/* Additional Checks */
|
| 13 |
+
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
| 14 |
+
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
| 15 |
+
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
| 16 |
+
}
|
| 17 |
+
}
|
vsc-extension-quickstart.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Welcome to your VS Code Extension
|
| 2 |
+
|
| 3 |
+
## What's in the folder
|
| 4 |
+
|
| 5 |
+
* This folder contains all of the files necessary for your extension.
|
| 6 |
+
* `package.json` - this is the manifest file in which you declare your extension and command.
|
| 7 |
+
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
|
| 8 |
+
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
|
| 9 |
+
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
|
| 10 |
+
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
|
| 11 |
+
|
| 12 |
+
## Get up and running straight away
|
| 13 |
+
|
| 14 |
+
* Press `F5` to open a new window with your extension loaded.
|
| 15 |
+
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
|
| 16 |
+
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
|
| 17 |
+
* Find output from your extension in the debug console.
|
| 18 |
+
|
| 19 |
+
## Make changes
|
| 20 |
+
|
| 21 |
+
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
|
| 22 |
+
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
|
| 23 |
+
|
| 24 |
+
## Explore the API
|
| 25 |
+
|
| 26 |
+
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
|
| 27 |
+
|
| 28 |
+
## Run tests
|
| 29 |
+
|
| 30 |
+
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
|
| 31 |
+
* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered.
|
| 32 |
+
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
|
| 33 |
+
* See the output of the test result in the Test Results view.
|
| 34 |
+
* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder.
|
| 35 |
+
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
|
| 36 |
+
* You can create folders inside the `test` folder to structure your tests any way you want.
|
| 37 |
+
|
| 38 |
+
## Go further
|
| 39 |
+
|
| 40 |
+
* [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns.
|
| 41 |
+
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
|
| 42 |
+
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
|
| 43 |
+
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
|
| 44 |
+
* Integrate to the [report issue](https://code.visualstudio.com/api/get-started/wrapping-up#issue-reporting) flow to get issue and feature requests reported by users.
|