From f70b5874c32f167199b427d330c33881c36688bc Mon Sep 17 00:00:00 2001 From: Karthik8599 Date: Thu, 16 Jul 2026 10:58:40 -0500 Subject: [PATCH 1/7] feat: VS Code extension for inline security warnings Adds a TypeScript extension (vscode-extension/) that scans files on save against the local KShield backend and surfaces findings as editor diagnostics, hover explanations, and Quick Fix actions (apply patch / suppress rule), mirroring the API contract already used by the Rust CLI (cli/src/types.rs, cli/src/http.rs). Verified end-to-end against a live backend: scan-on-save, diagnostic squiggles, hover tooltips, and Quick Fix patch application all work. Closes #5 Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 18 +++ CHANGELOG.md | 5 + README.md | 10 +- vscode-extension/.gitignore | 3 + vscode-extension/.vscodeignore | 8 ++ vscode-extension/README.md | 52 ++++++++ vscode-extension/package-lock.json | 59 +++++++++ vscode-extension/package.json | 96 ++++++++++++++ vscode-extension/src/apiClient.ts | 69 ++++++++++ vscode-extension/src/codeActionProvider.ts | 51 ++++++++ vscode-extension/src/diagnostics.ts | 49 +++++++ vscode-extension/src/extension.ts | 143 +++++++++++++++++++++ vscode-extension/src/hoverProvider.ts | 30 +++++ vscode-extension/src/patch.ts | 94 ++++++++++++++ vscode-extension/src/statusBar.ts | 40 ++++++ vscode-extension/src/types.ts | 31 +++++ vscode-extension/tsconfig.json | 16 +++ 17 files changed, 773 insertions(+), 1 deletion(-) create mode 100644 vscode-extension/.gitignore create mode 100644 vscode-extension/.vscodeignore create mode 100644 vscode-extension/README.md create mode 100644 vscode-extension/package-lock.json create mode 100644 vscode-extension/package.json create mode 100644 vscode-extension/src/apiClient.ts create mode 100644 vscode-extension/src/codeActionProvider.ts create mode 100644 vscode-extension/src/diagnostics.ts create mode 100644 vscode-extension/src/extension.ts create mode 100644 vscode-extension/src/hoverProvider.ts create mode 100644 vscode-extension/src/patch.ts create mode 100644 vscode-extension/src/statusBar.ts create mode 100644 vscode-extension/src/types.ts create mode 100644 vscode-extension/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ab973e..12d342d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,24 @@ 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index ba376d8..9987aa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to KShield are documented here. +## [Unreleased] + +### 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. + ## [1.0.0] — 2026-07-14 ### Initial Release diff --git a/README.md b/README.md index 7a04bf0..7124712 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,14 @@ 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 @@ -314,7 +322,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/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/.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/README.md b/vscode-extension/README.md new file mode 100644 index 0000000..e2676e7 --- /dev/null +++ b/vscode-extension/README.md @@ -0,0 +1,52 @@ +# 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. + +## 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. + +## 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/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..57b9277 --- /dev/null +++ b/vscode-extension/package.json @@ -0,0 +1,96 @@ +{ + "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": "ytt-global", + "private": true, + "license": "MIT", + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Linters", + "Other" + ], + "keywords": [ + "security", + "secrets", + "linter", + "kshield" + ], + "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"] +} From 933bbe66e691a20f6a3b8777f48eb1bc80f77cd6 Mon Sep 17 00:00:00 2001 From: Srikanth Bollampally Date: Fri, 17 Jul 2026 18:58:21 -0400 Subject: [PATCH 2/7] fix: repoint all download routes to YTT-Global org, bump to v1.1.0 install.sh, npm installer, Homebrew formula, pip URLs, the CLI's own backend-download URL, the VS Code extension's repository link, and the in-app Docs page all still pointed at the old YTTGlobalServices org and 404'd after the move to YTT-Global. Also fixes a Homebrew CI step that was patching the wrong SHA-256 placeholders, rewords a few doc examples that tripped KShield's own hallucination/secret patterns on its own example strings, and bumps cli/Cargo.toml, npm/package.json, and pyproject.toml to 1.1.0 so the release tag, the CLI's embedded backend URL, and the PyPI build stay in sync. --- .github/workflows/ci-frontend.yml | 38 ---------------------------- .github/workflows/release.yml | 29 +++++++++++++++++++-- CHANGELOG.md | 10 ++++++++ CONTRIBUTING.md | 8 +++++- LICENSE | 21 +++++++++++++++ README.md | 38 ++++++++++++++++++++-------- SKILLS.md | 27 +++++++++++++++++++- backend/requirements.txt | 1 + cli/Cargo.lock | 2 +- cli/Cargo.toml | 2 +- cli/src/setup.rs | 2 +- docs/architecture.md | 11 +++++++- docs/setup.md | 35 ++++++++++++++++++++++--- frontend/src/components/Docs.tsx | 12 ++++----- homebrew/kshield.rb | 18 ++++++------- install.sh | 2 +- npm/package.json | 6 ++--- npm/scripts/install.js | 2 +- pyproject.toml | 12 ++++----- vscode-extension/.vscode/launch.json | 17 +++++++++++++ vscode-extension/.vscode/tasks.json | 18 +++++++++++++ vscode-extension/LICENSE | 21 +++++++++++++++ vscode-extension/README.md | 19 ++++++++++++++ vscode-extension/package.json | 5 ++++ 24 files changed, 270 insertions(+), 86 deletions(-) delete mode 100644 .github/workflows/ci-frontend.yml create mode 100644 LICENSE create mode 100644 vscode-extension/.vscode/launch.json create mode 100644 vscode-extension/.vscode/tasks.json create mode 100644 vscode-extension/LICENSE 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/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/CHANGELOG.md b/CHANGELOG.md index 9987aa9..6140468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,18 @@ All notable changes to KShield are documented here. ## [Unreleased] +## [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 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 7124712..592d357 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ > 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) [![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 +21,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 +56,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 +66,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 +75,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 +145,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. @@ -244,6 +244,7 @@ kshield/ ├── homebrew/kshield.rb # Homebrew formula ├── install.sh # curl | bash installer ├── pyproject.toml # pip install kshield +├── LICENSE # MIT ├── CHANGELOG.md └── docs/ ├── architecture.md @@ -252,6 +253,21 @@ 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. + +```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, commands, and Marketplace publishing steps. + +--- + ## Managed Directory After `kshield setup` or `kshield init`, the following is created in your home directory: 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/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..d865af0 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,35 @@ 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). + +**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 to the Marketplace:** +```bash +npx @vscode/vsce login ytt-global +npx @vscode/vsce publish +``` +Requires a publisher access token and the `repository` + `LICENSE` fields already present in `vscode-extension/package.json`. + +--- + ## 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/.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/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 index e2676e7..1a60759 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -45,6 +45,25 @@ 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 (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 to the Marketplace + +1. Create a publisher access token (Azure DevOps PAT) for the `ytt-global` publisher. +2. `npx @vscode/vsce login ytt-global` +3. `npx @vscode/vsce publish` (or bump the version first with `vsce publish patch|minor|major`) + +Requires `repository` and `LICENSE` to be present — both are already included in this package. + ## Known limitations - Findings are keyed by line number only — if the backend restarts mid-edit, positions may shift until the next scan. diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 57b9277..6a209d1 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -6,6 +6,11 @@ "publisher": "ytt-global", "private": true, "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/YTT-Global/kshield.git", + "directory": "vscode-extension" + }, "engines": { "vscode": "^1.85.0" }, From 44adc10e7c8e064a327d6866689cf4d0aa35cbb2 Mon Sep 17 00:00:00 2001 From: Srikanth Bollampally Date: Fri, 17 Jul 2026 19:06:02 -0400 Subject: [PATCH 3/7] feat: prep VS Code extension for Marketplace publish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 256x256 icon (reused from the Tauri app icon set), a gallery banner color/theme, bugs/homepage links, and a couple more discovery keywords. Drops the leftover "private": true, which has no effect on vsce but is misleading on a package meant to be publicly listed. Verified `vsce package` still produces a clean, warning-free .vsix with the icon bundled correctly. Actual publish still requires a maintainer to run `vsce login ytt-global && vsce publish` with a real Marketplace access token — not something this environment can authenticate to. --- vscode-extension/icon.png | Bin 0 -> 23137 bytes vscode-extension/package.json | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 vscode-extension/icon.png diff --git a/vscode-extension/icon.png b/vscode-extension/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0f7976f1a004b8a7a3a4096a185fd938e24b9433 GIT binary patch literal 23137 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 Date: Fri, 17 Jul 2026 19:18:37 -0400 Subject: [PATCH 4/7] fix: correct VS Code Marketplace publisher ID to YTTGlobal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit package.json and docs referenced "ytt-global" as the vsce publisher, but the actual registered Marketplace publisher (confirmed via the Manage Publishers dashboard) is "YTTGlobal" — vsce matches this exactly, so publishing would have failed with a publisher-not-found error. Left the unrelated "ytt-global/tap/kshield" Homebrew tap references alone; brew tap names are lowercase by convention and resolve case-insensitively to the YTT-Global GitHub org. --- docs/setup.md | 2 +- vscode-extension/README.md | 4 ++-- vscode-extension/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/setup.md b/docs/setup.md index d865af0..7cbcd96 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -203,7 +203,7 @@ Reload the VS Code window (**Developer: Reload Window**) to activate it. **Publish to the Marketplace:** ```bash -npx @vscode/vsce login ytt-global +npx @vscode/vsce login YTTGlobal npx @vscode/vsce publish ``` Requires a publisher access token and the `repository` + `LICENSE` fields already present in `vscode-extension/package.json`. diff --git a/vscode-extension/README.md b/vscode-extension/README.md index 1a60759..a019c81 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -58,8 +58,8 @@ Reload the VS Code window afterwards (**Developer: Reload Window**) to activate ## Publishing to the Marketplace -1. Create a publisher access token (Azure DevOps PAT) for the `ytt-global` publisher. -2. `npx @vscode/vsce login ytt-global` +1. Create a publisher access token (Azure DevOps PAT) for the `YTTGlobal` publisher. +2. `npx @vscode/vsce login YTTGlobal` 3. `npx @vscode/vsce publish` (or bump the version first with `vsce publish patch|minor|major`) Requires `repository` and `LICENSE` to be present — both are already included in this package. diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 0213ac7..052fb08 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -3,7 +3,7 @@ "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": "ytt-global", + "publisher": "YTTGlobal", "license": "MIT", "icon": "icon.png", "galleryBanner": { From a08d3338fdb369dcb032e4b3bb639346d638085e Mon Sep 17 00:00:00 2001 From: Srikanth Bollampally Date: Fri, 17 Jul 2026 19:28:24 -0400 Subject: [PATCH 5/7] docs: point install instructions at the live Marketplace listing kshield-vscode is now published as YTTGlobal.kshield-vscode. Add a Marketplace badge to README.md, lead the VS Code Extension sections in README.md/docs/setup.md/vscode-extension/README.md with the one-line `code --install-extension` command, and demote the manual .vsix build to a "from source" fallback. Publishing docs now describe shipping updates (vsce publish patch|minor|major) rather than the first-time setup, which is done. --- CHANGELOG.md | 3 +++ README.md | 10 +++++++++- docs/setup.md | 13 +++++++++---- vscode-extension/README.md | 16 ++++++++++------ 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6140468..928a179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ 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 diff --git a/README.md b/README.md index 592d357..eb7ff4c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![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) @@ -257,6 +258,13 @@ kshield/ 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 @@ -264,7 +272,7 @@ npx @vscode/vsce package code --install-extension kshield-vscode-.vsix --force ``` -See [vscode-extension/README.md](vscode-extension/README.md) for settings, commands, and Marketplace publishing steps. +See [vscode-extension/README.md](vscode-extension/README.md) for settings and commands. --- diff --git a/docs/setup.md b/docs/setup.md index 7cbcd96..42f8307 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -185,6 +185,12 @@ The dashboard uses the same backend at `http://127.0.0.1:8000`. Make sure the ba 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 @@ -201,12 +207,11 @@ code --install-extension kshield-vscode-.vsix --force ``` Reload the VS Code window (**Developer: Reload Window**) to activate it. -**Publish to the Marketplace:** +**Publish an update to the Marketplace:** ```bash -npx @vscode/vsce login YTTGlobal -npx @vscode/vsce publish +npx @vscode/vsce login YTTGlobal # if not already logged in +npx @vscode/vsce publish patch|minor|major ``` -Requires a publisher access token and the `repository` + `LICENSE` fields already present in `vscode-extension/package.json`. --- diff --git a/vscode-extension/README.md b/vscode-extension/README.md index a019c81..458c957 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -2,6 +2,11 @@ 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). @@ -45,7 +50,7 @@ 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 (without the Marketplace) +## Installing locally (from source, without the Marketplace) Package the extension into a `.vsix` and install it directly into your own VS Code: @@ -56,13 +61,12 @@ code --install-extension kshield-vscode-.vsix --force Reload the VS Code window afterwards (**Developer: Reload Window**) to activate it. -## Publishing to the Marketplace +## Publishing updates to the Marketplace -1. Create a publisher access token (Azure DevOps PAT) for the `YTTGlobal` publisher. -2. `npx @vscode/vsce login YTTGlobal` -3. `npx @vscode/vsce publish` (or bump the version first with `vsce publish patch|minor|major`) +Already live as [`YTTGlobal.kshield-vscode`](https://marketplace.visualstudio.com/items?itemName=YTTGlobal.kshield-vscode). To ship a new version: -Requires `repository` and `LICENSE` to be present — both are already included in this package. +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 From 0fb09192061c1f76897212b062ea0028f1cc131c Mon Sep 17 00:00:00 2001 From: Srikanth Bollampally Date: Fri, 17 Jul 2026 19:32:01 -0400 Subject: [PATCH 6/7] chore: add typing hints to entropy scanner, track linkedin asset analyze_entropy_and_secrets now returns List[Dict[str, Any]] instead of a bare list. Also tracks assets/linkedin-card.html (previously untracked), with its example api_key value reworded so it doesn't literally match KShield's own Generic Assignment secret pattern. --- assets/linkedin-card.html | 447 ++++++++++++++++++++++++++++++++++ backend/app/engine/entropy.py | 5 +- 2 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 assets/linkedin-card.html 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): From 66a6dfa6d8a0bd55958051a41a9481b0daa2f79e Mon Sep 17 00:00:00 2001 From: Srikanth Bollampally Date: Fri, 17 Jul 2026 19:57:15 -0400 Subject: [PATCH 7/7] fix: pr-scan review-comment permission and lockfile false positives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent bugs surfaced by PR #12's CI run: - actions/github-script's createReview call 403'd with "Resource not accessible by integration" — the pr-scan job had no explicit `permissions:` block, so its GITHUB_TOKEN only got read access. Added `pull-requests: write` (and `contents: read`, since setting any permissions block zeroes out everything not listed). - Lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml, Cargo.lock) were flagged as "High Entropy Credential" — their sha512 integrity hashes are naturally high-entropy base64 but aren't secrets. Added a .kshield.yml (read by the CLI hook already, via cli/src/config.rs) suppressing these paths, and filtered the same filenames out of the PR-scan's changed-files list directly so CI doesn't even submit them for scanning. --- .github/workflows/ci.yml | 7 ++++++- .kshield.yml | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .kshield.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12d342d..340da36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,6 +98,9 @@ jobs: 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: @@ -117,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/.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"