Skip to content

Commit ecd0390

Browse files
committed
Add context menus and a Replay shortcut to the plugin's own tabs
Caido builds a command's page context from a fixed list of its own routes, so a plugin page contributes none and the global bindings cannot see what a plugin table has selected. The plugin's views therefore carry their own: - HTTP Records and Findings rows, and their request and response panes, get a right-click menu; Cmd-R / Ctrl-R sends whatever is on screen to Replay. - Findings replay evidence through a new sendRawToReplay, which recovers the target from the message's own Host header, with matchedAt supplying only the scheme - so an agent finding, whose matchedAt is a source path, still works. - HttpMessageView rebuilds its editors when Caido tears them down on page navigation, so a pane no longer comes back empty or showing the last record. - Per-request bridge and proxy success logs are dropped: a scan drove a few thousand of them and buried every other plugin's output. Failures still log. Bump to 0.1.1 and repack caido-vigolium.zip.
1 parent 8b1ce66 commit ecd0390

21 files changed

Lines changed: 616 additions & 65 deletions

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ resulting findings, and synchronizing traffic in both directions. It supports ex
55
Caido, automatic Proxy forwarding, Sitemap snapshots, and an optional loopback-only live bridge for
66
CLI and server integrations.
77

8-
- **Version:** `0.1.0`
8+
- **Version:** `0.1.1`
99
- **GitHub:** [github.com/vigolium/vigolium](https://github.com/vigolium/vigolium)
1010
- **Docs:** [docs.vigolium.com](https://docs.vigolium.com/)
1111
- **Plugin guide:**
@@ -162,6 +162,29 @@ Every binding is also a command: open the palette with `⌘K` / `Ctrl+K` and sea
162162
The context-menu actions on a request row, request pane or response pane do the same thing without
163163
a keyboard.
164164

165+
Those five bindings push Caido's own traffic into Vigolium, so they act on whatever Caido has
166+
selected - a row in HTTP History, Search, Sitemap or Replay - and report `nothing selected to send`
167+
anywhere else. They cannot read a selection inside the Vigolium page itself: Caido builds a
168+
command's page context from a fixed list of its own routes, and a plugin page contributes none.
169+
170+
The plugin's own views therefore carry their own bindings and menus, which work only while that view
171+
is on screen:
172+
173+
| Where | Action |
174+
| ------------------------- | ------------------------------------------------------------------------------------- |
175+
| HTTP Records row or panes | Right-click for Send to Replay, Scan, Copy URL, Delete |
176+
| Findings row or evidence | Right-click for Send to Replay, Copy as Markdown, Copy request, Copy response, Delete |
177+
| Either tab | `⌘R` / `Ctrl+R` sends the open record - or the evidence on screen - to Replay |
178+
179+
A finding's evidence is stored text with no request behind it, so replaying it re-imports the
180+
message. The target is recovered from the message's own `Host` header, with the finding's
181+
`matchedAt` supplying only the scheme - an absolute request target wins over both, and an agent
182+
finding, whose `matchedAt` is a source path, still replays as long as the message carries a `Host`.
183+
184+
Right-click does nothing on these views in stock Caido. Its `RequestRow`, `Request` and `Response`
185+
menus are attached by Caido's own tables and request panes; `sdk.ui.httpRequestEditor()` hands a
186+
plugin a bare editor with no menu wiring, so the plugin has to supply its own.
187+
165188
The Burp extension's `Ctrl+Alt+…` bindings carry over on Windows and Linux. macOS uses `⌘⌃`
166189
instead, because `Alt` there is the Option dead-key - the OS turns `Alt+V` into `` before Caido
167190
sees it. `⌘⇧` would have been the obvious choice but is already taken: Caido binds `⌘⇧A` to Automate

caido-vigolium.zip

8.41 KB
Binary file not shown.

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "vigolium",
33
"name": "Vigolium",
4-
"version": "0.1.0",
4+
"version": "0.1.1",
55
"description": "Send Caido traffic to the Vigolium scanning engine, review findings, and bridge traffic in both directions with the Vigolium CLI and server.",
66
"author": {
77
"name": "Vigolium",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "caido-vigolium",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"private": true,
55
"type": "module",
66
"description": "Vigolium plugin for Caido",

packages/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "backend",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"private": true,
55
"type": "module",
66
"types": "./src/index.ts",

packages/backend/src/bridge/caido.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,14 @@ export async function openInReplay(
242242
collectionId?: string,
243243
): Promise<string> {
244244
const requestId = await injectRequest(sdk, input);
245-
const session = await sdk.replay.createSession(requestId, collectionId);
245+
// Caido converts every argument it is handed, so an explicit `undefined`
246+
// collection is not read as "no collection" - it fails the ID conversion with
247+
// "Not an ID. Error converting from js 'undefined' into type 'string'". The
248+
// argument has to be left off entirely, which is why this is two calls.
249+
const session =
250+
collectionId === undefined
251+
? await sdk.replay.createSession(requestId)
252+
: await sdk.replay.createSession(requestId, collectionId);
246253
await renameSession(sdk, session.getId(), name);
247254
return session.getId();
248255
}

packages/backend/src/bridge/routes.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ export const MAX_REPEATER_BODY_BYTES = 2 * 1024 * 1024;
4343
const DEFAULT_SEND_TIMEOUT_MS = 30_000;
4444
const MAX_SEND_TIMEOUT_MS = 120_000;
4545

46+
/*
47+
* A note on logging, which these routes deliberately do almost none of.
48+
*
49+
* A scan drives /send, /sitemap, /repeater and /organizer once per request it
50+
* makes, so a line each - however terse - buries every other plugin's logs
51+
* under a few thousand entries nobody reads. What the line would have said is
52+
* already somewhere better: the caller gets it in the reply, and the traffic
53+
* itself lands in Caido's own Sitemap and Replay.
54+
*
55+
* Failures are the exception, being rare and worth interrupting for. Most
56+
* arrive here as a throw and are logged centrally by the service; a send that
57+
* comes back unsent is not one of those, so it logs where it happens.
58+
*/
59+
4660
const SCOPE_BLOCKED_MESSAGE =
4761
"target is out of Caido scope; disable in-scope-only or add it to the project scope";
4862

@@ -210,7 +224,6 @@ export async function sitemap(ctx: RouteContext, args: Json): Promise<Json> {
210224
source,
211225
});
212226
await addToSitemap(ctx.sdk, requestId);
213-
ctx.log.info(`[Bridge] Added 1 item to the Sitemap from ${source}`);
214227

215228
return {
216229
added: 1,
@@ -267,7 +280,6 @@ export async function repeater(ctx: RouteContext, args: Json): Promise<Json> {
267280
},
268281
tabName,
269282
);
270-
ctx.log.info(`[Bridge] Opened Replay session "${tabName}"`);
271283

272284
const output: Json = {
273285
sent: 1,
@@ -324,11 +336,11 @@ export async function send(ctx: RouteContext, args: Json): Promise<Json> {
324336
output.added_to_sitemap = false;
325337
}
326338

327-
ctx.log.info(
328-
outcome.sent
329-
? `[Bridge] Sent 1 request via Caido to ${resolved.url} (HTTP ${outcome.statusCode})`
330-
: `[Bridge] Send via Caido to ${resolved.url} failed: ${outcome.error ?? "unknown"}`,
331-
);
339+
if (!outcome.sent) {
340+
ctx.log.warn(
341+
`[Bridge] Send via Caido to ${resolved.url} failed: ${outcome.error ?? "unknown"}`,
342+
);
343+
}
332344
return output;
333345
}
334346

@@ -394,7 +406,6 @@ export async function organizer(ctx: RouteContext, args: Json): Promise<Json> {
394406
notes || source,
395407
collectionId,
396408
);
397-
ctx.log.info(`[Bridge] Added 1 item to Replay collection "${collectionName}" from ${source}`);
398409

399410
const output: Json = {
400411
added: 1,

packages/backend/src/index.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import { openInReplay } from "./bridge/caido";
2525
import { BridgeService } from "./bridge/service";
2626
import { RequestCounters } from "./counters";
2727
import { publish } from "./events";
28-
import { fromBase64 } from "./util/bytes";
28+
import { fromBase64, fromUtf8 } from "./util/bytes";
29+
import { deriveRequestUrl } from "./util/rawhttp";
2930
import { LogService } from "./logging";
3031
import { ProxyForwarder } from "./proxy/forwarder";
3132
import { SettingsStore, blankToNull, splitModules } from "./settings";
@@ -244,6 +245,44 @@ async function sendRecordToReplay(sdk: SDK, uuid: string): Promise<string> {
244245
return sessionId;
245246
}
246247

248+
/**
249+
* Opens a raw message in Caido Replay - a finding's evidence, which has no
250+
* stored record behind it to look up.
251+
*
252+
* `urlHint` is the caller's best guess at the target, typically a finding's
253+
* `matchedAt`. It is only a hint because that field is not always a URL at all:
254+
* an agent finding matches a source file. The message itself is the better
255+
* authority, so the two are reconciled rather than one being trusted outright.
256+
*/
257+
async function sendRawToReplay(
258+
sdk: SDK,
259+
urlHint: string,
260+
request: string,
261+
response: string,
262+
name: string,
263+
): Promise<string> {
264+
const { log } = required();
265+
const requestBytes = fromUtf8(request);
266+
if (requestBytes.length === 0) {
267+
throw new VigoliumApiError(0, "There is no request to replay");
268+
}
269+
270+
const url = deriveRequestUrl(requestBytes, urlHint);
271+
const responseBytes = response ? fromUtf8(response) : null;
272+
const sessionId = await openInReplay(
273+
sdk,
274+
{
275+
url,
276+
requestBytes,
277+
responseBytes: responseBytes && responseBytes.length > 0 ? responseBytes : null,
278+
source: "vigolium-evidence",
279+
},
280+
name || "vigolium",
281+
);
282+
log.info(`[Findings] Opened Replay session for ${url}`);
283+
return sessionId;
284+
}
285+
247286
/**
248287
* The RPC surface, declared once.
249288
*
@@ -284,6 +323,7 @@ const HANDLERS = {
284323
agentSessions,
285324
agentSessionLogs,
286325
sendRecordToReplay,
326+
sendRawToReplay,
287327
};
288328

289329
export type API = DefineAPI<typeof HANDLERS>;

packages/backend/src/proxy/forwarder.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,12 @@ export class ProxyForwarder {
6363
http_request_base64: requestBase64,
6464
http_response_base64: responseBase64,
6565
});
66+
// Deliberately silent on success: this runs once per proxied response,
67+
// so a line each would bury every other plugin's logs. The counters
68+
// already carry the sent/failed tally to the Settings tab, which is
69+
// where a per-request rate belongs. Failures still log - they are rare,
70+
// and each one is a request Vigolium never saw.
6671
this.#counters.markSent();
67-
this.#log.info("[Proxy] Sent 1 request");
6872
} catch (e) {
6973
this.#counters.markFailed();
7074
this.#log.error(`[Proxy] Request failed: ${errorMessage(e)}`);

packages/backend/src/util/bytes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,14 @@ export function toLatin1(bytes: Uint8Array, maxBytes?: number): string {
4848
export function fromLatin1(value: string): Uint8Array {
4949
return new Uint8Array(Buffer.from(value, "latin1"));
5050
}
51+
52+
/**
53+
* UTF-8, for text that reached us as text.
54+
*
55+
* A finding's evidence arrives decoded inside a JSON document rather than as
56+
* bytes, so latin-1 - correct for a message carried through the editor - would
57+
* flatten every multi-byte character it picked up on the way.
58+
*/
59+
export function fromUtf8(value: string): Uint8Array {
60+
return new Uint8Array(Buffer.from(value, "utf8"));
61+
}

0 commit comments

Comments
 (0)