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 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
- # Byte-compiled / optimized / DLL files
2
- __pycache__/
3
- *.py[codz]
4
- *$py.class
5
 
6
- # C extensions
7
- *.so
 
8
 
9
- # Distribution / packaging
10
- .Python
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
- # PyInstaller
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
- # pytype static type analyzer
166
- .pytype/
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
- # Cursor
198
- # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
- # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
- # refer to https://docs.cursor.com/context/ignore-files
201
- .cursorignore
202
- .cursorindexingignore
203
 
204
- # Marimo
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.