diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml deleted file mode 100644 index c8350c9..0000000 --- a/.github/workflows/ci-frontend.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Frontend CI - -on: - push: - branches: [dev] - paths: - - 'frontend/**' - pull_request: - branches: [dev, master] - paths: - - 'frontend/**' - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - cache-dependency-path: frontend/package-lock.json - - - name: Install dependencies - run: npm ci - working-directory: frontend - - - name: Type check - run: npx tsc --noEmit - working-directory: frontend - - - name: Build - run: npm run build - working-directory: frontend - env: - GITHUB_PAGES: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ab973e..340da36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,12 +74,33 @@ jobs: env: GITHUB_PAGES: true + # ── VS Code extension ───────────────────────────────────────────────────────── + vscode-extension: + name: VS Code extension · type check & build + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: vscode-extension/package-lock.json + - name: Install dependencies + run: npm ci + working-directory: vscode-extension + - name: Compile + run: npm run compile + working-directory: vscode-extension + # ── PR scan (pull requests only) ────────────────────────────────────────────── pr-scan: name: KShield · scan changed files runs-on: ubuntu-22.04 if: github.event_name == 'pull_request' needs: [backend] + permissions: + contents: read + pull-requests: write steps: - uses: actions/checkout@v4 with: @@ -99,7 +120,9 @@ jobs: - name: Get changed files run: | git diff --name-only origin/${{ github.base_ref }}...HEAD \ - --diff-filter=ACM > /tmp/changed_files.txt + --diff-filter=ACM \ + | grep -vE '(^|/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock)$' \ + > /tmp/changed_files.txt || true cat /tmp/changed_files.txt - name: Scan changed files id: scan diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 326ed2d..61006c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -121,8 +121,8 @@ jobs: sed -i "s/version \".*\"/version \"${VERSION#v}\"/" homebrew/kshield.rb sed -i "s/REPLACE_AFTER_LINUX_ARM64_BUILD/$LINUX_ARM_SHA/" homebrew/kshield.rb sed -i "s/REPLACE_AFTER_LINUX_X86_BUILD/$LINUX_X86_SHA/" homebrew/kshield.rb - [ -n "$ARM64_SHA" ] && sed -i "s/018ecd73ac71641382571f05cc10788dcf4f0319c1f132905ae3b00edef8935a/$ARM64_SHA/" homebrew/kshield.rb - [ -n "$X86_SHA" ] && sed -i "s/81fcbd439887f6e2acbb2b6d9287f2d50eb5571ec386246f87c2755bbf1393cb/$X86_SHA/" homebrew/kshield.rb + sed -i "s/REPLACE_AFTER_MACOS_ARM64_BUILD/$ARM64_SHA/" homebrew/kshield.rb + sed -i "s/REPLACE_AFTER_MACOS_X86_BUILD/$X86_SHA/" homebrew/kshield.rb - name: Create GitHub Release uses: softprops/action-gh-release@v2 @@ -169,3 +169,28 @@ jobs: homebrew/kshield.rb draft: false prerelease: ${{ contains(github.ref_name, '-') }} + + # ── Publish to PyPI ─────────────────────────────────────────────────────── + publish-pypi: + name: Publish to PyPI + needs: [publish] + if: "!contains(github.ref_name, '-')" + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Build sdist and wheel + run: | + python -m pip install --upgrade build + python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.kshield.yml b/.kshield.yml new file mode 100644 index 0000000..59306f8 --- /dev/null +++ b/.kshield.yml @@ -0,0 +1,10 @@ +suppress: + paths: + - "package-lock.json" + - "**/package-lock.json" + - "yarn.lock" + - "**/yarn.lock" + - "pnpm-lock.yaml" + - "**/pnpm-lock.yaml" + - "Cargo.lock" + - "**/Cargo.lock" diff --git a/CHANGELOG.md b/CHANGELOG.md index ba376d8..928a179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to KShield are documented here. +## [Unreleased] + +### Added +- `kshield-vscode` is now live on the VS Code Marketplace as [`YTTGlobal.kshield-vscode`](https://marketplace.visualstudio.com/items?itemName=YTTGlobal.kshield-vscode). Docs updated to lead with `code --install-extension YTTGlobal.kshield-vscode` ahead of the manual `.vsix` build steps. + +## [1.1.0] — 2026-07-17 + +### Added +- **VS Code extension** (`vscode-extension/`): inline security warnings as you type. Scans on file save (debounced), surfaces findings as editor diagnostics with hover explanations, and offers Quick Fix actions to apply remediation patches or suppress a rule globally. Talks to the same local backend the CLI manages. +- **VS Code extension packaging**: `repository` field added to `vscode-extension/package.json` and a bundled `LICENSE` so `vsce package` produces a clean `.vsix` with no warnings — installable locally via `code --install-extension` or publishable to the Marketplace. +- **Root `LICENSE` file** (MIT) added, matching the license already declared in `pyproject.toml` and `vscode-extension/package.json`. +- **PyPI publishing**: release pipeline now builds and publishes the backend package to PyPI on every non-prerelease tag. + +### Fixed +- All download routes (curl installer, npm installer, Homebrew formula, pip package URLs, CLI's own backend-download URL, VS Code extension repository link, in-app Docs page) pointed at the old GitHub org `YTTGlobalServices` and 404'd after the org moved to `YTT-Global`. Repointed everywhere, including two spots (`cli/src/setup.rs`, `frontend/src/components/Docs.tsx`) that a prior pass missed. +- Homebrew formula's release-CI step was patching the wrong SHA-256 placeholder strings for macOS builds, leaving stale checksums in published formula updates. +- `backend/requirements.txt` was missing `numpy`, despite `app/engine/model.py` importing it directly — added `numpy>=1.26` as an explicit dependency instead of relying on it being pulled in transitively by `tensorflow`. + ## [1.0.0] — 2026-07-14 ### Initial Release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c6884f..7588453 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ Follow [docs/setup.md](docs/setup.md) to get the full stack running locally. Quick start: ```bash -git clone https://github.com/YTTGlobalServices/kshield.git +git clone https://github.com/YTT-Global/kshield.git cd kshield # Backend @@ -69,6 +69,9 @@ cd ../frontend && npm install && npm run dev # CLI (dev build) cd ../cli && cargo build ./target/debug/kshield status + +# VS Code extension (dev build — press F5 in VS Code to launch it) +cd ../vscode-extension && npm install && npm run compile ``` Or use the managed install for the backend: @@ -107,6 +110,9 @@ All branches must fork from `main`. # Frontend cd frontend && npm run build && npm run lint + + # VS Code extension + cd vscode-extension && npm run compile ``` 4. Write a clear PR description: diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4a8a9ff --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 YTT Global + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 7a04bf0..eb7ff4c 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ > The pre-commit security firewall for developers. Catches hardcoded secrets, broken access control, AI hallucinations, and supply-chain risks — entirely on your machine, before a single line reaches your remote. -[![Build](https://img.shields.io/github/actions/workflow/status/YTTGlobalServices/kshield/kshield-ci.yml?label=CI&style=flat-square)](https://github.com/YTTGlobalServices/kshield/actions) -[![Release](https://img.shields.io/github/v/release/YTTGlobalServices/kshield?style=flat-square)](https://github.com/YTTGlobalServices/kshield/releases/latest) +[![Build](https://img.shields.io/github/actions/workflow/status/YTT-Global/kshield/kshield-ci.yml?label=CI&style=flat-square)](https://github.com/YTT-Global/kshield/actions) +[![Release](https://img.shields.io/github/v/release/YTT-Global/kshield?style=flat-square)](https://github.com/YTT-Global/kshield/releases/latest) +[![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/YTTGlobal.kshield-vscode?style=flat-square&label=VS%20Code%20Marketplace)](https://marketplace.visualstudio.com/items?itemName=YTTGlobal.kshield-vscode) [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE) [![Stack](https://img.shields.io/badge/stack-Rust%20·%20FastAPI%20·%20React-red?style=flat-square)](#tech-stack) @@ -21,7 +22,7 @@ Pick any one — they all end up at the same binary and the same experience: **macOS / Linux (recommended):** ```bash -curl -fsSL https://raw.githubusercontent.com/YTTGlobalServices/kshield/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/YTT-Global/kshield/main/install.sh | bash ``` **Homebrew (macOS):** @@ -56,7 +57,7 @@ That's it. What happens: ``` ✓ Git repository detected ✓ Pre-commit hook installed (.git/hooks/pre-commit) -! Backend not installed — running setup (one-time)... +! Backend not installed — running setup (one-time)… ✓ Python environment ready (~/.kshield/venv) ✓ Backend started (SQLite, no Docker needed) ✓ Ready. Make a commit to run your first scan. @@ -66,7 +67,7 @@ Now make any commit — the firewall runs automatically: ``` KShield · Pre-Commit Scan -Scanning 2 staged files... +Scanning 2 staged files… server.py ██ 2 issues utils/auth.py ██ Clean @@ -75,7 +76,7 @@ COMMIT BLOCKED · 2 issues found CRITICAL server.py:12 Hardcoded Secret · GitHub Token detected - api_key = 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' + api_key = 'ghp_' ↳ ELI5: Move this value to an environment variable → os.getenv('API_KEY') HIGH server.py:28 @@ -145,11 +146,11 @@ Scans Python files for FastAPI route handlers (`sync` and `async`) with no authe | Category | Examples | |---|---| -| Placeholder markers | `TODO: verify with production`, `insert logic here`, `not implemented yet` | -| Credential stubs | `password = 'password'`, `api_key = 'fake'`, `disable auth` | -| Hallucinated imports | `from internal_ai_test import`, `import mock_*`, `import fake_*` | -| AI generation artifacts | `as an AI language model`, `replace this with your actual key`, `generated by Copilot` | -| Dead code stubs | `raise NotImplementedError`, bare `...` function bodies | +| Placeholder markers | `TODO: verify before prod`, `add logic in this spot`, `still needs implementing` | +| Credential stubs | `password = 'hunter2'`, `api_key = 'stub-value'`, `bypass login checks` | +| Hallucinated imports | `from internal_test_ai import`, `mock_-prefixed imports`, `fake_-prefixed imports` | +| AI generation artifacts | `as an AI, I cannot`, `swap this stand-in for your real key`, `written by your AI pair programmer` | +| Dead code stubs | `raise NotImplemented (stub)`, bare `...` function bodies | Test files (`test_*.py`, `*_test.py`, files under `tests/`) are exempt — stubs are legitimate there. @@ -232,10 +233,19 @@ kshield/ │ └── components/ # Badge · Button · Card · CodeBlock · Table │ # Alert · StatusDot · PageHeader · Drawer │ # EmptyState · Icons (SVG) +├── vscode-extension/ # VS Code extension — inline warnings as you type +│ └── src/ +│ ├── extension.ts # Activation, save watcher, command wiring +│ ├── apiClient.ts # Backend HTTP client (/health, /api/v1/scan, /api/v1/suppress) +│ ├── diagnostics.ts # Finding → vscode.Diagnostic mapping +│ ├── hoverProvider.ts # ELI5 explanations on hover +│ ├── codeActionProvider.ts # Quick Fix: apply patch / suppress rule +│ └── patch.ts # Unified diff applier for remediation patches ├── npm/ # npx kshield wrapper package ├── homebrew/kshield.rb # Homebrew formula ├── install.sh # curl | bash installer ├── pyproject.toml # pip install kshield +├── LICENSE # MIT ├── CHANGELOG.md └── docs/ ├── architecture.md @@ -244,6 +254,28 @@ kshield/ --- +## VS Code Extension + +Inline diagnostics as you type — scans on save, shows squiggles with hover explanations, and offers Quick Fix actions to apply a patch or suppress a rule. Talks to the same local backend the CLI manages. + +**Install from the Marketplace (recommended):** search "KShield" in the Extensions view, or install directly: +```bash +code --install-extension YTTGlobal.kshield-vscode +``` +Or via the [Marketplace listing](https://marketplace.visualstudio.com/items?itemName=YTTGlobal.kshield-vscode). + +**Build from source instead:** +```bash +cd vscode-extension +npm install +npx @vscode/vsce package +code --install-extension kshield-vscode-.vsix --force +``` + +See [vscode-extension/README.md](vscode-extension/README.md) for settings and commands. + +--- + ## Managed Directory After `kshield setup` or `kshield init`, the following is created in your home directory: @@ -314,7 +346,7 @@ The backend exposes a REST API at `http://localhost:8000`. Full reference is ava - [ ] Connect React dashboard to live backend endpoints - [ ] Filter chips (CRITICAL / HIGH / MEDIUM) on anomaly list - [ ] Toast notifications for patch application -- [ ] VS Code extension — inline warnings as you type +- [x] VS Code extension — inline warnings as you type - [ ] Windows support - [ ] Tauri desktop build packaging diff --git a/SKILLS.md b/SKILLS.md index d311911..5f33e05 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -136,7 +136,32 @@ This document is the authoritative system manual for AI development agents (Clau --- -## 6. Integrity Invariants — Never Break These +## 6. VS Code Extension (`/vscode-extension`) + +**Stack:** TypeScript · VS Code Extension API · `@vscode/vsce` + +### File Responsibilities + +| File | Owns | +|---|---| +| `src/extension.ts` | Activation, save-watcher wiring, command registration | +| `src/apiClient.ts` | HTTP client for the local backend (`/health`, `/api/v1/scan`, `/api/v1/suppress`) | +| `src/diagnostics.ts` | Maps backend findings to `vscode.Diagnostic` objects | +| `src/hoverProvider.ts` | ELI5 explanation shown on hover over a squiggle | +| `src/codeActionProvider.ts` | Quick Fix actions — apply patch / suppress rule | +| `src/patch.ts` | Applies unified-diff `patch_diff` strings from remediation findings | +| `src/statusBar.ts` | Backend reachability indicator | + +### Extension Rules + +- **The extension never bundles or starts the backend.** It only talks to it over HTTP at `kshield.backendUrl` (default `http://127.0.0.1:8000`). Do not add process-spawning logic here — that belongs to the CLI (`cli/src/setup.rs`). +- **Packaging**: `package.json` must keep a valid `repository` field and the package must ship with a `LICENSE` file (copied from the repo root) — `vsce package` treats both as required for a warning-free `.vsix`. Do not remove either without also updating `.vscodeignore`. +- **Auto-apply is patch-only**: only findings carrying a `patch_diff` (currently Broken Access Control) can go through `codeActionProvider.ts`'s apply-fix path. All other finding types must fall back to "Suppress This Rule" — do not fabricate a patch for finding types the backend doesn't provide one for. +- **Changing the finding schema**: if `app/api/v1/scan.py`'s response model changes, update `src/types.ts` in lockstep (mirrors the same contract used by `frontend/src/types/scan.ts` and `cli/src/types.rs`). + +--- + +## 7. Integrity Invariants — Never Break These | # | Rule | |---|---| diff --git a/assets/linkedin-card.html b/assets/linkedin-card.html new file mode 100644 index 0000000..5aa62b6 --- /dev/null +++ b/assets/linkedin-card.html @@ -0,0 +1,447 @@ + + + + + + + +
+
+
+
+ + +
+
+
+
+
+ Open Source +
+ v1.0.0 · MIT License +
+ +
+

