Skip to content

Commit 7048d57

Browse files
authored
Merge pull request #268 from caido-community/ae-plugin-api
Scanner API
2 parents 0bcfb8c + 68f6909 commit 7048d57

137 files changed

Lines changed: 1742 additions & 811 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ env:
77
NODE_VERSION: 20
88
PNPM_VERSION: 9
99

10+
permissions:
11+
id-token: write
12+
contents: write
13+
1014
jobs:
1115
release:
1216
name: Release
1317
runs-on: ubuntu-latest
14-
permissions:
15-
contents: write
1618

1719
steps:
1820
- name: Verify main branch
@@ -29,6 +31,7 @@ jobs:
2931
uses: actions/setup-node@v4
3032
with:
3133
node-version: ${{ env.NODE_VERSION }}
34+
registry-url: 'https://registry.npmjs.org'
3235

3336
- name: Setup pnpm
3437
uses: pnpm/action-setup@v4
@@ -57,6 +60,13 @@ jobs:
5760
VERSION=$(unzip -p plugin_package.zip manifest.json | jq -r .version)
5861
echo "version=${VERSION}" >> $GITHUB_OUTPUT
5962
63+
- name: Publish package
64+
working-directory: packages/shared
65+
run: |
66+
pnpm run build
67+
npm pkg set "version=${{ steps.meta.outputs.version }}"
68+
npm publish --access public --provenance
69+
6070
- name: Create release
6171
uses: caido/action-release@v1
6272
with:

AGENTS.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,20 @@ pnpm vitest run --coverage packages/backend/src/checks/<check-name>/index.spec.t
2020

2121
This is a **Caido vulnerability scanner plugin** organized as a pnpm monorepo with four packages:
2222

23-
- **`packages/engine`**Core scanning engine. Defines the check execution model (step-based), types (`CheckDefinition`, `Finding`, `Severity`), and test utilities (`runCheck`, `createMockRequest`, `createMockResponse`). Imported as `"engine"` by other packages.
24-
- **`packages/backend`**Backend plugin. Contains 40+ vulnerability checks in `src/checks/`, plus services, stores, and storage layers. Registers API endpoints and hooks into Caido SDK events (intercepted responses for passive scanning, `sdk.requests.send()` for active scanning).
25-
- **`packages/frontend`**Vue 3 + Pinia + PrimeVue + TailwindCSS frontend. Registers a sidebar page at `/scanner` and a keyboard shortcut (Ctrl+Shift+S) for active scanning.
26-
- **`packages/shared`**Shared TypeScript types between frontend and backend.
23+
- **`packages/shared`**Published as `@caido-community/scanner` on NPM. Type-only contract: `API`, `Events`, `Spec`, plus all public types (`Session`, `UserConfig`, `Finding`, `Severity`, `ScanConfig`, `CheckMetadata`, ...). Self-contained, zero workspace deps. Built with `tsdown` via the `prepare` script. Backend, frontend, and external CI/CD clients all consume `Spec`.
24+
- **`packages/engine`**Core scanning engine. Defines the check execution model (step-based), runtime helpers (`defineCheck`, `defineCheckV2`, `runCheck`, `createRegistry`, `createScheduler`, `keyStrategy`, `createMockRequest`, `createMockResponse`). Imports its public types from `shared` and re-exports them for backwards compatibility with existing checks.
25+
- **`packages/backend`**Backend plugin. Contains 40+ vulnerability checks in `src/checks/`. RPC handlers live in `src/api/{checks,config,queue,scanner}.ts` and delegate to services in `src/services/`. The SDK is set once via `setSDK(sdk)` in `src/sdk.ts`; services pull it via `requireSDK()`. Hooks into Caido SDK events (intercepted responses for passive scanning, `sdk.requests.send()` for active scanning).
26+
- **`packages/frontend`**Vue 3 + Pinia + PrimeVue + TailwindCSS frontend. Depends on `shared` only — never on `backend` or `engine`. Registers a sidebar page at `/scanner` and a keyboard shortcut (Ctrl+Shift+S) for active scanning.
2727

