From 66eb3b2f8026d7ca121284d64e535e6714961ea9 Mon Sep 17 00:00:00 2001 From: chaksaray Date: Tue, 20 Oct 2026 06:41:23 +0700 Subject: [PATCH] feat(vscode): bawbel.aveApiUrl setting, AVE links resolved in one place The AVE records base URL was a hardcoded constant used at six call sites, each building its own URL, plus a literal in the report footer. - bawbel.aveApiUrl setting (default https://api.aveproject.org), with a schema pattern so the Settings UI rejects non-http(s) values - core/urls.ts (no vscode import): resolveAveApiUrl() parses the value with URL, requires http(s) and a host, and rebuilds it from origin and path; anything else gives the default. A crafted value cannot reach a link or the report HTML unescaped - core/links.ts: aveRecordLink() and aveRecordsIndexLink(), the only places that build AVE links; the report footer also escapes the URL - README settings table and CHANGELOG Closes #54 Co-Authored-By: Claude Opus 5.5 Signed-off-by: chaksaray --- vscode/CHANGELOG.md | 4 ++++ vscode/README.md | 1 + vscode/package.json | 7 +++++++ vscode/src/core/links.ts | 24 ++++++++++++++++++++++ vscode/src/core/types.ts | 2 +- vscode/src/core/urls.ts | 33 ++++++++++++++++++++++++++++++ vscode/src/extension.ts | 4 ++-- vscode/src/features/codeActions.ts | 5 +++-- vscode/src/features/diagnostics.ts | 8 ++++---- vscode/src/ui/reportPanel.ts | 3 ++- 10 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 vscode/src/core/links.ts create mode 100644 vscode/src/core/urls.ts diff --git a/vscode/CHANGELOG.md b/vscode/CHANGELOG.md index 960d3ab..b56302c 100644 --- a/vscode/CHANGELOG.md +++ b/vscode/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added +- `bawbel.aveApiUrl` setting: the base URL for AVE record links (default + https://api.aveproject.org). A value that is not an http(s) URL falls back to the default. + ### Fixed - Hover shows the OWASP AIVSS score (`aivss_score`) as `AIVSS: x/10`. It read the retired `cvss_ai` field, which bawbel-scanner no longer emits, and showed diff --git a/vscode/README.md b/vscode/README.md index e82f716..46647d6 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -134,6 +134,7 @@ To remove: right-click the faded hint → **Remove suppression**. | `bawbel.failOnSeverity` | `high` | Minimum severity shown as error squiggle | | `bawbel.scanExtensions` | `[.md,.yaml,.yml,.json,.txt]` | Extensions to auto-scan | | `bawbel.bawbelPath` | `""` | Path to bawbel binary (auto-detected if empty) | +| `bawbel.aveApiUrl` | `https://api.aveproject.org` | Base URL of the AVE records API; finding links open `/records/`. Must be http(s) | | `bawbel.showStatusBar` | `true` | Show status bar item | --- diff --git a/vscode/package.json b/vscode/package.json index d9c20e5..81cf426 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -150,6 +150,13 @@ "default": "", "description": "Absolute path to the bawbel binary. Leave empty to auto-detect (checks PATH and common pip install locations)." }, + "bawbel.aveApiUrl": { + "type": "string", + "default": "https://api.aveproject.org", + "pattern": "^https?://", + "patternErrorMessage": "Must be an http:// or https:// URL.", + "description": "Base URL of the AVE records API. Finding links open /records/. A value that is not an http(s) URL is ignored and the default is used." + }, "bawbel.showStatusBar": { "type": "boolean", "default": true, diff --git a/vscode/src/core/links.ts b/vscode/src/core/links.ts new file mode 100644 index 0000000..274d06d --- /dev/null +++ b/vscode/src/core/links.ts @@ -0,0 +1,24 @@ +/** + * core/links.ts: Bawbel VS Code Extension + * + * The one place that turns settings into AVE links. Call sites never build a records + * URL by hand. + */ + +import * as vscode from "vscode"; +import { aveRecordUrl, resolveAveApiUrl } from "./urls"; + +/** The AVE API base URL: the bawbel.aveApiUrl setting, validated, or the default. */ +export function aveApiUrl(): string { + return resolveAveApiUrl(vscode.workspace.getConfiguration("bawbel").get("aveApiUrl")); +} + +/** The link to one AVE record. */ +export function aveRecordLink(aveId: string): string { + return aveRecordUrl(aveApiUrl(), aveId); +} + +/** The link to the AVE records list. */ +export function aveRecordsIndexLink(): string { + return `${aveApiUrl()}/records`; +} diff --git a/vscode/src/core/types.ts b/vscode/src/core/types.ts index a5f48d2..636dd1f 100644 --- a/vscode/src/core/types.ts +++ b/vscode/src/core/types.ts @@ -82,7 +82,7 @@ export type StatusState = export const SUPPRESS_FILE = ".bawbel-suppress.json"; export const OUTPUT_CHANNEL = "Bawbel Scanner"; export const EXTENSION_ID = "bawbel.bawbel-scanner"; -export const AVE_RECORDS_BASE = "https://api.aveproject.org"; +export const DEFAULT_AVE_API_URL = "https://api.aveproject.org"; // setting: bawbel.aveApiUrl export const MARKETPLACE_URL = "https://marketplace.visualstudio.com/items?itemName=bawbel.bawbel-scanner"; export const SCAN_EXTENSIONS_DEFAULT = [".md", ".yaml", ".yml", ".json", ".txt"]; diff --git a/vscode/src/core/urls.ts b/vscode/src/core/urls.ts new file mode 100644 index 0000000..b3df403 --- /dev/null +++ b/vscode/src/core/urls.ts @@ -0,0 +1,33 @@ +/** + * core/urls.ts: Bawbel VS Code Extension + * + * Pure URL helpers. No vscode import, so they run and can be checked in plain Node. + * core/links.ts reads the settings and calls these. + */ + +import { DEFAULT_AVE_API_URL } from "./types"; + +/** + * The AVE API base URL to use for a raw setting value. The value is parsed as a URL and + * must be http(s) with a host; the result is rebuilt from origin and path (percent-encoded, + * no query or fragment, no trailing slash). Anything else gives DEFAULT_AVE_API_URL, so a + * bad setting never produces a link to an unexpected scheme or a broken HTML attribute. + */ +export function resolveAveApiUrl(raw: unknown): string { + if (typeof raw === "string") { + try { + const url = new URL(raw.trim()); + if ((url.protocol === "https:" || url.protocol === "http:") && url.hostname) { + return `${url.origin}${url.pathname}`.replace(/\/+$/, ""); + } + } catch { + // not a URL: fall through to the default + } + } + return DEFAULT_AVE_API_URL; +} + +/** The URL of one AVE record under base. */ +export function aveRecordUrl(base: string, aveId: string): string { + return `${base}/records/${encodeURIComponent(aveId)}`; +} diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 83b994e..e589860 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -35,8 +35,8 @@ import { SCAN_EXTENSIONS_DEFAULT, OUTPUT_CHANNEL, SUPPRESS_FILE, - AVE_RECORDS_BASE, } from "./core/types"; +import { aveRecordsIndexLink } from "./core/links"; import { Scanner, autoScanRequest, fullWorkspaceScanRequest, fullFolderScanRequest } from "./features/scanner"; import { DiagnosticsManager } from "./features/diagnostics"; @@ -260,7 +260,7 @@ async function cmdInstallCLI(): Promise { } function cmdOpenAveRecords(): void { - vscode.env.openExternal(vscode.Uri.parse(`${AVE_RECORDS_BASE}/records`)); + vscode.env.openExternal(vscode.Uri.parse(aveRecordsIndexLink())); } // ── Event handlers ──────────────────────────────────────────────────────────── diff --git a/vscode/src/features/codeActions.ts b/vscode/src/features/codeActions.ts index 3c28615..016d689 100644 --- a/vscode/src/features/codeActions.ts +++ b/vscode/src/features/codeActions.ts @@ -14,7 +14,8 @@ import * as path from "path"; import * as vscode from "vscode"; -import { BawbelFinding, AVE_RECORDS_BASE, SUPPRESS_FILE } from "../core/types"; +import { BawbelFinding, SUPPRESS_FILE } from "../core/types"; +import { aveRecordLink } from "../core/links"; import { isSuppressed, loadSuppressions } from "../core/suppressions"; import { DiagnosticsManager } from "./diagnostics"; @@ -157,7 +158,7 @@ export class BawbelCodeActionProvider implements vscode.CodeActionProvider { details.command = { command: "vscode.open", title: "Open AVE record", - arguments: [vscode.Uri.parse(`${AVE_RECORDS_BASE}/records/${aveId}`)], + arguments: [vscode.Uri.parse(aveRecordLink(aveId))], }; actions.push(details); } diff --git a/vscode/src/features/diagnostics.ts b/vscode/src/features/diagnostics.ts index 65d6bfc..e2d67e5 100644 --- a/vscode/src/features/diagnostics.ts +++ b/vscode/src/features/diagnostics.ts @@ -16,9 +16,9 @@ import { BawbelFileResult, SEVERITY_INDEX, SEVERITY_EMOJI, - AVE_RECORDS_BASE, SUPPRESS_FILE, } from "../core/types"; +import { aveRecordLink } from "../core/links"; import { getRemediation, hasSpecificRemediation } from "../core/remediation"; import { isSuppressed, loadSuppressions } from "../core/suppressions"; @@ -235,14 +235,14 @@ export class DiagnosticsManager { `Engine: ${f.engine}`, ].filter(Boolean).join(" | "), owasp ? `OWASP: ${owasp}` : "", - `Details: ${AVE_RECORDS_BASE}/records/${f.ave_id}`, + `Details: ${aveRecordLink(f.ave_id)}`, ].filter(s => s !== undefined).join("\n"); const diag = new vscode.Diagnostic(range, message, vsSev); diag.source = "Bawbel"; diag.code = { value: f.ave_id, - target: vscode.Uri.parse(`${AVE_RECORDS_BASE}/records/${f.ave_id}`), + target: vscode.Uri.parse(aveRecordLink(f.ave_id)), }; if (f.severity === "LOW") { @@ -267,7 +267,7 @@ export class DiagnosticsManager { diag.tags = [vscode.DiagnosticTag.Unnecessary]; diag.code = { value: f.ave_id, - target: vscode.Uri.parse(`${AVE_RECORDS_BASE}/records/${f.ave_id}`), + target: vscode.Uri.parse(aveRecordLink(f.ave_id)), }; return diag; } diff --git a/vscode/src/ui/reportPanel.ts b/vscode/src/ui/reportPanel.ts index 903a79f..3f593d4 100644 --- a/vscode/src/ui/reportPanel.ts +++ b/vscode/src/ui/reportPanel.ts @@ -9,6 +9,7 @@ */ import * as vscode from "vscode"; +import { aveRecordsIndexLink } from "../core/links"; import { runCommand } from "../core/cli"; import { BawbelFileResult } from "../core/types"; @@ -129,7 +130,7 @@ export class ReportPanel {

Bawbel Report — ${escapeHtml(name)}

${html} -

Generated by bawbel report · AVE records

+

Generated by bawbel report · AVE records

`; }