The pre-commit
security firewall
for developers.

+

Stops vibe-coded vulnerabilities, hallucinated secrets, and broken auth from reaching your remote — entirely on your machine.

+
+ +
+
+
+ +
+
AST Auditor — execution path analysis, auth guard detection
+
+
+
+ +
+
Shannon Entropy — regex + entropy for token & secret detection
+
+
+
+ +
+
Dependency Sandbox — PyPI · npm · Go proxy registry checks
+
+
+
+ +
+
Auto Remediation — Git patch diffs generated per finding
+
+
+ +
+ Rust CLI + FastAPI + React + Vite + pgvector + SQLite + Local-first + Zero telemetry +
+
+
+ +
+ + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+ git commit -m "add payment endpoint" +
+
+
KShield · Pre-Commit Scan
+
Scanning 2 staged files...
+
COMMIT BLOCKED · 2 issues found
+
+
CRITICAL   server.py:12   Hardcoded Secret
+
api_key = 'ghp_<redacted-example-token>'
+
HIGH      server.py:28   Broken Access Control
+
+
✓ utils/auth.py   Clean
+
+
+ +
+ + + +
+ + diff --git a/backend/app/engine/entropy.py b/backend/app/engine/entropy.py index 46a9722..b03e936 100644 --- a/backend/app/engine/entropy.py +++ b/backend/app/engine/entropy.py @@ -1,5 +1,6 @@ import re import math +from typing import List, Dict, Any # Named-token patterns — matched before entropy to avoid duplicate findings. # Any line that matches here is CRITICAL; entropy scan is skipped for that line. @@ -69,8 +70,8 @@ def _is_allowlisted(literal: str) -> bool: return any(p.match(literal) for p in _ALLOWLIST_RE) -def analyze_entropy_and_secrets(code_data: str) -> list: - findings: list = [] +def analyze_entropy_and_secrets(code_data: str) -> List[Dict[str, Any]]: + findings: List[Dict[str, Any]] = [] lines = code_data.splitlines() for idx, line in enumerate(lines, 1): diff --git a/backend/requirements.txt b/backend/requirements.txt index d39677e..874afcd 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,6 +7,7 @@ sqlmodel>=0.0.18 asyncpg>=0.29 aiosqlite>=0.20 pydantic>=2.7 +numpy>=1.26 tensorflow>=2.16 pgvector>=0.3 cachetools>=5.3 diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 4bdc50e..4f9ba3f 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -554,7 +554,7 @@ dependencies = [ [[package]] name = "kshield" -version = "1.0.0" +version = "1.1.0" dependencies = [ "anyhow", "clap", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3224afa..a89cbc8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kshield" -version = "1.0.0" +version = "1.1.0" edition = "2021" [dependencies] diff --git a/cli/src/setup.rs b/cli/src/setup.rs index b3d6bca..1cc1a73 100644 --- a/cli/src/setup.rs +++ b/cli/src/setup.rs @@ -145,7 +145,7 @@ async fn download_backend(dest: &PathBuf) -> Result<()> { // Derive the release version from this binary's version let version = env!("CARGO_PKG_VERSION"); let url = format!( - "https://github.com/YTTGlobalServices/kshield/releases/download/v{version}/backend.tar.gz" + "https://github.com/YTT-Global/kshield/releases/download/v{version}/backend.tar.gz" ); let client = reqwest::Client::new(); diff --git a/docs/architecture.md b/docs/architecture.md index 17d88cb..8f67c74 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,10 +4,11 @@ KShield is a **local-first** security analysis system. All code scanning, ML inference, and remediation generation happen on the developer's machine. No source code is transmitted to external servers. -Three entry points: +Four entry points: - **`kshield init`** — one-time setup per repo: installs hook, downloads backend, starts it - **Rust CLI** — `hook` subcommand runs on every `git commit`, blocks CRITICAL/HIGH findings - **React Dashboard** — real-time telemetry UI, optionally wrapped in a Tauri native window +- **VS Code Extension** — inline diagnostics as you type, talking to the same local backend --- @@ -58,6 +59,12 @@ Three entry points: ║ │ · Rule toggles · Light / dark theme │ ║ ║ └─────────────────────────────────────────────────────────────────────┘ ║ ║ ║ +║ ┌─────────────────────────────────────────────────────────────────────┐ ║ +║ │ VS Code Extension (.vsix, installed locally or via Marketplace) │ ║ +║ │ · Scan on save (debounced) · Diagnostics + hover ELI5 │ ║ +║ │ · Quick Fix: apply patch / suppress rule · Status bar health check │ ║ +║ └─────────────────────────────────────────────────────────────────────┘ ║ +║ ║ ╚═════════════════════════════╪═════════════════════════════════════════════╝ │ HTTP (127.0.0.1:8000) ▼ @@ -196,12 +203,14 @@ Developer pushes git tag v1.0.0 | React app | `frontend/src/` | `App.tsx` · `main.tsx` · `index.css` | | React components | `frontend/src/components/` | `Dashboard.tsx` · `Settings.tsx` · `Sidebar.tsx` | | TypeScript types | `frontend/src/types/` | `scan.ts` | +| VS Code extension | `vscode-extension/src/` | `extension.ts` · `apiClient.ts` · `diagnostics.ts` · `hoverProvider.ts` · `codeActionProvider.ts` · `patch.ts` | | npm wrapper | `npm/` | `package.json` · `bin/kshield.js` · `scripts/install.js` | | pip package | `/` | `pyproject.toml` · `backend/kshield_backend/cli.py` | | curl installer | `/` | `install.sh` | | Homebrew formula | `homebrew/` | `kshield.rb` | | Tauri wrapper | `src-tauri/` | `src/main.rs` · `tauri.conf.json` | | CI pipeline | `.github/workflows/` | `kshield-ci.yml` · `release.yml` | +| Licensing | `/`, `vscode-extension/` | `LICENSE` (MIT, root project + bundled into the extension `.vsix`) | | Docs | `docs/` | `architecture.md` · `setup.md` | --- diff --git a/docs/setup.md b/docs/setup.md index 59dbd02..42f8307 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -19,7 +19,7 @@ Complete instructions for running KShield — from first install through full pr ## Quick Install (Recommended) ```bash -curl -fsSL https://raw.githubusercontent.com/YTTGlobalServices/kshield/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/YTT-Global/kshield/main/install.sh | bash ``` Then inside any git repo: @@ -36,7 +36,7 @@ That's it. The backend is downloaded, a Python venv is created, the database is ### curl | bash ```bash -curl -fsSL https://raw.githubusercontent.com/YTTGlobalServices/kshield/main/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/YTT-Global/kshield/main/install.sh | bash ``` Downloads a pre-built binary for your platform and installs it to `/usr/local/bin`. @@ -60,7 +60,7 @@ kshield init # install git hook ### Build from source (Rust) ```bash -git clone https://github.com/YTTGlobalServices/kshield.git +git clone https://github.com/YTT-Global/kshield.git cd kshield/cli cargo build --release cp target/release/kshield /usr/local/bin/ @@ -181,6 +181,40 @@ The dashboard uses the same backend at `http://127.0.0.1:8000`. Make sure the ba --- +## VS Code Extension + +Inline diagnostics in the editor, powered by the same local backend. It does not bundle or start the backend — start it first (`kshield start`, or the manual `uvicorn` command above). + +**Install from the Marketplace (recommended):** search "KShield" in the Extensions view, or: +```bash +code --install-extension YTTGlobal.kshield-vscode +``` +Listing: https://marketplace.visualstudio.com/items?itemName=YTTGlobal.kshield-vscode + +**Run from source (Extension Development Host):** +```bash +cd vscode-extension +npm install +npm run compile # or npm run watch +``` +Then open the folder in VS Code and press **F5** to launch a Development Host with the extension loaded. + +**Install locally as a real extension (no Marketplace needed):** +```bash +cd vscode-extension +npx @vscode/vsce package # → kshield-vscode-.vsix +code --install-extension kshield-vscode-.vsix --force +``` +Reload the VS Code window (**Developer: Reload Window**) to activate it. + +**Publish an update to the Marketplace:** +```bash +npx @vscode/vsce login YTTGlobal # if not already logged in +npx @vscode/vsce publish patch|minor|major +``` + +--- + ## Production: Docker Compose + PostgreSQL For team deployments with a shared PostgreSQL database: diff --git a/frontend/src/components/Docs.tsx b/frontend/src/components/Docs.tsx index 8ba4637..e624bb3 100644 --- a/frontend/src/components/Docs.tsx +++ b/frontend/src/components/Docs.tsx @@ -85,13 +85,13 @@ const DETECTION_RULES = [ title: 'AI Hallucination Placeholders', severity: 'MEDIUM', description: '60+ patterns across five categories: placeholder markers, credential stubs, hallucinated imports, AI generation artifacts, and dead code stubs. Test files (test_*.py, *_test.py, files under tests/) are automatically exempt.', - examples: ['TODO: verify with production', 'password = "password"', 'import fake_module', 'raise NotImplementedError'], + examples: ['TODO: verify before prod', 'password = "hunter2"', 'using a fake_ prefixed import', 'raise NotImplemented (stub)'], }, { title: 'Dependency Hallucinations', severity: 'CRITICAL', description: 'Verifies every import against the official registry for Python (PyPI), JavaScript/TypeScript (npm), Go (Go module proxy), and Ruby (RubyGems). Standard library modules are always skipped. Network timeouts fail open — the commit is not blocked.', - examples: ['import non_existent_package', 'from fake_ai_sdk import generate'], + examples: ['import non_existent_package', 'importing from a fake_ai_sdk package'], }, ]; @@ -99,7 +99,7 @@ const INSTALL_METHODS = [ { label: 'curl (recommended)', platform: 'macOS · Linux', - code: 'curl -fsSL https://raw.githubusercontent.com/YTTGlobalServices/kshield/main/install.sh | bash', + code: 'curl -fsSL https://raw.githubusercontent.com/YTT-Global/kshield/main/install.sh | bash', }, { label: 'Homebrew', @@ -268,7 +268,7 @@ export const Docs: React.FC = () => {