28-
Plugin configuration lives in `caido.config.ts`. The plugin is built using `@caido-community/dev`.
28+
The frontend↔backend contract is `Spec = DefinePluginPackageSpec<{ manifestId: "scanner"; api: API; events: Events }>` from `packages/shared/src/index.ts`. Backend uses `SDK<Spec>`, frontend uses `Caido<Spec>`. Plugin configuration lives in `caido.config.ts`. The plugin is built using `@caido-community/dev`.
29+
30+
### Adding an API endpoint
31+
32+
1. Add the method signature to `packages/shared/src/api.ts` (no `SDK` first param — args only).
33+
2. Implement the service in `packages/backend/src/services/<domain>.ts`. Call `requireSDK()` if you need the SDK; never accept it as a parameter.
34+
3. Add the wrapper in `packages/backend/src/api/<domain>.ts` (`(_sdk: SDK, ...args) => service(...args)`) and re-export from `src/api/index.ts`.
35+
4. Register it in `packages/backend/src/index.ts` via `sdk.api.register("methodName", api.apiMethodName)`.
36+
5. Add a unit test in `packages/backend/src/api/<domain>.test.ts` that mocks the relevant store and asserts the result.
2937

3038
## Check Architecture
3139

caido.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default defineConfig({
1212
id,
1313
name: "Scanner",
1414
description: "Passive and active vulnerability scanner",
15-
version: "1.0.7",
15+
version: "1.0.8",
1616
author: {
1717
name: "Caido Labs Inc.",
1818
email: "dev@caido.io",

knip.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,24 @@ const config: RawConfigurationOrFn = {
44
workspaces: {
55
".": {
66
entry: ["caido.config.ts"],
7-
ignoreDependencies: ["@caido/sdk-backend", "@vitest/coverage-v8", "rollup-plugin-dts"],
7+
ignoreDependencies: ["@vitest/coverage-v8", "rollup-plugin-dts"],
88
},
99
"packages/backend": {
1010
project: ["src/**/*.ts"],
11-
ignoreDependencies: ["caido", "@lezer/common", "@lezer/generator"],
11+
ignoreDependencies: [
12+
"caido",
13+
"shared",
14+
"@lezer/common",
15+
"@lezer/generator",
16+
],
1217
ignore: [
1318
"src/parsers/**/__generated__*",
1419
"src/checks/sql-injection/mysql-time-based/**",
1520
],
1621
},
1722
"packages/frontend": {
1823
project: ["src/**/*.{ts,tsx,vue}"],
24+
ignoreDependencies: ["shared"],
1925
ignore: [
2026
"src/views/Queue.vue",
2127
"src/components/queue/**",
@@ -27,7 +33,7 @@ const config: RawConfigurationOrFn = {
2733
},
2834
"packages/engine": {
2935
project: ["src/**/*.ts"],
30-
ignoreDependencies: ["caido"],
36+
ignoreDependencies: ["caido", "shared"],
3137
ignore: ["src/__tests__/**"],
3238
},
3339
"packages/trace-viewer": {

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"scripts": {
99
"typecheck": "pnpm -r typecheck",
1010
"build": "caido-dev build",
11+
"build:shared": "pnpm --filter @caido-community/scanner run build",
1112
"watch": "caido-dev watch",
1213
"lint": "eslint --fix packages/*/src",
1314
"knip": "knip",
@@ -19,7 +20,7 @@
1920
"devDependencies": {
2021
"@caido-community/dev": "0.1.6",
2122
"@caido/eslint-config": "0.8.0",
22-
"@caido/sdk-backend": "0.55.3",
23+
"@caido/sdk-backend": "0.56.0",
2324
"@caido/tailwindcss": "0.0.1",
2425
"@types/node": "25.2.1",
2526
"@vitejs/plugin-vue": "6.0.1",
@@ -34,5 +35,10 @@
3435
"typescript": "5.8.3",
3536
"vite": "6.3.5",
3637
"vitest": "3.2.3"
38+
},
39+
"pnpm": {
40+
"overrides": {
41+
"@caido/sdk-shared": "0.2.2"
42+
}
3743
}
3844
}

packages/backend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
"generate:parsers": "bash scripts/generate-parsers.sh"
1010
},
1111
"devDependencies": {
12-
"@caido/sdk-backend": "0.55.3",
13-
"shared": "workspace:*",
12+
"@caido/sdk-backend": "0.56.0",
1413
"vitest": "3.2.3"
1514
},
1615
"dependencies": {
@@ -19,6 +18,7 @@
1918
"@lezer/lr": "1.4.8",
2019
"engine": "workspace:*",
2120
"mutative": "1.3.0",
21+
"shared": "workspace:@caido-community/scanner@*",
2222
"zod": "4.3.6"
2323
}
2424
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { vi } from "vitest";
2+
3+
export const createMockSDK = () => ({
4+
console: {
5+
log: vi.fn(),
6+
error: vi.fn(),
7+
},
8+
meta: {
9+
path: vi.fn().mockReturnValue("/tmp/scanner-test"),
10+
id: vi.fn().mockReturnValue("scanner"),
11+
db: vi.fn(),
12+
assetsPath: vi.fn(),
13+
version: vi.fn().mockReturnValue("0.0.0"),
14+
updateAvailable: vi.fn(),
15+
},
16+
api: {
17+
send: vi.fn(),
18+
register: vi.fn(),
19+
},
20+
env: {
21+
getVar: vi.fn(),
22+
getVars: vi.fn().mockReturnValue([]),
23+
setVar: vi.fn().mockResolvedValue(undefined),
24+
},
25+
events: {
26+
onInterceptRequest: vi.fn(),
27+
onInterceptResponse: vi.fn(),
28+
onProjectChange: vi.fn(),
29+
onUpstream: vi.fn(),
30+
},
31+
findings: { create: vi.fn() },
32+
requests: {
33+
send: vi.fn(),
34+
get: vi.fn(),
35+
inScope: vi.fn(),
36+
},
37+
replay: {},
38+
projects: { getCurrent: vi.fn() },
39+
scope: {},
40+
runtime: {},
41+
graphql: {},
42+
hostedFile: {},
43+
net: {},
44+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { CheckMetadata } from "shared";
2+
import { describe, expect, it, vi } from "vitest";
3+
4+
import { createMockSDK } from "../__tests__/mockSdk";
5+
import { ChecksStore } from "../stores/checks";
6+
7+
import { apiGetChecks } from "./checks";
8+
9+
describe("apiGetChecks", () => {
10+
it("returns metadata from the store", () => {
11+
const metadata: CheckMetadata = {
12+
id: "demo",
13+
name: "Demo",
14+
description: "",
15+
tags: [],
16+
type: "active",
17+
aggressivity: { minRequests: 1, maxRequests: 1 },
18+
severities: ["info"],
19+
};
20+
vi.spyOn(ChecksStore, "get").mockReturnValue({
21+
select: () => [metadata],
22+
} as unknown as ChecksStore);
23+
24+
const result = apiGetChecks(createMockSDK() as never);
25+
26+
expect(result).toEqual({ kind: "Ok", value: [metadata] });
27+
});
28+
29+
it("returns Error on invalid options", () => {
30+
const result = apiGetChecks(
31+
createMockSDK() as never,
32+
{ include: "not-an-array" } as never,
33+
);
34+
35+
expect(result.kind).toBe("Error");
36+
});
37+
});

packages/backend/src/api/checks.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { SDK } from "caido:plugin";
2+
import type { CheckMetadata, GetChecksOptions, Result } from "shared";
3+
4+
import { getChecks } from "../services/checks";
5+
6+
export const apiGetChecks = (
7+
_sdk: SDK,
8+
options?: GetChecksOptions,
9+
): Result<CheckMetadata[]> => getChecks(options);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { UserConfig } from "shared";
2+
import { describe, expect, it, vi } from "vitest";
3+
4+
import { createMockSDK } from "../__tests__/mockSdk";
5+
import { ConfigStore } from "../stores/config";
6+
7+
import { apiGetUserConfig, apiUpdateUserConfig } from "./config";
8+
9+
vi.mock("caido:utils", () => ({
10+
RequestSpec: class {},
11+
}));
12+
13+
const baseConfig: UserConfig = {
14+
passive: {
15+
enabled: false,
16+
aggressivity: "medium",
17+
scopeIDs: [],
18+
concurrentTargets: 1,
19+
concurrentRequests: 1,
20+
overrides: [],
21+
severities: ["info"],
22+
},
23+
active: { overrides: [] },
24+
presets: [],
25+
};
26+
27+
describe("apiGetUserConfig", () => {
28+
it("returns the current user config", () => {
29+
vi.spyOn(ConfigStore, "get").mockReturnValue({
30+
getUserConfig: () => baseConfig,
31+
} as unknown as ConfigStore);
32+
33+
const result = apiGetUserConfig(createMockSDK() as never);
34+
35+
expect(result).toEqual({ kind: "Ok", value: baseConfig });
36+
});
37+
});
38+
39+
describe("apiUpdateUserConfig", () => {
40+
it("forwards the patch to the store", () => {
41+
const updateUserConfig = vi.fn();
42+
vi.spyOn(ConfigStore, "get").mockReturnValue({
43+
updateUserConfig,
44+
} as unknown as ConfigStore);
45+
46+
const patch: Partial<UserConfig> = {
47+
defaultPresetName: "Light",
48+
};
49+
const result = apiUpdateUserConfig(createMockSDK() as never, patch);
50+
51+
expect(result).toEqual({ kind: "Ok", value: undefined });
52+
expect(updateUserConfig).toHaveBeenCalledWith(patch);
53+
});
54+
});

0 commit comments

Comments
 (0)