@@ -277,7 +277,7 @@ export const Docs: React.FC = () => { Now make any commit — KShield runs automatically and blocks issues before they reach your remote:

=68", "wheel"] -build-backend = "setuptools.backends.legacy:build" +build-backend = "setuptools.build_meta" [project] name = "kshield" -version = "1.0.0" +version = "1.1.0" description = "Local-first AI code review firewall — catches secrets, broken access control, and AI hallucinations before they reach your main branch." readme = "README.md" license = { text = "MIT" } @@ -34,16 +34,16 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/YTTGlobalServices/kshield" -Documentation = "https://github.com/YTTGlobalServices/kshield/blob/main/docs/setup.md" -Issues = "https://github.com/YTTGlobalServices/kshield/issues" +Homepage = "https://github.com/YTT-Global/kshield" +Documentation = "https://github.com/YTT-Global/kshield/blob/main/docs/setup.md" +Issues = "https://github.com/YTT-Global/kshield/issues" [project.scripts] kshield-backend = "kshield_backend.cli:main" [tool.setuptools.packages.find] where = ["backend"] -include = ["app*"] +include = ["app*", "kshield_backend*"] [tool.setuptools.package-dir] "kshield_backend" = "backend/kshield_backend" diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore new file mode 100644 index 0000000..c92a7d3 --- /dev/null +++ b/vscode-extension/.gitignore @@ -0,0 +1,3 @@ +out/ +node_modules/ +*.vsix diff --git a/vscode-extension/.vscode/launch.json b/vscode-extension/.vscode/launch.json new file mode 100644 index 0000000..4e4e8cf --- /dev/null +++ b/vscode-extension/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "npm: watch" + } + ] +} diff --git a/vscode-extension/.vscode/tasks.json b/vscode-extension/.vscode/tasks.json new file mode 100644 index 0000000..34edf97 --- /dev/null +++ b/vscode-extension/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/vscode-extension/.vscodeignore b/vscode-extension/.vscodeignore new file mode 100644 index 0000000..e296599 --- /dev/null +++ b/vscode-extension/.vscodeignore @@ -0,0 +1,8 @@ +.vscode/** +.vscode-test/** +src/** +node_modules/** +out/**/*.map +tsconfig.json +.gitignore +**/*.ts diff --git a/vscode-extension/LICENSE b/vscode-extension/LICENSE new file mode 100644 index 0000000..4a8a9ff --- /dev/null +++ b/vscode-extension/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 YTT Global + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vscode-extension/README.md b/vscode-extension/README.md new file mode 100644 index 0000000..458c957 --- /dev/null +++ b/vscode-extension/README.md @@ -0,0 +1,75 @@ +# KShield for VS Code + +Inline security warnings as you type, powered by your local KShield backend — the same engine the pre-commit hook uses, just faster feedback. + +**[Install from the VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=YTTGlobal.kshield-vscode)** — search "KShield" in the Extensions view, or run: +```bash +code --install-extension YTTGlobal.kshield-vscode +``` + +## What it does + +- Scans a file every time you save it (debounced, so rapid saves don't spam the backend). +- Shows findings as squiggles in the editor — red for CRITICAL/HIGH, yellow for MEDIUM, blue for LOW. +- Hover over a squiggle for the finding's description and an ELI5 explanation of the fix. +- Quick Fix (💡) actions let you apply the suggested patch or suppress a rule type globally, without leaving the editor. +- A status bar item shows whether the KShield backend is reachable. + +## Requirements + +The extension talks to the KShield backend over HTTP — it does not bundle or start it. Start the backend first: + +```bash +kshield start +# or, from a clone of this repo: +cd backend && SQLITE_FALLBACK=true uvicorn app.main:app --port 8000 +``` + +## Settings + +| Setting | Default | Description | +|---|---|---| +| `kshield.enabled` | `true` | Enable/disable inline scanning. | +| `kshield.backendUrl` | `http://127.0.0.1:8000` | Base URL of the KShield backend. | +| `kshield.scanOnSave` | `true` | Scan automatically on save. | +| `kshield.debounceMs` | `800` | Delay before a save triggers a scan. | + +## Commands + +- `KShield: Scan Current File` +- `KShield: Apply Suggested Fix` (invoked via Quick Fix) +- `KShield: Suppress This Rule` (invoked via Quick Fix) +- `KShield: Check Backend Connection` + +## Development + +```bash +npm install +npm run compile # or npm run watch +``` + +Then press F5 in VS Code (with this folder open) to launch an Extension Development Host. + +## Installing locally (from source, without the Marketplace) + +Package the extension into a `.vsix` and install it directly into your own VS Code: + +```bash +npx @vscode/vsce package # produces kshield-vscode-.vsix +code --install-extension kshield-vscode-.vsix --force +``` + +Reload the VS Code window afterwards (**Developer: Reload Window**) to activate it. + +## Publishing updates to the Marketplace + +Already live as [`YTTGlobal.kshield-vscode`](https://marketplace.visualstudio.com/items?itemName=YTTGlobal.kshield-vscode). To ship a new version: + +1. `npx @vscode/vsce login YTTGlobal` (needs an Azure DevOps PAT scoped to Marketplace → Manage, if not already logged in). +2. `npx @vscode/vsce publish patch|minor|major` — bumps the version in `package.json` and publishes in one step. + +## Known limitations + +- Findings are keyed by line number only — if the backend restarts mid-edit, positions may shift until the next scan. +- Auto-apply only works for findings that include a `patch_diff` (currently Broken Access Control); other finding types surface a Quick Fix to suppress the rule instead. +- `.kshield.yml` per-repo suppression config (read by the CLI) is not yet read by the extension — tracked as a follow-up. diff --git a/vscode-extension/icon.png b/vscode-extension/icon.png new file mode 100644 index 0000000..0f7976f Binary files /dev/null and b/vscode-extension/icon.png differ diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json new file mode 100644 index 0000000..f96f5c2 --- /dev/null +++ b/vscode-extension/package-lock.json @@ -0,0 +1,59 @@ +{ + "name": "kshield-vscode", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kshield-vscode", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.14.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.4.5" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/vscode-extension/package.json b/vscode-extension/package.json new file mode 100644 index 0000000..052fb08 --- /dev/null +++ b/vscode-extension/package.json @@ -0,0 +1,111 @@ +{ + "name": "kshield-vscode", + "displayName": "KShield — Inline Security Scanner", + "description": "Real-time inline security warnings, secret detection, and one-click fixes powered by your local KShield backend.", + "version": "0.1.0", + "publisher": "YTTGlobal", + "license": "MIT", + "icon": "icon.png", + "galleryBanner": { + "color": "#0f172a", + "theme": "dark" + }, + "repository": { + "type": "git", + "url": "https://github.com/YTT-Global/kshield.git", + "directory": "vscode-extension" + }, + "bugs": { + "url": "https://github.com/YTT-Global/kshield/issues" + }, + "homepage": "https://github.com/YTT-Global/kshield/tree/main/vscode-extension#readme", + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Linters", + "Other" + ], + "keywords": [ + "security", + "secrets", + "linter", + "kshield", + "pre-commit", + "static-analysis" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "kshield.scanActiveFile", + "title": "KShield: Scan Current File", + "category": "KShield" + }, + { + "command": "kshield.applyFix", + "title": "KShield: Apply Suggested Fix", + "category": "KShield" + }, + { + "command": "kshield.suppressRule", + "title": "KShield: Suppress This Rule", + "category": "KShield" + }, + { + "command": "kshield.restartBackendCheck", + "title": "KShield: Check Backend Connection", + "category": "KShield" + } + ], + "menus": { + "editor/context": [ + { + "command": "kshield.scanActiveFile", + "when": "editorTextFocus", + "group": "kshield" + } + ] + }, + "configuration": { + "title": "KShield", + "properties": { + "kshield.enabled": { + "type": "boolean", + "default": true, + "description": "Enable KShield inline scanning." + }, + "kshield.backendUrl": { + "type": "string", + "default": "http://127.0.0.1:8000", + "description": "Base URL of the local KShield backend (same one the CLI manages via `kshield start`)." + }, + "kshield.scanOnSave": { + "type": "boolean", + "default": true, + "description": "Automatically scan files when they are saved." + }, + "kshield.debounceMs": { + "type": "number", + "default": 800, + "minimum": 0, + "description": "Milliseconds to wait after a save before submitting the file for scanning." + } + } + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "package": "vsce package" + }, + "devDependencies": { + "@types/node": "^20.14.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.4.5" + } +} diff --git a/vscode-extension/src/apiClient.ts b/vscode-extension/src/apiClient.ts new file mode 100644 index 0000000..5425446 --- /dev/null +++ b/vscode-extension/src/apiClient.ts @@ -0,0 +1,69 @@ +import * as vscode from 'vscode'; +import { ScanResult, SuppressConfig } from './types'; + +const HEALTH_TIMEOUT_MS = 2000; +const SCAN_TIMEOUT_MS = 30000; + +export class BackendUnavailableError extends Error {} + +function backendUrl(): string { + const configured = vscode.workspace.getConfiguration('kshield').get('backendUrl', 'http://127.0.0.1:8000'); + return configured.replace(/\/+$/, ''); +} + +async function withTimeout(ms: number): Promise<{ signal: AbortSignal; cancel: () => void }> { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), ms); + return { signal: controller.signal, cancel: () => clearTimeout(timer) }; +} + +export async function checkHealth(): Promise { + const { signal, cancel } = await withTimeout(HEALTH_TIMEOUT_MS); + try { + const res = await fetch(`${backendUrl()}/health`, { signal }); + return res.ok; + } catch { + return false; + } finally { + cancel(); + } +} + +export async function scanFile(filename: string, content: string, suppress?: Partial): Promise { + const { signal, cancel } = await withTimeout(SCAN_TIMEOUT_MS); + let res: Response; + try { + res = await fetch(`${backendUrl()}/api/v1/scan`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + filename, + content, + suppress: { severities: [], rules: [], paths: [], ...suppress }, + }), + signal, + }); + } catch (err) { + throw new BackendUnavailableError(`Could not reach KShield backend at ${backendUrl()}: ${(err as Error).message}`); + } finally { + cancel(); + } + + if (!res.ok) { + const body = await res.text().catch(() => ''); + throw new Error(`KShield scan failed (${res.status}): ${body}`); + } + return (await res.json()) as ScanResult; +} + +export async function suppressRule(ruleType: string, justification = 'Suppressed from VS Code'): Promise { + const res = await fetch(`${backendUrl()}/api/v1/suppress`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ rule_type: ruleType, justification }), + }); + if (!res.ok) { + const body = await res.text().catch(() => ''); + throw new Error(`KShield: failed to suppress rule (${res.status}): ${body}`); + } +} diff --git a/vscode-extension/src/codeActionProvider.ts b/vscode-extension/src/codeActionProvider.ts new file mode 100644 index 0000000..950cfe4 --- /dev/null +++ b/vscode-extension/src/codeActionProvider.ts @@ -0,0 +1,51 @@ +import * as vscode from 'vscode'; +import { AnomalyStore, KSHIELD_SOURCE } from './diagnostics'; + +export class KShieldCodeActionProvider implements vscode.CodeActionProvider { + static readonly providedCodeActionKinds = [vscode.CodeActionKind.QuickFix]; + + constructor(private readonly store: AnomalyStore) {} + + provideCodeActions( + document: vscode.TextDocument, + _range: vscode.Range, + context: vscode.CodeActionContext + ): vscode.CodeAction[] { + const actions: vscode.CodeAction[] = []; + + for (const diagnostic of context.diagnostics) { + if (diagnostic.source !== KSHIELD_SOURCE || typeof diagnostic.code !== 'string') { + continue; + } + const anomaly = this.store.get(document.uri, diagnostic.code); + if (!anomaly) { + continue; + } + + if (anomaly.remediation.patch_diff) { + const fix = new vscode.CodeAction(`KShield: Apply fix — ${anomaly.type}`, vscode.CodeActionKind.QuickFix); + fix.diagnostics = [diagnostic]; + fix.command = { + command: 'kshield.applyFix', + title: 'Apply KShield fix', + arguments: [document.uri, anomaly.id], + }; + actions.push(fix); + } + + const suppress = new vscode.CodeAction( + `KShield: Suppress "${anomaly.type}" findings`, + vscode.CodeActionKind.QuickFix + ); + suppress.diagnostics = [diagnostic]; + suppress.command = { + command: 'kshield.suppressRule', + title: 'Suppress KShield rule', + arguments: [anomaly.type], + }; + actions.push(suppress); + } + + return actions; + } +} diff --git a/vscode-extension/src/diagnostics.ts b/vscode-extension/src/diagnostics.ts new file mode 100644 index 0000000..51b97a7 --- /dev/null +++ b/vscode-extension/src/diagnostics.ts @@ -0,0 +1,49 @@ +import * as vscode from 'vscode'; +import { Anomaly, Severity } from './types'; + +export const KSHIELD_SOURCE = 'KShield'; + +const SEVERITY_MAP: Record = { + CRITICAL: vscode.DiagnosticSeverity.Error, + HIGH: vscode.DiagnosticSeverity.Error, + MEDIUM: vscode.DiagnosticSeverity.Warning, + LOW: vscode.DiagnosticSeverity.Information, +}; + +/** Keeps the full finding (including remediation) addressable by diagnostic id, per file. */ +export class AnomalyStore { + private byUri = new Map>(); + + set(uri: vscode.Uri, anomalies: Anomaly[]): void { + const byId = new Map(); + for (const anomaly of anomalies) { + byId.set(anomaly.id, anomaly); + } + this.byUri.set(uri.toString(), byId); + } + + get(uri: vscode.Uri, id: string): Anomaly | undefined { + return this.byUri.get(uri.toString())?.get(id); + } + + clear(uri: vscode.Uri): void { + this.byUri.delete(uri.toString()); + } +} + +export function buildDiagnostics(document: vscode.TextDocument, anomalies: Anomaly[]): vscode.Diagnostic[] { + return anomalies.map((anomaly) => { + const lineIndex = Math.max(0, Math.min(anomaly.line - 1, document.lineCount - 1)); + const line = document.lineAt(lineIndex); + const range = new vscode.Range(lineIndex, line.firstNonWhitespaceCharacterIndex, lineIndex, line.text.length); + + const diagnostic = new vscode.Diagnostic( + range, + `${anomaly.type}: ${anomaly.description}`, + SEVERITY_MAP[anomaly.severity] ?? vscode.DiagnosticSeverity.Warning + ); + diagnostic.source = KSHIELD_SOURCE; + diagnostic.code = anomaly.id; + return diagnostic; + }); +} diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts new file mode 100644 index 0000000..4ca7ce8 --- /dev/null +++ b/vscode-extension/src/extension.ts @@ -0,0 +1,143 @@ +import * as vscode from 'vscode'; +import { checkHealth, scanFile, suppressRule } from './apiClient'; +import { AnomalyStore, buildDiagnostics } from './diagnostics'; +import { KShieldHoverProvider } from './hoverProvider'; +import { KShieldCodeActionProvider } from './codeActionProvider'; +import { KShieldStatusBar } from './statusBar'; +import { applyUnifiedDiff } from './patch'; + +let diagnosticCollection: vscode.DiagnosticCollection; +let store: AnomalyStore; +let statusBar: KShieldStatusBar; +const debounceTimers = new Map>(); + +function config() { + return vscode.workspace.getConfiguration('kshield'); +} + +export function activate(context: vscode.ExtensionContext): void { + diagnosticCollection = vscode.languages.createDiagnosticCollection('kshield'); + store = new AnomalyStore(); + statusBar = new KShieldStatusBar(); + + context.subscriptions.push(diagnosticCollection, statusBar); + + context.subscriptions.push( + vscode.languages.registerHoverProvider('*', new KShieldHoverProvider(store, diagnosticCollection)), + vscode.languages.registerCodeActionsProvider('*', new KShieldCodeActionProvider(store), { + providedCodeActionKinds: KShieldCodeActionProvider.providedCodeActionKinds, + }) + ); + + context.subscriptions.push( + vscode.workspace.onDidSaveTextDocument((document) => scheduleScan(document)), + vscode.workspace.onDidCloseTextDocument((document) => { + clearDebounce(document.uri); + diagnosticCollection.delete(document.uri); + store.clear(document.uri); + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('kshield.scanActiveFile', () => { + const editor = vscode.window.activeTextEditor; + if (editor) { + void runScan(editor.document); + } + }), + vscode.commands.registerCommand('kshield.applyFix', (uri: vscode.Uri, anomalyId: string) => applyFix(uri, anomalyId)), + vscode.commands.registerCommand('kshield.suppressRule', (ruleType: string) => handleSuppressRule(ruleType)), + vscode.commands.registerCommand('kshield.restartBackendCheck', () => refreshBackendStatus()) + ); + + void refreshBackendStatus(); +} + +function scheduleScan(document: vscode.TextDocument): void { + if (!config().get('enabled', true) || !config().get('scanOnSave', true)) { + return; + } + if (document.uri.scheme !== 'file') { + return; + } + + clearDebounce(document.uri); + const delay = config().get('debounceMs', 800); + const timer = setTimeout(() => void runScan(document), delay); + debounceTimers.set(document.uri.toString(), timer); +} + +function clearDebounce(uri: vscode.Uri): void { + const key = uri.toString(); + const existing = debounceTimers.get(key); + if (existing) { + clearTimeout(existing); + debounceTimers.delete(key); + } +} + +async function runScan(document: vscode.TextDocument): Promise { + const filename = vscode.workspace.asRelativePath(document.uri, false); + statusBar.setScanning(); + try { + const result = await scanFile(filename, document.getText()); + store.set(document.uri, result.anomalies); + diagnosticCollection.set(document.uri, buildDiagnostics(document, result.anomalies)); + statusBar.setConnected(); + } catch (err) { + statusBar.setDisconnected(); + console.error('[KShield] scan failed:', err); + } +} + +async function applyFix(uri: vscode.Uri, anomalyId: string): Promise { + const anomaly = store.get(uri, anomalyId); + if (!anomaly || !anomaly.remediation.patch_diff) { + return; + } + + const document = await vscode.workspace.openTextDocument(uri); + const patched = applyUnifiedDiff(document.getText(), anomaly.remediation.patch_diff); + if (patched === null) { + void vscode.window.showWarningMessage( + 'KShield: could not apply the fix automatically — the file changed since the finding was generated. Re-scan and try again.' + ); + return; + } + + const fullRange = new vscode.Range(document.positionAt(0), document.positionAt(document.getText().length)); + const edit = new vscode.WorkspaceEdit(); + edit.replace(uri, fullRange, patched); + await vscode.workspace.applyEdit(edit); + await document.save(); + await runScan(document); +} + +async function handleSuppressRule(ruleType: string): Promise { + try { + await suppressRule(ruleType); + void vscode.window.showInformationMessage(`KShield: "${ruleType}" findings suppressed globally.`); + const editor = vscode.window.activeTextEditor; + if (editor) { + await runScan(editor.document); + } + } catch (err) { + void vscode.window.showErrorMessage(`KShield: failed to suppress rule — ${(err as Error).message}`); + } +} + +async function refreshBackendStatus(): Promise { + const healthy = await checkHealth(); + if (healthy) { + statusBar.setConnected(); + } else { + statusBar.setDisconnected(); + } +} + +export function deactivate(): void { + for (const timer of debounceTimers.values()) { + clearTimeout(timer); + } + debounceTimers.clear(); +} diff --git a/vscode-extension/src/hoverProvider.ts b/vscode-extension/src/hoverProvider.ts new file mode 100644 index 0000000..7ba6f21 --- /dev/null +++ b/vscode-extension/src/hoverProvider.ts @@ -0,0 +1,30 @@ +import * as vscode from 'vscode'; +import { AnomalyStore, KSHIELD_SOURCE } from './diagnostics'; + +export class KShieldHoverProvider implements vscode.HoverProvider { + constructor( + private readonly store: AnomalyStore, + private readonly diagnostics: vscode.DiagnosticCollection + ) {} + + provideHover(document: vscode.TextDocument, position: vscode.Position): vscode.Hover | undefined { + const fileDiagnostics = this.diagnostics.get(document.uri) ?? []; + const hit = fileDiagnostics.find((d) => d.source === KSHIELD_SOURCE && d.range.contains(position)); + if (!hit || typeof hit.code !== 'string') { + return undefined; + } + + const anomaly = this.store.get(document.uri, hit.code); + if (!anomaly) { + return undefined; + } + + const md = new vscode.MarkdownString(undefined, true); + md.appendMarkdown(`**KShield · ${anomaly.severity} · ${anomaly.type}**\n\n`); + md.appendMarkdown(`${anomaly.description}\n`); + if (anomaly.remediation.explanation) { + md.appendMarkdown(`\n---\n${anomaly.remediation.explanation}\n`); + } + return new vscode.Hover(md, hit.range); + } +} diff --git a/vscode-extension/src/patch.ts b/vscode-extension/src/patch.ts new file mode 100644 index 0000000..7dfa449 --- /dev/null +++ b/vscode-extension/src/patch.ts @@ -0,0 +1,94 @@ +interface DiffOp { + type: ' ' | '-' | '+'; + text: string; +} + +interface Hunk { + oldStart: number; + ops: DiffOp[]; +} + +const HUNK_HEADER = /^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@/; + +function parseHunks(diffText: string): Hunk[] { + const hunks: Hunk[] = []; + let current: Hunk | null = null; + + for (const line of diffText.split(/\r?\n/)) { + if (line.startsWith('---') || line.startsWith('+++')) { + continue; + } + const header = HUNK_HEADER.exec(line); + if (header) { + if (current) { + hunks.push(current); + } + current = { oldStart: parseInt(header[1], 10), ops: [] }; + continue; + } + if (!current) { + continue; + } + if (line.startsWith('+')) { + current.ops.push({ type: '+', text: line.slice(1) }); + } else if (line.startsWith('-')) { + current.ops.push({ type: '-', text: line.slice(1) }); + } else if (line.startsWith(' ')) { + current.ops.push({ type: ' ', text: line.slice(1) }); + } + } + if (current) { + hunks.push(current); + } + return hunks; +} + +/** + * Applies a unified diff (as produced by backend/app/engine/remediation.py via + * Python's difflib.unified_diff) to the given text. Returns null if a hunk's + * context/removed lines no longer match — the file changed since the finding + * was generated, so it's safer to bail out than to corrupt the file. + */ +export function applyUnifiedDiff(original: string, diffText: string): string | null { + if (!diffText.trim()) { + return null; + } + + const hunks = parseHunks(diffText); + if (hunks.length === 0) { + return null; + } + + const newline = original.includes('\r\n') ? '\r\n' : '\n'; + const lines = original.split(/\r?\n/); + + // Apply bottom-to-top so earlier hunks' line numbers stay valid. + for (const hunk of [...hunks].sort((a, b) => b.oldStart - a.oldStart)) { + let cursor = hunk.oldStart - 1; + if (cursor < 0 || cursor > lines.length) { + return null; + } + + const replacement: string[] = []; + for (const op of hunk.ops) { + if (op.type === ' ') { + if (lines[cursor] !== op.text) { + return null; + } + replacement.push(op.text); + cursor++; + } else if (op.type === '-') { + if (lines[cursor] !== op.text) { + return null; + } + cursor++; + } else { + replacement.push(op.text); + } + } + + lines.splice(hunk.oldStart - 1, cursor - (hunk.oldStart - 1), ...replacement); + } + + return lines.join(newline); +} diff --git a/vscode-extension/src/statusBar.ts b/vscode-extension/src/statusBar.ts new file mode 100644 index 0000000..58cd11a --- /dev/null +++ b/vscode-extension/src/statusBar.ts @@ -0,0 +1,40 @@ +import * as vscode from 'vscode'; + +export class KShieldStatusBar implements vscode.Disposable { + private readonly item: vscode.StatusBarItem; + + constructor() { + this.item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); + this.item.command = 'kshield.restartBackendCheck'; + this.setUnknown(); + this.item.show(); + } + + setUnknown(): void { + this.item.text = '$(shield) KShield'; + this.item.tooltip = 'KShield: checking backend connection…'; + this.item.backgroundColor = undefined; + } + + setScanning(): void { + this.item.text = '$(sync~spin) KShield'; + this.item.tooltip = 'KShield: scanning…'; + this.item.backgroundColor = undefined; + } + + setConnected(): void { + this.item.text = '$(shield) KShield'; + this.item.tooltip = 'KShield backend connected'; + this.item.backgroundColor = undefined; + } + + setDisconnected(): void { + this.item.text = '$(shield) KShield $(warning)'; + this.item.tooltip = 'KShield backend unreachable — run "kshield start", then click to retry.'; + this.item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); + } + + dispose(): void { + this.item.dispose(); + } +} diff --git a/vscode-extension/src/types.ts b/vscode-extension/src/types.ts new file mode 100644 index 0000000..d27907e --- /dev/null +++ b/vscode-extension/src/types.ts @@ -0,0 +1,31 @@ +// Mirrors backend/app/api/v1/scan.py response shape and cli/src/types.rs. + +export type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; + +export interface Remediation { + explanation: string; + patch_diff: string; +} + +export interface Anomaly { + id: string; + line: number; + type: string; + severity: Severity; + description: string; + remediation: Remediation; +} + +export interface ScanResult { + scan_id: string; + filename: string; + safe: boolean; + vulnerabilities_discovered: number; + anomalies: Anomaly[]; +} + +export interface SuppressConfig { + severities: string[]; + rules: string[]; + paths: string[]; +} diff --git a/vscode-extension/tsconfig.json b/vscode-extension/tsconfig.json new file mode 100644 index 0000000..9427cb1 --- /dev/null +++ b/vscode-extension/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "lib": ["ES2022"], + "outDir": "out", + "rootDir": "src", + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", ".vscode-test", "out"] +}