├── packages ├── backend │ ├── src │ │ ├── plugin_tools │ │ │ └── index.ts │ │ ├── graphql │ │ │ └── queries │ │ │ │ ├── index.ts │ │ │ │ ├── websockets.ts │ │ │ │ ├── scopes.ts │ │ │ │ ├── filters.ts │ │ │ │ ├── findings.ts │ │ │ │ ├── replay.ts │ │ │ │ └── tampers.ts │ │ ├── models.ts │ │ ├── tool-tracking.ts │ │ ├── handlers.ts │ │ ├── tools_handlers │ │ │ ├── requests.ts │ │ │ ├── websockets.ts │ │ │ ├── scopes.ts │ │ │ ├── filters.ts │ │ │ ├── findings.ts │ │ │ └── tampers.ts │ │ ├── index.ts │ │ ├── graphql.ts │ │ ├── sessions.ts │ │ ├── database.ts │ │ └── chat.ts │ ├── tsconfig.json │ └── package.json └── frontend │ ├── src │ ├── types.ts │ ├── styles │ │ ├── index.css │ │ ├── caido.css │ │ └── primevue.css │ ├── types │ │ ├── components.ts │ │ └── index.ts │ ├── plugins │ │ └── sdk.ts │ └── index.ts │ ├── tsconfig.json │ └── package.json ├── pnpm-workspace.yaml ├── claude-mcp-server ├── src │ ├── tools.ts │ └── index.ts ├── tsconfig.json ├── package.json ├── README.md └── build │ └── index.js ├── .gitignore ├── static ├── claude-caido.png ├── claude-init.png └── claude-desktop.jpg ├── eslint.config.mjs ├── tsconfig.json ├── package.json ├── .github └── workflows │ ├── validate.yml │ └── release.yml ├── caido.config.ts └── README.md /packages/backend/src/plugin_tools/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /claude-mcp-server/src/tools.ts: -------------------------------------------------------------------------------- 1 | ../../packages/backend/src/tools.ts -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | package-lock.json 5 | pnpm-lock.yaml 6 | .env -------------------------------------------------------------------------------- /static/claude-caido.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slonser/Ebka-Caido-AI/main/static/claude-caido.png -------------------------------------------------------------------------------- /static/claude-init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slonser/Ebka-Caido-AI/main/static/claude-init.png -------------------------------------------------------------------------------- /static/claude-desktop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Slonser/Ebka-Caido-AI/main/static/claude-desktop.jpg -------------------------------------------------------------------------------- /packages/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["@caido/sdk-backend"] 5 | }, 6 | "include": ["./src/**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/frontend/src/types.ts: -------------------------------------------------------------------------------- 1 | import { type Caido } from "@caido/sdk-frontend"; 2 | import { type API } from "backend"; 3 | // @ts-ignore 4 | export type FrontendSDK = Caido>; 5 | -------------------------------------------------------------------------------- /packages/backend/src/graphql/queries/index.ts: -------------------------------------------------------------------------------- 1 | // Export all GraphQL queries and mutations 2 | export * from "./tampers"; 3 | export * from "./websockets"; 4 | export * from "./scopes"; 5 | export * from "./findings"; 6 | export * from "./filters"; 7 | export * from "./replay"; 8 | -------------------------------------------------------------------------------- /packages/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "ESNext"], 5 | "types": ["@caido/sdk-backend"], 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["src/*"] 9 | } 10 | }, 11 | "include": ["./src/**/*.ts", "./src/**/*.vue"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/frontend/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @import "./caido.css"; 2 | @import "./primevue.css"; 3 | @import "tailwindcss/base"; 4 | @import "tailwindcss/components"; 5 | @import "tailwindcss/utilities"; 6 | 7 | /* Global styles to prevent page scroll */ 8 | html, body { 9 | height: 100%; 10 | overflow: hidden; 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | #app { 16 | height: 100vh; 17 | overflow: hidden; 18 | } 19 | -------------------------------------------------------------------------------- /packages/frontend/src/types/components.ts: -------------------------------------------------------------------------------- 1 | export interface ChatMessage { 2 | id: number; 3 | role: "user" | "assistant"; 4 | content: string; 5 | timestamp: Date; 6 | } 7 | 8 | export interface ClaudeModel { 9 | id: string; 10 | name: string; 11 | description: string; 12 | } 13 | 14 | export interface ChatSession { 15 | id: number; 16 | name: string; 17 | created_at: string; 18 | updated_at: string; 19 | } 20 | -------------------------------------------------------------------------------- /claude-mcp-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } -------------------------------------------------------------------------------- /packages/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "typecheck": "vue-tsc --noEmit" 7 | }, 8 | "dependencies": { 9 | "@caido/primevue": "0.1.2", 10 | "primevue": "4.1.0", 11 | "vue": "3.4.37" 12 | }, 13 | "devDependencies": { 14 | "@caido/sdk-backend": "^0.46.0", 15 | "@caido/sdk-frontend": "^0.46.0", 16 | "@codemirror/view": "6.28.1", 17 | "backend": "workspace:*", 18 | "vue-tsc": "2.0.29" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defaultConfig } from "@caido/eslint-config"; 2 | 3 | /** @type {import('eslint').Linter.Config } */ 4 | export default [ 5 | ...defaultConfig(), 6 | { 7 | rules: { 8 | "@typescript-eslint/no-explicit-any": "off", 9 | "@typescript-eslint/ban-ts-comment": "off", 10 | "@typescript-eslint/strict-boolean-expressions": "off", 11 | "@typescript-eslint/no-restricted-types": "off", 12 | "@typescript-eslint/restrict-plus-operands": "off", 13 | "compat/compat": "off", 14 | "compat": "off" 15 | } 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["ESNext"], 6 | 7 | "jsx": "preserve", 8 | "noImplicitAny": true, 9 | "noUncheckedIndexedAccess": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "resolveJsonModule": true, 13 | 14 | "moduleResolution": "bundler", 15 | "esModuleInterop": true, 16 | "sourceMap": true, 17 | "noUnusedLocals": true, 18 | 19 | "useDefineForClassFields": true, 20 | "isolatedModules": true, 21 | 22 | "baseUrl": "." 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "types": "src/index.ts", 6 | "scripts": { 7 | "typecheck": "tsc --noEmit" 8 | }, 9 | "devDependencies": { 10 | "@caido/sdk-backend": "^0.51.1", 11 | "@types/node": "^24.2.1" 12 | }, 13 | "dependencies": { 14 | "@anthropic-ai/sdk": "^0.59.0", 15 | "@caido/quickjs-types": "^0.21.1", 16 | "@modelcontextprotocol/sdk": "^1.17.2", 17 | "@types/node-fetch": "^2.6.13", 18 | "form-data": "^4.0.4", 19 | "node-fetch": "^3.3.2", 20 | "undici": "^7.13.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ebka AI Assistant", 3 | "version": "0.1.2", 4 | "private": true, 5 | "scripts": { 6 | "typecheck": "pnpm -r typecheck", 7 | "lint": "eslint ./packages/**/src --fix", 8 | "build": "caido-dev build", 9 | "watch": "caido-dev watch" 10 | }, 11 | "devDependencies": { 12 | "@caido-community/dev": "^0.1.6", 13 | "@caido/eslint-config": "^0.5.0", 14 | "@caido/tailwindcss": "0.0.1", 15 | "@vitejs/plugin-vue": "5.2.1", 16 | "postcss-prefixwrap": "1.51.0", 17 | "tailwindcss": "3.4.13", 18 | "tailwindcss-primeui": "0.3.4", 19 | "typescript": "5.5.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /claude-mcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claude-mcp-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "bin": { 8 | "caido-mcp": "./build/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc && chmod 755 build/index.js" 12 | }, 13 | "files": ["build"], 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@modelcontextprotocol/sdk": "^1.17.2", 19 | "axios": "^1.6.0", 20 | "zod": "^3.25.76" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^24.2.1", 24 | "typescript": "^5.9.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/frontend/src/plugins/sdk.ts: -------------------------------------------------------------------------------- 1 | import { inject, type InjectionKey, type Plugin } from "vue"; 2 | 3 | import { type FrontendSDK } from "@/types"; 4 | 5 | const KEY: InjectionKey = Symbol("FrontendSDK"); 6 | 7 | // This is the plugin that will provide the FrontendSDK to VueJS 8 | // To access the frontend SDK from within a component, use the `useSDK` function. 9 | export const SDKPlugin: Plugin = (app, sdk: FrontendSDK) => { 10 | app.provide(KEY, sdk); 11 | }; 12 | 13 | // This is the function that will be used to access the FrontendSDK from within a component. 14 | export const useSDK = () => { 15 | return inject(KEY) as FrontendSDK; 16 | }; 17 | -------------------------------------------------------------------------------- /claude-mcp-server/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | Local connector for caido ebka plugin 3 | 4 | # Requirements for installation 5 | NodeJS 16+ 6 | 7 | # Installation 8 | Install dependency and build: 9 | ```bash 10 | git clone https://github.com/Slonser/Ebka-Caido-AI.git 11 | cd Ebka-Caido-AI/claude-mcp-server 12 | npm install 13 | npm run build 14 | ``` 15 | 16 | Add to claude desktop config: 17 | Linux/Macos path - `code ~/Library/Application\ Support/Claude/claude_desktop_config.json` 18 | Windows: `code $env:AppData\Claude\claude_desktop_config.json` 19 | ```json 20 | { 21 | "mcpServers": { 22 | "caido": { 23 | "command": "node", 24 | "args": ["/path/to/claude-mcp-server/build/index.js"] 25 | } 26 | } 27 | } 28 | ``` 29 | 30 | For Cursor integration you need use this path: 31 | ``` bash 32 | code ~/.cursor/mcp.json 33 | ``` -------------------------------------------------------------------------------- /packages/backend/src/graphql/queries/websockets.ts: -------------------------------------------------------------------------------- 1 | // GraphQL queries for WebSocket operations 2 | 3 | export const WEBSOCKET_STREAMS_QUERY = ` 4 | query websocketStreamsByOffset($offset: Int!, $limit: Int!, $scopeId: ID, $order: StreamOrderInput!) { 5 | streamsByOffset( 6 | offset: $offset 7 | limit: $limit 8 | scopeId: $scopeId 9 | order: $order 10 | protocol: WS 11 | ) { 12 | edges { 13 | cursor 14 | node { 15 | id 16 | createdAt 17 | direction 18 | host 19 | isTls 20 | path 21 | port 22 | protocol 23 | source 24 | } 25 | } 26 | pageInfo { 27 | hasPreviousPage 28 | hasNextPage 29 | startCursor 30 | endCursor 31 | } 32 | snapshot 33 | } 34 | } 35 | `; 36 | 37 | export const WEBSOCKET_MESSAGE_COUNT_QUERY = ` 38 | query websocketMessageCount($streamId: ID!) { 39 | streamWsMessages(first: 0, streamId: $streamId) { 40 | count { 41 | value 42 | snapshot 43 | } 44 | } 45 | } 46 | `; 47 | 48 | export const WEBSOCKET_MESSAGE_QUERY = ` 49 | query websocketMessageEdit($id: ID!) { 50 | streamWsMessageEdit(id: $id) { 51 | id 52 | length 53 | alteration 54 | direction 55 | format 56 | createdAt 57 | raw 58 | } 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /packages/frontend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Classic } from "@caido/primevue"; 2 | import PrimeVue from "primevue/config"; 3 | import { createApp } from "vue"; 4 | 5 | import { SDKPlugin } from "./plugins/sdk"; 6 | import "./styles/index.css"; 7 | import type { FrontendSDK } from "./types"; 8 | import App from "./views/App.vue"; 9 | 10 | // This is the entry point for the frontend plugin 11 | export const init = (sdk: FrontendSDK) => { 12 | const app = createApp(App); 13 | 14 | // Load the PrimeVue component library 15 | app.use(PrimeVue, { 16 | unstyled: true, 17 | pt: Classic, 18 | }); 19 | 20 | // Provide the FrontendSDK 21 | app.use(SDKPlugin, sdk); 22 | 23 | // Create the root element for the app 24 | const root = document.createElement("div"); 25 | Object.assign(root.style, { 26 | height: "100%", 27 | width: "100%", 28 | }); 29 | 30 | // Set the ID of the root element 31 | // Replace this with the value of the prefixWrap plugin in caido.config.ts 32 | // This is necessary to prevent styling conflicts between plugins 33 | root.id = `plugin--ebka-ai-assistant`; 34 | 35 | // Mount the app to the root element 36 | app.mount(root); 37 | 38 | // Add the page to the navigation 39 | // Make sure to use a unique name for the page 40 | sdk.navigation.addPage("/mcp", { 41 | body: root, 42 | }); 43 | 44 | // Add a sidebar item 45 | sdk.sidebar.registerItem("Ebka AI", "/mcp"); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/backend/src/graphql/queries/scopes.ts: -------------------------------------------------------------------------------- 1 | // GraphQL queries and mutations for scope operations 2 | 3 | // Query for listing scopes 4 | export const SCOPES_QUERY = ` 5 | query scopes { 6 | scopes { 7 | id 8 | name 9 | allowlist 10 | denylist 11 | } 12 | } 13 | `; 14 | 15 | // Mutation for creating a scope 16 | export const CREATE_SCOPE_MUTATION = ` 17 | mutation createScope($input: CreateScopeInput!) { 18 | createScope(input: $input) { 19 | error { 20 | ... on InvalidGlobTermsUserError { 21 | code 22 | terms 23 | } 24 | ... on OtherUserError { 25 | code 26 | } 27 | } 28 | scope { 29 | id 30 | name 31 | allowlist 32 | denylist 33 | } 34 | } 35 | } 36 | `; 37 | 38 | // Mutation for updating a scope 39 | export const UPDATE_SCOPE_MUTATION = ` 40 | mutation updateScope($id: ID!, $input: UpdateScopeInput!) { 41 | updateScope(id: $id, input: $input) { 42 | error { 43 | ... on InvalidGlobTermsUserError { 44 | code 45 | terms 46 | } 47 | ... on OtherUserError { 48 | code 49 | } 50 | } 51 | scope { 52 | id 53 | name 54 | allowlist 55 | denylist 56 | } 57 | } 58 | } 59 | `; 60 | 61 | // Mutation for deleting a scope 62 | export const DELETE_SCOPE_MUTATION = ` 63 | mutation deleteScope($id: ID!) { 64 | deleteScope(id: $id) { 65 | deletedId 66 | } 67 | } 68 | `; 69 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | workflow_call: 8 | 9 | concurrency: 10 | group: validate-${{ github.ref_name }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | CAIDO_NODE_VERSION: 20 15 | CAIDO_PNPM_VERSION: 9 16 | 17 | jobs: 18 | typecheck: 19 | name: 'Typecheck' 20 | runs-on: ubuntu-latest 21 | timeout-minutes: 10 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v3 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ env.CAIDO_NODE_VERSION }} 31 | 32 | - name: Setup pnpm 33 | uses: pnpm/action-setup@v3.0.0 34 | with: 35 | version: ${{ env.CAIDO_PNPM_VERSION }} 36 | 37 | - name: Install dependencies 38 | run: pnpm install 39 | 40 | - name: Run typechecker 41 | run: pnpm typecheck 42 | 43 | lint: 44 | name: 'Lint' 45 | runs-on: ubuntu-latest 46 | timeout-minutes: 10 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v4 50 | 51 | - name: Setup Node.js 52 | uses: actions/setup-node@v4 53 | with: 54 | node-version: ${{ env.CAIDO_NODE_VERSION }} 55 | 56 | - name: Setup pnpm 57 | uses: pnpm/action-setup@v4.1.0 58 | with: 59 | version: ${{ env.CAIDO_PNPM_VERSION }} 60 | 61 | - name: Install dependencies 62 | run: pnpm install 63 | 64 | - name: Run linter 65 | run: pnpm lint -------------------------------------------------------------------------------- /packages/backend/src/models.ts: -------------------------------------------------------------------------------- 1 | import Anthropic from "@anthropic-ai/sdk"; 2 | import type { SDK } from "caido:plugin"; 3 | 4 | import { getClaudeApiKey } from "./database"; 5 | 6 | export const getAvailableModels = async (sdk: SDK) => { 7 | try { 8 | const claudeApiKey = await getClaudeApiKey(sdk); 9 | 10 | if (!claudeApiKey) { 11 | return { success: false, error: "API key not set" }; 12 | } 13 | 14 | // Initialize Anthropic client 15 | const anthropic = new Anthropic({ 16 | apiKey: claudeApiKey, 17 | }); 18 | 19 | // Get available models 20 | const models = new Anthropic.Models(anthropic); 21 | const modelList = await models.list(); 22 | // Filter and format models 23 | let availableModels = modelList.data.map((model: any) => ({ 24 | id: model.id, 25 | name: model.name || model.id, 26 | description: model.description || "", 27 | })); 28 | availableModels.reverse(); 29 | // TODO: remove this slice, once 1 model starts properly working 30 | availableModels = availableModels.slice(1, availableModels.length); 31 | 32 | sdk.console.log(`Found ${availableModels.length} available Claude models`); 33 | 34 | return { 35 | success: true, 36 | models: availableModels, 37 | }; 38 | } catch (error) { 39 | sdk.console.error("Error getting models:" + error); 40 | return { 41 | success: false, 42 | error: error instanceof Error ? error.message : "Unknown error", 43 | }; 44 | } 45 | }; 46 | 47 | export const createAnthropicClient = async (sdk: SDK) => { 48 | const claudeApiKey = await getClaudeApiKey(sdk); 49 | 50 | if (!claudeApiKey) { 51 | throw new Error("Claude API key not set"); 52 | } 53 | 54 | return new Anthropic({ 55 | apiKey: claudeApiKey, 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | NODE_VERSION: 20 8 | PNPM_VERSION: 9 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | 17 | steps: 18 | - name: Checkout project 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ env.NODE_VERSION }} 25 | 26 | - name: Setup pnpm 27 | uses: pnpm/action-setup@v4 28 | with: 29 | version: ${{ env.PNPM_VERSION }} 30 | run_install: true 31 | 32 | - name: Build package 33 | run: pnpm build 34 | 35 | - name: Sign package 36 | working-directory: dist 37 | run: | 38 | if [[ -z "${{ secrets.PRIVATE_KEY }}" ]]; then 39 | echo "Set an ed25519 key as PRIVATE_KEY in GitHub Action secret to sign." 40 | else 41 | echo "${{ secrets.PRIVATE_KEY }}" > private_key.pem 42 | openssl pkeyutl -sign -inkey private_key.pem -out plugin_package.zip.sig -rawin -in plugin_package.zip 43 | rm private_key.pem 44 | fi 45 | 46 | - name: Check version 47 | id: meta 48 | working-directory: dist 49 | run: | 50 | VERSION=$(unzip -p plugin_package.zip manifest.json | jq -r .version) 51 | echo "version=${VERSION}" >> $GITHUB_OUTPUT 52 | 53 | - name: Create release 54 | uses: ncipollo/release-action@v1 55 | with: 56 | tag: ${{ steps.meta.outputs.version }} 57 | commit: ${{ github.sha }} 58 | body: 'Release ${{ steps.meta.outputs.version }}' 59 | artifacts: 'dist/plugin_package.zip,dist/plugin_package.zip.sig' 60 | -------------------------------------------------------------------------------- /packages/backend/src/tool-tracking.ts: -------------------------------------------------------------------------------- 1 | // In-memory storage for tool execution state 2 | const toolExecutionState = new Map< 3 | string, 4 | { 5 | isExecuting: boolean; 6 | toolName: string; 7 | toolInput: any; 8 | startTime: number; 9 | } 10 | >(); 11 | 12 | export const startToolExecution = ( 13 | sessionId: number, 14 | toolName: string, 15 | toolInput: any, 16 | ) => { 17 | const key = `session_${sessionId}`; 18 | const state = { 19 | isExecuting: true, 20 | toolName, 21 | toolInput, 22 | startTime: Date.now(), 23 | }; 24 | toolExecutionState.set(key, state); 25 | console.log( 26 | `[BACKEND] startToolExecution: Set state for session ${sessionId}:`, 27 | state, 28 | ); 29 | }; 30 | 31 | export const stopToolExecution = (sessionId: number) => { 32 | const key = `session_${sessionId}`; 33 | const wasExecuting = toolExecutionState.has(key); 34 | toolExecutionState.delete(key); 35 | console.log( 36 | `[BACKEND] stopToolExecution: Removed state for session ${sessionId}, was executing: ${wasExecuting}`, 37 | ); 38 | }; 39 | 40 | export const getToolExecutionState = (sessionId: number) => { 41 | const key = `session_${sessionId}`; 42 | const state = toolExecutionState.get(key) || null; 43 | console.log( 44 | `[BACKEND] getToolExecutionState called for session ${sessionId}, state:`, 45 | state, 46 | ); 47 | return state; 48 | }; 49 | 50 | export const saveToolExecutionState = ( 51 | sessionId: number, 52 | toolName: string, 53 | toolInput: any, 54 | ) => { 55 | const key = `session_${sessionId}`; 56 | const state = { 57 | isExecuting: false, 58 | toolName, 59 | toolInput, 60 | startTime: Date.now(), 61 | }; 62 | toolExecutionState.set(key, state); 63 | }; 64 | 65 | export const isToolExecuting = (sessionId: number) => { 66 | const key = `session_${sessionId}`; 67 | const state = toolExecutionState.get(key); 68 | return state ? state.isExecuting : false; 69 | }; 70 | -------------------------------------------------------------------------------- /packages/backend/src/graphql/queries/filters.ts: -------------------------------------------------------------------------------- 1 | // GraphQL queries and mutations for filter operations 2 | 3 | // Query for listing filter presets 4 | export const FILTER_PRESETS_QUERY = ` 5 | query filterPresets { 6 | filterPresets { 7 | id 8 | alias 9 | name 10 | clause 11 | } 12 | } 13 | `; 14 | 15 | // Mutation for creating a filter preset 16 | export const CREATE_FILTER_PRESET_MUTATION = ` 17 | mutation createFilterPreset($input: CreateFilterPresetInput!) { 18 | createFilterPreset(input: $input) { 19 | filter { 20 | id 21 | alias 22 | name 23 | clause 24 | } 25 | error { 26 | ... on NameTakenUserError { 27 | code 28 | name 29 | } 30 | ... on AliasTakenUserError { 31 | code 32 | alias 33 | } 34 | ... on PermissionDeniedUserError { 35 | code 36 | reason 37 | } 38 | ... on CloudUserError { 39 | code 40 | reason 41 | } 42 | ... on OtherUserError { 43 | code 44 | } 45 | } 46 | } 47 | } 48 | `; 49 | 50 | // Mutation for updating a filter preset 51 | export const UPDATE_FILTER_PRESET_MUTATION = ` 52 | mutation updateFilterPreset($id: ID!, $input: UpdateFilterPresetInput!) { 53 | updateFilterPreset(id: $id, input: $input) { 54 | filter { 55 | id 56 | alias 57 | name 58 | clause 59 | } 60 | error { 61 | ... on NameTakenUserError { 62 | code 63 | name 64 | } 65 | ... on AliasTakenUserError { 66 | code 67 | alias 68 | } 69 | ... on OtherUserError { 70 | code 71 | } 72 | } 73 | } 74 | } 75 | `; 76 | 77 | // Mutation for deleting a filter preset 78 | export const DELETE_FILTER_PRESET_MUTATION = ` 79 | mutation deleteFilterPreset($id: ID!) { 80 | deleteFilterPreset(id: $id) { 81 | deletedId 82 | } 83 | } 84 | `; 85 | -------------------------------------------------------------------------------- /packages/frontend/src/styles/caido.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Generated from https://uicolors.app/ */ 3 | /* If you need additional variants, use https://uicolors.app/ to generate them */ 4 | /* These variables are meant to be customized by Caido users */ 5 | 6 | /* Primary with #a0213e as a base color */ 7 | --c-primary-100: 357deg 78% 95%; 8 | --c-primary-200: 354deg 76% 90%; 9 | --c-primary-300: 353deg 76% 82%; 10 | --c-primary-400: 352deg 75% 71%; 11 | --c-primary-500: 350deg 71% 60%; 12 | --c-primary-600: 348deg 61% 50%; 13 | --c-primary-700: 346deg 66% 38%; 14 | --c-primary-800: 345deg 64% 35%; 15 | --c-primary-900: 342deg 60% 30%; 16 | 17 | /* Secondary with #daa04a as a base color */ 18 | --c-secondary-100: 41deg 65% 89%; 19 | --c-secondary-200: 40deg 66% 77%; 20 | --c-secondary-300: 38deg 66% 65%; 21 | --c-secondary-400: 36deg 66% 57%; 22 | --c-secondary-500: 30deg 63% 50%; 23 | --c-secondary-600: 25deg 65% 44%; 24 | --c-secondary-700: 19deg 62% 37%; 25 | --c-secondary-800: 15deg 56% 31%; 26 | --c-secondary-900: 14deg 53% 26%; 27 | 28 | /* Danger with #f58e97 as a base color */ 29 | --c-danger-100: 0deg 85% 95%; 30 | --c-danger-200: 356deg 84% 90%; 31 | --c-danger-300: 356deg 85% 82%; 32 | --c-danger-400: 355deg 84% 76%; 33 | --c-danger-500: 353deg 79% 60%; 34 | --c-danger-600: 350deg 69% 50%; 35 | --c-danger-700: 349deg 73% 41%; 36 | --c-danger-800: 347deg 71% 35%; 37 | --c-danger-900: 345deg 66% 30%; 38 | 39 | /* Info with #88a2aa as a base color */ 40 | --c-info-100: 193deg 18% 90%; 41 | --c-info-200: 194deg 18% 82%; 42 | --c-info-300: 193deg 18% 69%; 43 | --c-info-400: 194deg 17% 60%; 44 | --c-info-500: 195deg 18% 43%; 45 | --c-info-600: 198deg 18% 36%; 46 | --c-info-700: 201deg 16% 31%; 47 | --c-info-800: 200deg 13% 27%; 48 | --c-info-900: hsl(204, 12%, 24%); 49 | 50 | /* Success with #579c57 as a base color */ 51 | --c-success-100: 120deg 32% 93%; 52 | --c-success-200: 118deg 32% 85%; 53 | --c-success-300: 120deg 31% 73%; 54 | --c-success-400: 120deg 28% 58%; 55 | --c-success-500: 120deg 28% 48%; 56 | --c-success-600: 120deg 31% 36%; 57 | --c-success-700: 120deg 29% 29%; 58 | --c-success-800: 122deg 25% 24%; 59 | --c-success-900: 122deg 24% 20%; 60 | 61 | /* Surface */ 62 | /* This is not generated from https://uicolors.app/ */ 63 | --c-surface-0: 24deg 12% 92%; 64 | --c-surface-200: 0deg 0% 78%; 65 | --c-surface-300: 0deg 0% 67%; 66 | --c-surface-400: 0deg 0% 57%; 67 | --c-surface-500: 0deg 0% 47%; 68 | --c-surface-600: 0deg 0% 38%; 69 | --c-surface-700: 224deg 9% 31%; 70 | --c-surface-800: 224deg 10% 21%; 71 | --c-surface-900: 225deg 10% 16%; 72 | } -------------------------------------------------------------------------------- /packages/backend/src/handlers.ts: -------------------------------------------------------------------------------- 1 | import { tools_version } from "./tools"; 2 | import { 3 | create_filter_preset, 4 | delete_filter_preset, 5 | list_filter_presets, 6 | update_filter_preset, 7 | } from "./tools_handlers/filters"; 8 | import { 9 | create_findings_from_requests, 10 | delete_findings, 11 | get_finding_by_id, 12 | list_findings, 13 | update_finding, 14 | } from "./tools_handlers/findings"; 15 | import { 16 | create_replay_collection, 17 | graphql_collection_requests, 18 | graphql_list_collections, 19 | list_replay_collections, 20 | list_replay_connections, 21 | move_replay_session, 22 | rename_replay_collection, 23 | rename_replay_session, 24 | send_to_replay, 25 | start_replay_task, 26 | } from "./tools_handlers/replay"; 27 | import { 28 | list_by_httpql, 29 | sendRequest, 30 | view_request_by_id, 31 | view_response_by_id, 32 | } from "./tools_handlers/requests"; 33 | import { 34 | create_scope, 35 | delete_scope, 36 | list_scopes, 37 | update_scope, 38 | } from "./tools_handlers/scopes"; 39 | import { 40 | create_tamper_rule, 41 | create_tamper_rule_collection, 42 | list_tamper_rule_collections, 43 | list_tamper_rules, 44 | read_tamper_rule, 45 | update_tamper_rule, 46 | } from "./tools_handlers/tampers"; 47 | import { 48 | get_websocket_message, 49 | get_websocket_message_count, 50 | list_websocket_streams, 51 | } from "./tools_handlers/websockets"; 52 | 53 | // Handler for getting tools version 54 | const get_tools_version = () => { 55 | return { 56 | success: true, 57 | version: tools_version, 58 | timestamp: new Date().toISOString(), 59 | summary: `Backend tools version: ${tools_version}`, 60 | }; 61 | }; 62 | 63 | export const handlers = { 64 | list_by_httpql, 65 | view_request_by_id, 66 | view_response_by_id, 67 | sendRequest, 68 | send_to_replay, 69 | list_replay_collections, 70 | rename_replay_collection, 71 | rename_replay_session, 72 | graphql_collection_requests, 73 | graphql_list_collections, 74 | list_replay_connections, 75 | move_replay_session, 76 | start_replay_task, 77 | create_replay_collection, 78 | create_tamper_rule_collection, 79 | create_tamper_rule, 80 | update_tamper_rule, 81 | list_tamper_rule_collections, 82 | list_tamper_rules, 83 | read_tamper_rule, 84 | list_findings, 85 | get_finding_by_id, 86 | update_finding, 87 | delete_findings, 88 | create_findings_from_requests, 89 | list_websocket_streams, 90 | get_websocket_message_count, 91 | get_websocket_message, 92 | list_filter_presets, 93 | create_filter_preset, 94 | update_filter_preset, 95 | delete_filter_preset, 96 | list_scopes, 97 | create_scope, 98 | update_scope, 99 | delete_scope, 100 | get_tools_version, 101 | }; 102 | -------------------------------------------------------------------------------- /caido.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@caido-community/dev'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import tailwindcss from "tailwindcss"; 4 | // @ts-expect-error no declared types at this time 5 | import tailwindPrimeui from "tailwindcss-primeui"; 6 | import tailwindCaido from "@caido/tailwindcss"; 7 | import path from "path"; 8 | import prefixwrap from "postcss-prefixwrap"; 9 | 10 | const id = "ebka-ai-assistant"; 11 | export default defineConfig({ 12 | id, 13 | name: "Ebka AI Assistant", 14 | description: "Integrates with Claude AI to provide AI-powered security testing capabilities", 15 | version: "0.1.2", 16 | author: { 17 | name: "Slonser", 18 | email: "slonser@neplox.security", 19 | }, 20 | plugins: [ 21 | { 22 | kind: "backend", 23 | id: "ebka-backend", 24 | root: "packages/backend", 25 | }, 26 | { 27 | kind: 'frontend', 28 | id: "ebka-frontend", 29 | root: 'packages/frontend', 30 | backend: { 31 | id: "ebka-backend", 32 | }, 33 | vite: { 34 | plugins: [vue()], 35 | build: { 36 | rollupOptions: { 37 | external: [ 38 | '@caido/frontend-sdk', 39 | "@codemirror/state", 40 | "@codemirror/view", 41 | "@codemirror/autocomplete", 42 | "@codemirror/commands", 43 | "@codemirror/lint", 44 | "@codemirror/search", 45 | "@codemirror/language", 46 | "@lezer/common", 47 | "@lezer/highlight", 48 | "@lezer/lr" 49 | ] 50 | } 51 | }, 52 | resolve: { 53 | alias: [ 54 | { 55 | find: "@", 56 | replacement: path.resolve(__dirname, "packages/frontend/src"), 57 | }, 58 | ], 59 | }, 60 | css: { 61 | postcss: { 62 | plugins: [ 63 | // This plugin wraps the root element in a unique ID 64 | // This is necessary to prevent styling conflicts between plugins 65 | prefixwrap(`#plugin--${id}`), 66 | 67 | tailwindcss({ 68 | corePlugins: { 69 | preflight: false, 70 | }, 71 | content: [ 72 | './packages/frontend/src/**/*.{vue,ts}', 73 | './node_modules/@caido/primevue/dist/primevue.mjs' 74 | ], 75 | // Check the [data-mode="dark"] attribute on the element to determine the mode 76 | // This attribute is set in the Caido core application 77 | darkMode: ["selector", '[data-mode="dark"]'], 78 | plugins: [ 79 | 80 | // This plugin injects the necessary Tailwind classes for PrimeVue components 81 | tailwindPrimeui, 82 | 83 | // This plugin injects the necessary Tailwind classes for the Caido theme 84 | tailwindCaido, 85 | ], 86 | }) 87 | ] 88 | } 89 | } 90 | } 91 | } 92 | ] 93 | }); -------------------------------------------------------------------------------- /packages/frontend/src/styles/primevue.css: -------------------------------------------------------------------------------- 1 | /* Primary and Surface Palettes */ 2 | :root { 3 | --p-primary-50: hsl(var(--c-primary-100)); 4 | --p-primary-100: hsl(var(--c-primary-100)); 5 | --p-primary-200: hsl(var(--c-primary-200)); 6 | --p-primary-300: hsl(var(--c-primary-300)); 7 | --p-primary-400: hsl(var(--c-primary-400)); 8 | --p-primary-500: hsl(var(--c-primary-500)); 9 | --p-primary-600: hsl(var(--c-primary-600)); 10 | --p-primary-700: hsl(var(--c-primary-700)); 11 | --p-primary-800: hsl(var(--c-primary-800)); 12 | --p-primary-900: hsl(var(--c-primary-900)); 13 | --p-primary-950: hsl(var(--c-primary-900)); 14 | --p-surface-0: hsl(var(--c-surface-0)); 15 | --p-surface-50: #f8fafc; 16 | --p-surface-100: #f1f5f9; 17 | --p-surface-200: hsl(var(--c-surface-200)); 18 | --p-surface-300: hsl(var(--c-surface-300)); 19 | --p-surface-400: hsl(var(--c-surface-400)); 20 | --p-surface-500: hsl(var(--c-surface-500)); 21 | --p-surface-600: hsl(var(--c-surface-600)); 22 | --p-surface-700: hsl(var(--c-surface-700)); 23 | --p-surface-800: hsl(var(--c-surface-800)); 24 | --p-surface-900: hsl(var(--c-surface-900)); 25 | --p-surface-950: hsl(var(--c-surface-900)); 26 | --p-content-border-radius: 6px; 27 | } 28 | 29 | /* Light Mode */ 30 | :root { 31 | --p-primary-color: var(--p-primary-500); 32 | --p-primary-contrast-color: var(--p-surface-0); 33 | --p-primary-hover-color: var(--p-primary-600); 34 | --p-primary-active-color: var(--p-primary-700); 35 | --p-content-border-color: var(--p-surface-200); 36 | --p-content-hover-background: var(--p-surface-100); 37 | --p-content-hover-color: var(--p-surface-800); 38 | --p-highlight-background: var(--p-primary-50); 39 | --p-highlight-color: var(--p-primary-700); 40 | --p-highlight-focus-background: var(--p-primary-100); 41 | --p-highlight-focus-color: var(--p-primary-800); 42 | --p-text-color: var(--p-surface-700); 43 | --p-text-hover-color: var(--p-surface-800); 44 | --p-text-muted-color: var(--p-surface-500); 45 | --p-text-hover-muted-color: var(--p-surface-600); 46 | } 47 | 48 | /* 49 | * Dark Mode 50 | * Change the .p-dark to match the darkMode in tailwind.config. 51 | * For example; 52 | * darkMode: ['selector', '[class*="app-dark"]'] 53 | * should match; 54 | * :root.app-dark 55 | */ 56 | :root[data-mode=dark] { 57 | --p-primary-color: var(--p-primary-400); 58 | --p-primary-contrast-color: var(--p-surface-900); 59 | --p-primary-hover-color: hsl(var(--c-secondary-300)); 60 | --p-primary-active-color: hsl(var(--c-secondary-200)); 61 | --p-content-border-color: var(--p-surface-700); 62 | --p-content-hover-background: var(--p-surface-800); 63 | --p-content-hover-color: var(--p-surface-0); 64 | --p-highlight-background: color-mix(in srgb, hsl(var(--c-secondary-400)), transparent 84%); 65 | --p-highlight-color: rgba(255,255,255,.87); 66 | --p-highlight-focus-background: color-mix(in srgb, hsl(var(--c-secondary-400)), transparent 76%); 67 | --p-highlight-focus-color: rgba(255,255,255,.87); 68 | --p-text-color: var(--p-surface-0); 69 | --p-text-hover-color: var(--p-surface-0); 70 | --p-text-muted-color: var(--p-surface-400); 71 | --p-text-hover-muted-color: var(--p-surface-300); 72 | } -------------------------------------------------------------------------------- /packages/backend/src/graphql/queries/findings.ts: -------------------------------------------------------------------------------- 1 | // GraphQL queries and mutations for findings operations 2 | 3 | // Mutation for updating a finding 4 | export const UPDATE_FINDING_MUTATION = ` 5 | mutation updateFinding($id: ID!, $input: UpdateFindingInput!) { 6 | updateFinding(id: $id, input: $input) { 7 | finding { 8 | id 9 | title 10 | description 11 | reporter 12 | host 13 | path 14 | createdAt 15 | request { 16 | id 17 | host 18 | port 19 | path 20 | query 21 | method 22 | edited 23 | isTls 24 | sni 25 | length 26 | alteration 27 | fileExtension 28 | source 29 | createdAt 30 | metadata { 31 | id 32 | color 33 | } 34 | response { 35 | id 36 | statusCode 37 | roundtripTime 38 | length 39 | createdAt 40 | alteration 41 | edited 42 | } 43 | stream { 44 | id 45 | } 46 | } 47 | } 48 | error { 49 | ... on OtherUserError { 50 | code 51 | } 52 | ... on UnknownIdUserError { 53 | code 54 | id 55 | } 56 | } 57 | } 58 | } 59 | `; 60 | 61 | // Mutation for deleting findings 62 | export const DELETE_FINDINGS_MUTATION = ` 63 | mutation deleteFindings($input: DeleteFindingsInput!) { 64 | deleteFindings(input: $input) { 65 | deletedIds 66 | } 67 | } 68 | `; 69 | 70 | // Query for listing findings with pagination 71 | export const FINDINGS_BY_OFFSET_QUERY = ` 72 | query getFindingsByOffset($offset: Int!, $limit: Int!, $filter: FilterClauseFindingInput!, $order: FindingOrderInput!) { 73 | findingsByOffset(offset: $offset, limit: $limit, filter: $filter, order: $order) { 74 | edges { 75 | cursor 76 | node { 77 | id 78 | title 79 | description 80 | reporter 81 | host 82 | path 83 | createdAt 84 | request { 85 | id 86 | host 87 | port 88 | path 89 | query 90 | method 91 | isTls 92 | length 93 | source 94 | createdAt 95 | response { 96 | id 97 | statusCode 98 | roundtripTime 99 | length 100 | createdAt 101 | } 102 | } 103 | } 104 | } 105 | pageInfo { 106 | hasPreviousPage 107 | hasNextPage 108 | startCursor 109 | endCursor 110 | } 111 | snapshot 112 | } 113 | } 114 | `; 115 | 116 | // Query for getting a finding by ID 117 | export const GET_FINDING_BY_ID_QUERY = ` 118 | query getFindingById($id: ID!) { 119 | finding(id: $id) { 120 | id 121 | title 122 | description 123 | reporter 124 | host 125 | path 126 | createdAt 127 | request { 128 | id 129 | host 130 | port 131 | path 132 | query 133 | method 134 | edited 135 | isTls 136 | sni 137 | length 138 | alteration 139 | fileExtension 140 | source 141 | createdAt 142 | raw 143 | metadata { 144 | id 145 | color 146 | } 147 | response { 148 | id 149 | statusCode 150 | roundtripTime 151 | length 152 | createdAt 153 | alteration 154 | edited 155 | raw 156 | } 157 | stream { 158 | id 159 | } 160 | } 161 | } 162 | } 163 | `; 164 | -------------------------------------------------------------------------------- /packages/backend/src/tools_handlers/requests.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | import { RequestSpec } from "caido:utils"; 3 | 4 | export const list_by_httpql = async (sdk: SDK, input: any) => { 5 | const query = sdk.requests.query().filter(input.httpql); 6 | const result = await query.execute(); 7 | 8 | return { 9 | count: result.items.length, 10 | id_list: result.items.map((item: any) => item.request.getId()), 11 | summary: `Found ${result.items.length} requests matching the query: [${result.items 12 | .map((item: any) => { 13 | return JSON.stringify({ 14 | id: item.request.getId(), 15 | method: item.request.getMethod(), 16 | host: item.request.getHost(), 17 | path: item.request.getPath(), 18 | }); 19 | }) 20 | .join(", ")}]`, 21 | }; 22 | }; 23 | 24 | export const view_request_by_id = async (sdk: SDK, input: any) => { 25 | const request = await sdk.requests.get(input.id); 26 | if (!request) { 27 | return { 28 | error: "No request found with id: " + input.id, 29 | summary: "Request not found", 30 | }; 31 | } 32 | 33 | return { 34 | request: "Request ID: " + request.request.getId(), 35 | summary: `Request details for ID: ${request.request.getRaw().toText()}`, 36 | }; 37 | }; 38 | 39 | export const view_response_by_id = async (sdk: SDK, input: any) => { 40 | const response = await sdk.requests.get(input.id); 41 | if (!response) { 42 | return { 43 | error: "No request found with id: " + input.id, 44 | summary: "Response not found", 45 | }; 46 | } 47 | if (!response.response) { 48 | return { 49 | error: "No response found for request with id: " + input.id, 50 | summary: "Response not found", 51 | }; 52 | } 53 | 54 | return { 55 | response: "Response ID: " + response.response.getId(), 56 | summary: `Response details for ID: ${response.response.getRaw().toText()}`, 57 | }; 58 | }; 59 | 60 | export const sendRequest = async (sdk: SDK, input: any) => { 61 | const request = new RequestSpec(input.url); 62 | if (input.raw_request) { 63 | request.setRaw(input.raw_request); 64 | } else { 65 | if (input.method) { 66 | request.setMethod(input.method); 67 | } 68 | if (input.headers) { 69 | for (const header in input.headers) { 70 | request.setHeader(header, input.headers[header]); 71 | } 72 | } 73 | if (input.body) { 74 | request.setBody(input.body); 75 | } 76 | if (input.method) { 77 | request.setMethod(input.method); 78 | } 79 | if (input.query) { 80 | request.setQuery(input.query); 81 | } 82 | if (input.host) { 83 | request.setHost(input.host); 84 | } 85 | if (input.port) { 86 | request.setPort(input.port); 87 | } 88 | if (input.tls) { 89 | request.setTls(input.tls); 90 | } 91 | if (input.path) { 92 | request.setPath(input.path); 93 | } 94 | if (input.query) { 95 | request.setQuery(input.query); 96 | } 97 | if (input.method) { 98 | request.setMethod(input.method); 99 | } 100 | if (input.body) { 101 | request.setBody(input.body); 102 | } 103 | if (input.port) { 104 | request.setPort(input.port); 105 | } 106 | if (input.tls) { 107 | request.setTls(input.tls); 108 | } 109 | if (input.path) { 110 | request.setPath(input.path); 111 | } 112 | } 113 | try { 114 | const response = await sdk.requests.send(request); 115 | return { 116 | success: true, 117 | response: response, 118 | summary: 119 | "Request sent successfully, Response: " + 120 | response.response.getRaw().toText(), 121 | }; 122 | } catch (error) { 123 | sdk.console.error("Error sending request:", error); 124 | return { 125 | success: false, 126 | error: `Failed to send request: ${error}`, 127 | details: error instanceof Error ? error.message : String(error), 128 | summary: "Failed to send request due to unexpected error.", 129 | }; 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /packages/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Blob, fetch, Headers, Request, Response } from "caido:http"; 2 | import type { DefineAPI, SDK } from "caido:plugin"; 3 | 4 | // Import functions from modules 5 | import { sendMessage } from "./chat"; 6 | import { 7 | getProgramResult, 8 | initializeDatabase, 9 | sendAuthToken, 10 | setClaudeApiKey, 11 | } from "./database"; 12 | import { handlers } from "./handlers"; 13 | import { getAvailableModels } from "./models"; 14 | import { 15 | createSession, 16 | deleteSession, 17 | getSessionMessages, 18 | getSessions, 19 | renameSession, 20 | } from "./sessions"; 21 | import { getToolExecutionState } from "./tool-tracking"; 22 | 23 | // @ts-ignore 24 | globalThis.fetch = fetch; 25 | // @ts-ignore 26 | globalThis.Headers = Headers; 27 | // @ts-ignore 28 | globalThis.Request = Request; 29 | // @ts-ignore 30 | globalThis.Response = Response; 31 | // @ts-ignore 32 | globalThis.Blob = Blob; 33 | 34 | // AI API requires FormData, but doesn't use it 35 | // @ts-ignore 36 | globalThis.FormData = class trash { 37 | constructor(...args: any) { 38 | return {}; 39 | } 40 | }; 41 | 42 | export type API = DefineAPI<{ 43 | setClaudeApiKey: ( 44 | apiKey: string, 45 | ) => Promise<{ success: boolean; message?: string }>; 46 | getClaudeApiKey: () => Promise; 47 | sendMessage: ( 48 | message: string, 49 | selectedModel?: string, 50 | sessionId?: number, 51 | ) => Promise; 52 | getAvailableModels: () => Promise<{ id: string; name: string }[]>; 53 | createSession: ( 54 | name: string, 55 | ) => Promise<{ success: boolean; sessionId?: number; message?: string }>; 56 | getSessions: () => Promise< 57 | { id: number; name: string; created_at: string; updated_at: string }[] 58 | >; 59 | getSessionMessages: ( 60 | sessionId: number, 61 | ) => Promise<{ role: string; content: string; timestamp: string }[]>; 62 | renameSession: ( 63 | sessionId: number, 64 | newName: string, 65 | ) => Promise<{ success: boolean; message?: string }>; 66 | deleteSession: ( 67 | sessionId: number, 68 | ) => Promise<{ success: boolean; message?: string }>; 69 | getConversationHistory: ( 70 | sessionId: number, 71 | ) => Promise<{ role: string; content: string; timestamp: string }[]>; 72 | getProgramResult: (resultId: number) => Promise; 73 | getToolExecutionState: (sessionId: number) => any; 74 | sendAuthToken: ( 75 | accessToken: string, 76 | apiEndpoint?: string, 77 | ) => Promise<{ success: boolean; message?: string }>; 78 | claudeDesktop: (toolName: string, args: string) => any; 79 | }>; 80 | 81 | export type Events = { 82 | "request-auth-token": { source: string; timestamp: number; message: string }; 83 | "auth-token-saved": { success: boolean; timestamp: number; message: string }; 84 | }; 85 | 86 | export function init(sdk: SDK) { 87 | // Initialize database when plugin starts 88 | initializeDatabase(sdk); 89 | 90 | // Trigger request-auth-token event to get auth token from frontend 91 | setTimeout(() => { 92 | try { 93 | sdk.console.log("🔐 Triggering request-auth-token event..."); 94 | sdk.api.send("request-auth-token", { 95 | source: "backend", 96 | timestamp: Date.now(), 97 | message: "Requesting auth token from frontend", 98 | }); 99 | } catch (error) { 100 | sdk.console.error("❌ Error triggering request-auth-token event:", error); 101 | } 102 | }, 1000); // Delay 1 second to ensure frontend is ready 103 | 104 | sdk.api.register("setClaudeApiKey", setClaudeApiKey); 105 | sdk.api.register("sendMessage", sendMessage); 106 | sdk.api.register("getAvailableModels", getAvailableModels); 107 | sdk.api.register("createSession", createSession); 108 | sdk.api.register("getSessions", getSessions); 109 | sdk.api.register("getSessionMessages", getSessionMessages); 110 | sdk.api.register("renameSession", renameSession); 111 | sdk.api.register("deleteSession", deleteSession); 112 | sdk.api.register("getProgramResult", getProgramResult); 113 | sdk.api.register("getToolExecutionState", (sdk: any, sessionId: number) => 114 | getToolExecutionState(sessionId), 115 | ); 116 | sdk.api.register( 117 | "sendAuthToken", 118 | (sdk: any, accessToken: string, apiEndpoint?: string) => 119 | sendAuthToken(sdk, accessToken, apiEndpoint), 120 | ); 121 | sdk.api.register("claudeDesktop", desktopIntegration); 122 | } 123 | 124 | async function desktopIntegration( 125 | sdk: SDK, 126 | toolName: string, 127 | input: string, 128 | ) { 129 | if (!Object.hasOwn(handlers, toolName.replace("caido_", ""))) { 130 | return `Handler for tool ${toolName} not found`; 131 | } 132 | return await handlers[ 133 | toolName.replace("caido_", "") as keyof typeof handlers 134 | ](sdk, JSON.parse(input)); 135 | } 136 | -------------------------------------------------------------------------------- /packages/backend/src/graphql.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | 3 | // Interface for GraphQL query options 4 | export interface GraphQLOptions { 5 | query: string; 6 | variables?: Record; 7 | operationName?: string; 8 | apiEndpoint?: string; 9 | } 10 | 11 | // Interface for GraphQL response 12 | export interface GraphQLResponse { 13 | data?: T; 14 | errors?: Array<{ 15 | message: string; 16 | locations?: Array<{ line: number; column: number }>; 17 | path?: string[]; 18 | }>; 19 | } 20 | 21 | /** 22 | * Get the saved Caido auth token from database 23 | */ 24 | export const getCaidoAuthToken = async (sdk: SDK): Promise => { 25 | try { 26 | const db = await sdk.meta.db(); 27 | const stmt = await db.prepare( 28 | "SELECT key_value FROM api_keys WHERE key_name = ?", 29 | ); 30 | const result = await stmt.get("caido-auth-token"); 31 | 32 | if (!result || typeof result !== "object" || !("key_value" in result)) { 33 | sdk.console.log("No Caido auth token found in database"); 34 | return null; 35 | } 36 | 37 | const token = (result as any).key_value; 38 | sdk.console.log(`Found Caido auth token: ${token.substring(0, 8)}...`); 39 | return token; 40 | } catch (error) { 41 | sdk.console.error("Error getting Caido auth token:", error); 42 | return null; 43 | } 44 | }; 45 | 46 | /** 47 | * Get the saved Caido API endpoint from database 48 | */ 49 | export const getCaidoApiEndpoint = async (sdk: SDK): Promise => { 50 | try { 51 | const db = await sdk.meta.db(); 52 | const stmt = await db.prepare( 53 | "SELECT key_value FROM api_keys WHERE key_name = ?", 54 | ); 55 | const result = await stmt.get("caido-api-endpoint"); 56 | 57 | if (!result || typeof result !== "object" || !("key_value" in result)) { 58 | sdk.console.log("No Caido API endpoint found in database, using default"); 59 | return null; 60 | } 61 | 62 | const endpoint = (result as any).key_value; 63 | sdk.console.log(`Found Caido API endpoint: ${endpoint}`); 64 | return endpoint; 65 | } catch (error) { 66 | sdk.console.error("Error getting Caido API endpoint:", error); 67 | return null; 68 | } 69 | }; 70 | 71 | // TODO: delete this function 72 | /** 73 | * Execute a GraphQL query using the saved auth token 74 | */ 75 | export const executeGraphQLQuery = async ( 76 | sdk: SDK, 77 | options: GraphQLOptions, 78 | ): Promise<{ 79 | success: boolean; 80 | data?: T; 81 | error?: string; 82 | response?: GraphQLResponse; 83 | }> => { 84 | try { 85 | const { query, variables = {}, operationName } = options; 86 | let apiEndpoint = options.apiEndpoint; 87 | // Get the auth token 88 | const authToken = await getCaidoAuthToken(sdk); 89 | if (!authToken) { 90 | return { 91 | success: false, 92 | error: 93 | "No Caido auth token found. Please set the auth token first using request-auth-token event.", 94 | }; 95 | } 96 | 97 | // If no API endpoint provided, try to get the saved one, otherwise use default 98 | if (!apiEndpoint) { 99 | const savedEndpoint = await getCaidoApiEndpoint(sdk); 100 | apiEndpoint = savedEndpoint || "http://localhost:8080/graphql"; 101 | } 102 | 103 | sdk.console.log(`Executing GraphQL query to ${apiEndpoint}...`); 104 | sdk.console.log("Query:", query.substring(0, 100) + "..."); 105 | sdk.console.log("Variables:", JSON.stringify(variables, null, 2)); 106 | 107 | // Execute the GraphQL query 108 | const response = await fetch(apiEndpoint, { 109 | method: "POST", 110 | headers: { 111 | "Content-Type": "application/json", 112 | Authorization: `Bearer ${authToken}`, 113 | Accept: 114 | "application/graphql-response+json, application/graphql+json, application/json", 115 | }, 116 | body: JSON.stringify({ 117 | operationName, 118 | query, 119 | variables, 120 | }), 121 | }); 122 | 123 | if (!response.ok) { 124 | const errorText = await response.text(); 125 | throw new Error( 126 | `HTTP error! status: ${response.status}, body: ${errorText}`, 127 | ); 128 | } 129 | 130 | const responseData = (await response.json()) as GraphQLResponse; 131 | sdk.console.log("GraphQL response received successfully"); 132 | 133 | // Check for GraphQL errors 134 | if (responseData.errors && responseData.errors.length > 0) { 135 | const errorMessages = responseData.errors 136 | .map((e) => e.message) 137 | .join("; "); 138 | return { 139 | success: false, 140 | error: `GraphQL errors: ${errorMessages}`, 141 | response: responseData, 142 | }; 143 | } 144 | 145 | return { 146 | success: true, 147 | data: responseData.data, 148 | response: responseData, 149 | }; 150 | } catch (error) { 151 | const errorMessage = 152 | error instanceof Error ? error.message : "Unknown error"; 153 | sdk.console.error(`Error executing GraphQL query: ${errorMessage}`); 154 | 155 | return { 156 | success: false, 157 | error: `Failed to execute GraphQL query: ${errorMessage}`, 158 | }; 159 | } 160 | }; 161 | 162 | export const executeGraphQLQueryviaSDK = async ( 163 | sdk: SDK, 164 | options: GraphQLOptions, 165 | ): Promise => { 166 | const { query, variables = {} } = options; 167 | // @ts-ignore 168 | const result = await sdk.graphql.execute(query, variables); 169 | if (result.errors) { 170 | return { 171 | success: false, 172 | error: result.errors, 173 | }; 174 | } 175 | return { 176 | success: true, 177 | data: result.data, 178 | }; 179 | }; 180 | -------------------------------------------------------------------------------- /packages/backend/src/sessions.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | 3 | export const createSession = async (sdk: SDK, name: string) => { 4 | try { 5 | const db = await sdk.meta.db(); 6 | const stmt = await db.prepare(` 7 | INSERT INTO chat_sessions (name) VALUES (?) 8 | `); 9 | const result = await stmt.run(name); 10 | 11 | sdk.console.log( 12 | `New session created: ${name} with ID ${result.lastInsertRowid}`, 13 | ); 14 | 15 | // Trigger request-auth-token event after creating session 16 | try { 17 | sdk.console.log( 18 | "🔐 Triggering request-auth-token event after session creation...", 19 | ); 20 | // @ts-ignore - We know this method exists 21 | sdk.api.send("request-auth-token", { 22 | source: "createSession", 23 | timestamp: Date.now(), 24 | message: "Requesting auth token after session creation", 25 | }); 26 | } catch (eventError) { 27 | sdk.console.log("Note: Could not trigger request-auth-token event"); 28 | } 29 | 30 | return { 31 | success: true, 32 | sessionId: result.lastInsertRowid, 33 | message: "Session created successfully", 34 | }; 35 | } catch (error) { 36 | sdk.console.error("Error creating session:" + error); 37 | return { 38 | success: false, 39 | error: error instanceof Error ? error.message : "Unknown error", 40 | }; 41 | } 42 | }; 43 | 44 | export const getSessions = async (sdk: SDK) => { 45 | try { 46 | const db = await sdk.meta.db(); 47 | const stmt = await db.prepare(` 48 | SELECT id, name, created_at, updated_at FROM chat_sessions 49 | ORDER BY updated_at DESC 50 | `); 51 | const sessions = await stmt.all(); 52 | 53 | sdk.console.log(`Retrieved ${sessions.length} sessions`); 54 | 55 | return { 56 | success: true, 57 | sessions: sessions, 58 | }; 59 | } catch (error) { 60 | sdk.console.error("Error getting sessions:" + error); 61 | return { 62 | success: false, 63 | error: error instanceof Error ? error.message : "Unknown error", 64 | }; 65 | } 66 | }; 67 | 68 | export const getSessionMessages = async (sdk: SDK, sessionId: number) => { 69 | try { 70 | const db = await sdk.meta.db(); 71 | const stmt = await db.prepare(` 72 | SELECT id, role, content, timestamp FROM chat_messages 73 | WHERE session_id = ? 74 | ORDER BY timestamp ASC 75 | `); 76 | const messages = await stmt.all(sessionId); 77 | 78 | sdk.console.log( 79 | `Retrieved ${messages.length} messages for session ${sessionId}`, 80 | ); 81 | 82 | return { 83 | success: true, 84 | messages: messages, 85 | }; 86 | } catch (error) { 87 | sdk.console.error("Error getting session messages:" + error); 88 | return { 89 | success: false, 90 | error: error instanceof Error ? error.message : "Unknown error", 91 | }; 92 | } 93 | }; 94 | 95 | export const renameSession = async ( 96 | sdk: SDK, 97 | sessionId: number, 98 | newName: string, 99 | ) => { 100 | try { 101 | const db = await sdk.meta.db(); 102 | const stmt = await db.prepare(` 103 | UPDATE chat_sessions SET name = ? WHERE id = ? 104 | `); 105 | await stmt.run(newName, sessionId); 106 | 107 | sdk.console.log(`Session ${sessionId} renamed to: ${newName}`); 108 | 109 | return { 110 | success: true, 111 | message: "Session renamed successfully", 112 | }; 113 | } catch (error) { 114 | sdk.console.error("Error renaming session:" + error); 115 | return { 116 | success: false, 117 | error: error instanceof Error ? error.message : "Unknown error", 118 | }; 119 | } 120 | }; 121 | 122 | export const deleteSession = async (sdk: SDK, sessionId: number) => { 123 | try { 124 | const db = await sdk.meta.db(); 125 | 126 | // Delete messages first (due to foreign key constraint) 127 | const deleteMessagesStmt = await db.prepare(` 128 | DELETE FROM chat_messages WHERE session_id = ? 129 | `); 130 | await deleteMessagesStmt.run(sessionId); 131 | 132 | // Delete session 133 | const deleteSessionStmt = await db.prepare(` 134 | DELETE FROM chat_sessions WHERE id = ? 135 | `); 136 | await deleteSessionStmt.run(sessionId); 137 | 138 | sdk.console.log(`Session ${sessionId} deleted successfully`); 139 | 140 | return { 141 | success: true, 142 | message: "Session deleted successfully", 143 | }; 144 | } catch (error) { 145 | sdk.console.error("Error deleting session:" + error); 146 | return { 147 | success: false, 148 | error: error instanceof Error ? error.message : "Unknown error", 149 | }; 150 | } 151 | }; 152 | 153 | export const saveMessage = async ( 154 | sdk: SDK, 155 | sessionId: number, 156 | role: string, 157 | content: string, 158 | ) => { 159 | try { 160 | const db = await sdk.meta.db(); 161 | const stmt = await db.prepare(` 162 | INSERT INTO chat_messages (session_id, role, content) VALUES (?, ?, ?) 163 | `); 164 | await stmt.run(sessionId, role, content); 165 | 166 | // Update session updated_at timestamp 167 | const updateSessionStmt = await db.prepare(` 168 | UPDATE chat_sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = ? 169 | `); 170 | await updateSessionStmt.run(sessionId); 171 | } catch (error) { 172 | sdk.console.error("Error saving message:", error); 173 | throw error; 174 | } 175 | }; 176 | 177 | export const getConversationHistory = async (sdk: SDK, sessionId: number) => { 178 | try { 179 | const db = await sdk.meta.db(); 180 | const historyStmt = await db.prepare(` 181 | SELECT role, content FROM chat_messages 182 | WHERE session_id = ? 183 | ORDER BY timestamp ASC 184 | `); 185 | const history = await historyStmt.all(sessionId); 186 | 187 | return history.map((msg: any) => ({ 188 | role: msg.role, 189 | content: msg.content, 190 | })); 191 | } catch (error) { 192 | sdk.console.error("Error getting conversation history:", error); 193 | throw error; 194 | } 195 | }; 196 | -------------------------------------------------------------------------------- /packages/frontend/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ChatMessage { 2 | id: number; 3 | role: "user" | "assistant"; 4 | content: string; 5 | timestamp: Date; 6 | } 7 | 8 | export interface ClaudeModel { 9 | id: string; 10 | name: string; 11 | description: string; 12 | } 13 | 14 | export interface ChatSession { 15 | id: number; 16 | name: string; 17 | created_at: string; 18 | updated_at: string; 19 | } 20 | 21 | export interface FrontendSDK { 22 | backend: BackendSDK; 23 | replay: ReplaySDK; 24 | requests: RequestsSDK; 25 | navigation: NavigationSDK; 26 | sidebar: SidebarSDK; 27 | ui: UISDK; 28 | window: WindowSDK; 29 | } 30 | 31 | export interface BackendSDK { 32 | setClaudeApiKey: ( 33 | apiKey: string, 34 | ) => Promise<{ success: boolean; message?: string }>; 35 | getClaudeApiKey: () => Promise; 36 | sendMessage: ( 37 | message: string, 38 | selectedModel?: string, 39 | sessionId?: number, 40 | ) => Promise; 41 | getAvailableModels: () => Promise<{ id: string; name: string }[]>; 42 | createSession: ( 43 | name: string, 44 | ) => Promise<{ success: boolean; sessionId?: number; message?: string }>; 45 | getSessions: () => Promise< 46 | { id: number; name: string; created_at: string; updated_at: string }[] 47 | >; 48 | getSessionMessages: ( 49 | sessionId: number, 50 | ) => Promise<{ role: string; content: string; timestamp: string }[]>; 51 | renameSession: ( 52 | sessionId: number, 53 | newName: string, 54 | ) => Promise<{ success: boolean; message?: string }>; 55 | deleteSession: ( 56 | sessionId: number, 57 | ) => Promise<{ success: boolean; message?: string }>; 58 | getConversationHistory: ( 59 | sessionId: number, 60 | ) => Promise<{ role: string; content: string; timestamp: string }[]>; 61 | getProgramResult: (resultId: number) => Promise; 62 | getToolExecutionState: (sessionId: number) => any; 63 | sendAuthToken: ( 64 | accessToken: string, 65 | apiEndpoint?: string, 66 | ) => Promise<{ success: boolean; message?: string }>; 67 | claudeDesktop: (toolName: string, args: any) => any; 68 | } 69 | 70 | export interface ReplaySDK { 71 | addToSlot: (slot: string, content: any) => void; 72 | openTab: (options?: any) => any; 73 | closeTab: () => void; 74 | getActiveTab: () => any; 75 | getTabs: () => any[]; 76 | setActiveTab: (tab: any) => void; 77 | setTabTitle: (tab: any, title: string) => void; 78 | setTabContent: (tab: any, content: HTMLElement) => void; 79 | setTabToolbar: (tab: any, toolbar: HTMLElement) => void; 80 | setTabSidebar: (tab: any, sidebar: HTMLElement) => void; 81 | setTabStatus: (tab: any, status: string) => void; 82 | setTabProgress: (tab: any, progress: number) => void; 83 | setTabError: (tab: any, error: string) => void; 84 | setTabWarning: (tab: any, warning: string) => void; 85 | setTabInfo: (tab: any, info: string) => void; 86 | setTabSuccess: (tab: any, success: string) => void; 87 | setTabLoading: (tab: any, loading: boolean) => void; 88 | setTabDisabled: (tab: any, disabled: boolean) => void; 89 | setTabHidden: (tab: any, hidden: boolean) => void; 90 | setTabClosable: (tab: any, closable: boolean) => void; 91 | setTabMovable: (tab: any, movable: boolean) => void; 92 | setTabResizable: (tab: any, resizable: boolean) => void; 93 | setTabMaximizable: (tab: any, maximizable: boolean) => void; 94 | setTabMinimizable: (tab: any, minimizable: boolean) => void; 95 | setTabRestorable: (tab: any, restorable: boolean) => void; 96 | setTabFullscreen: (tab: any, fullscreen: boolean) => void; 97 | setTabPinned: (tab: any, pinned: boolean) => void; 98 | setTabGroup: (tab: any, group: string) => void; 99 | setTabOrder: (tab: any, order: number) => void; 100 | setTabPosition: (tab: any, position: { x: number; y: number }) => void; 101 | setTabSize: (tab: any, size: { width: number; height: number }) => void; 102 | setTabBounds: ( 103 | tab: any, 104 | bounds: { x: number; y: number; width: number; height: number }, 105 | ) => void; 106 | setTabZIndex: (tab: any, zIndex: number) => void; 107 | setTabOpacity: (tab: any, opacity: number) => void; 108 | setTabVisible: (tab: any, visible: boolean) => void; 109 | setTabFocusable: (tab: any, focusable: boolean) => void; 110 | setTabSelectable: (tab: any, selectable: boolean) => void; 111 | setTabEditable: (tab: any, editable: boolean) => void; 112 | setTabReadOnly: (tab: any, readOnly: boolean) => void; 113 | setTabRequired: (tab: any, required: boolean) => void; 114 | setTabValid: (tab: any, valid: boolean) => void; 115 | setTabInvalid: (tab: any, invalid: boolean) => void; 116 | setTabDirty: (tab: any, dirty: boolean) => void; 117 | setTabPristine: (tab: any, pristine: boolean) => void; 118 | setTabTouched: (tab: any, touched: boolean) => void; 119 | setTabUntouched: (tab: any, untouched: boolean) => void; 120 | } 121 | 122 | export interface RequestsSDK { 123 | query: () => any; 124 | get: (id: string) => Promise; 125 | create: (request: any) => Promise; 126 | update: (id: string, request: any) => Promise; 127 | delete: (id: string) => Promise; 128 | } 129 | 130 | export interface NavigationSDK { 131 | addPage: (path: string, options: any) => void; 132 | goTo: (path: string) => void; 133 | } 134 | 135 | export interface SidebarSDK { 136 | registerItem: (name: string, path: string, options?: any) => any; 137 | } 138 | 139 | export interface UISDK { 140 | button: (options?: any) => HTMLElement; 141 | card: (options?: any) => HTMLElement; 142 | well: (options?: any) => HTMLElement; 143 | showDialog: (component: any, options?: any) => any; 144 | showToast: (message: string, options?: any) => void; 145 | } 146 | 147 | export interface WindowSDK { 148 | getActiveEditor: () => any; 149 | } 150 | 151 | export interface FrontendTool { 152 | name: string; 153 | description: string; 154 | inputSchema: any; 155 | execute: (sdk: FrontendSDK, input: any) => Promise; 156 | } 157 | 158 | export interface FrontendToolResult { 159 | success: boolean; 160 | data?: any; 161 | error?: string; 162 | summary: string; 163 | } 164 | -------------------------------------------------------------------------------- /packages/backend/src/graphql/queries/replay.ts: -------------------------------------------------------------------------------- 1 | // GraphQL queries and mutations for replay operations 2 | 3 | // Mutation for renaming a replay session 4 | export const RENAME_REPLAY_SESSION_MUTATION = ` 5 | mutation renameReplaySession($id: ID!, $name: String!) { 6 | renameReplaySession(id: $id, name: $name) { 7 | session { 8 | id 9 | name 10 | } 11 | } 12 | } 13 | `; 14 | 15 | // Mutation for creating a replay session collection 16 | export const CREATE_REPLAY_SESSION_COLLECTION_MUTATION = ` 17 | mutation createReplaySessionCollection($input: CreateReplaySessionCollectionInput!) { 18 | createReplaySessionCollection(input: $input) { 19 | collection { 20 | id 21 | name 22 | } 23 | } 24 | } 25 | `; 26 | 27 | // Mutation for renaming a replay session collection 28 | export const RENAME_REPLAY_SESSION_COLLECTION_MUTATION = ` 29 | mutation renameReplaySessionCollection($id: ID!, $name: String!) { 30 | renameReplaySessionCollection(id: $id, name: $name) { 31 | collection { 32 | ...replaySessionCollectionMeta 33 | } 34 | } 35 | } 36 | `; 37 | 38 | // GraphQL fragments for replay operations 39 | export const REPLAY_FRAGMENTS = { 40 | connectionInfoFull: ` 41 | fragment connectionInfoFull on ConnectionInfo { 42 | __typename 43 | host 44 | port 45 | isTLS 46 | SNI 47 | } 48 | `, 49 | requestMetadataFull: ` 50 | fragment requestMetadataFull on RequestMetadata { 51 | __typename 52 | id 53 | color 54 | } 55 | `, 56 | responseMeta: ` 57 | fragment responseMeta on Response { 58 | __typename 59 | id 60 | statusCode 61 | roundtripTime 62 | length 63 | createdAt 64 | alteration 65 | edited 66 | } 67 | `, 68 | requestMeta: ` 69 | fragment requestMeta on Request { 70 | __typename 71 | id 72 | host 73 | port 74 | path 75 | query 76 | method 77 | edited 78 | isTls 79 | sni 80 | length 81 | alteration 82 | metadata { 83 | ...requestMetadataFull 84 | } 85 | fileExtension 86 | source 87 | createdAt 88 | response { 89 | ...responseMeta 90 | } 91 | stream { 92 | id 93 | } 94 | } 95 | `, 96 | replayEntryMeta: ` 97 | fragment replayEntryMeta on ReplayEntry { 98 | __typename 99 | id 100 | error 101 | connection { 102 | ...connectionInfoFull 103 | } 104 | session { 105 | id 106 | } 107 | request { 108 | ...requestMeta 109 | } 110 | } 111 | `, 112 | replaySessionMeta: ` 113 | fragment replaySessionMeta on ReplaySession { 114 | __typename 115 | id 116 | name 117 | activeEntry { 118 | ...replayEntryMeta 119 | } 120 | collection { 121 | id 122 | } 123 | entries { 124 | nodes { 125 | ...replayEntryMeta 126 | } 127 | } 128 | } 129 | `, 130 | replaySessionCollectionMeta: ` 131 | fragment replaySessionCollectionMeta on ReplaySessionCollection { 132 | __typename 133 | id 134 | name 135 | sessions { 136 | ...replaySessionMeta 137 | } 138 | } 139 | `, 140 | }; 141 | 142 | // Function to get all replay fragments 143 | export const getReplayFragments = () => { 144 | return Object.values(REPLAY_FRAGMENTS).join("\n"); 145 | }; 146 | 147 | // Function to get fragments needed for move replay session 148 | export const getMoveReplaySessionFragments = () => { 149 | return [ 150 | REPLAY_FRAGMENTS.connectionInfoFull, 151 | REPLAY_FRAGMENTS.requestMetadataFull, 152 | REPLAY_FRAGMENTS.responseMeta, 153 | REPLAY_FRAGMENTS.requestMeta, 154 | REPLAY_FRAGMENTS.replayEntryMeta, 155 | REPLAY_FRAGMENTS.replaySessionMeta, 156 | ].join("\n"); 157 | }; 158 | 159 | // Function to get fragments needed for start replay task 160 | export const getStartReplayTaskFragments = () => { 161 | return [ 162 | REPLAY_FRAGMENTS.connectionInfoFull, 163 | REPLAY_FRAGMENTS.requestMetadataFull, 164 | REPLAY_FRAGMENTS.responseMeta, 165 | REPLAY_FRAGMENTS.requestMeta, 166 | REPLAY_FRAGMENTS.replayEntryMeta, 167 | ].join("\n"); 168 | }; 169 | 170 | // Function to get fragments needed for rename replay session collection 171 | export const getRenameReplaySessionCollectionFragments = () => { 172 | return Object.values(REPLAY_FRAGMENTS).join("\n"); 173 | }; 174 | 175 | // Mutation for moving a replay session to a different collection 176 | export const MOVE_REPLAY_SESSION_MUTATION = ` 177 | mutation moveReplaySession($id: ID!, $collectionId: ID!) { 178 | moveReplaySession(collectionId: $collectionId, id: $id) { 179 | session { 180 | ...replaySessionMeta 181 | } 182 | } 183 | } 184 | `; 185 | 186 | // Mutation for starting a replay task 187 | export const START_REPLAY_TASK_MUTATION = ` 188 | mutation startReplayTask($sessionId: ID!, $input: StartReplayTaskInput!) { 189 | startReplayTask(sessionId: $sessionId, input: $input) { 190 | task { 191 | id 192 | createdAt 193 | replayEntry { 194 | ...replayEntryMeta 195 | settings { 196 | placeholders { 197 | inputRange { 198 | start 199 | end 200 | } 201 | outputRange { 202 | start 203 | end 204 | } 205 | preprocessors { 206 | options { 207 | ... on ReplayPrefixPreprocessor { 208 | value 209 | } 210 | ... on ReplaySuffixPreprocessor { 211 | value 212 | } 213 | ... on ReplayUrlEncodePreprocessor { 214 | charset 215 | nonAscii 216 | } 217 | ... on ReplayWorkflowPreprocessor { 218 | id 219 | } 220 | ... on ReplayEnvironmentPreprocessor { 221 | variableName 222 | } 223 | } 224 | } 225 | } 226 | } 227 | request { 228 | ...requestMeta 229 | raw 230 | } 231 | } 232 | } 233 | error { 234 | ... on TaskInProgressUserError { 235 | code 236 | taskId 237 | } 238 | ... on PermissionDeniedUserError { 239 | code 240 | reason 241 | } 242 | ... on CloudUserError { 243 | code 244 | reason 245 | } 246 | ... on OtherUserError { 247 | code 248 | } 249 | } 250 | } 251 | } 252 | `; 253 | 254 | // Query for getting replay session collections 255 | export const getDefaultReplayCollectionsQuery = () => ` 256 | query replaySessionCollections { 257 | replaySessionCollections { 258 | edges { 259 | node { 260 | ...replaySessionCollectionMeta 261 | } 262 | } 263 | } 264 | } 265 | 266 | ${REPLAY_FRAGMENTS.connectionInfoFull} 267 | ${REPLAY_FRAGMENTS.requestMetadataFull} 268 | ${REPLAY_FRAGMENTS.responseMeta} 269 | ${REPLAY_FRAGMENTS.requestMeta} 270 | ${REPLAY_FRAGMENTS.replayEntryMeta} 271 | ${REPLAY_FRAGMENTS.replaySessionMeta} 272 | ${REPLAY_FRAGMENTS.replaySessionCollectionMeta} 273 | `; 274 | -------------------------------------------------------------------------------- /packages/backend/src/tools_handlers/websockets.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | 3 | import { executeGraphQLQueryviaSDK } from "../graphql"; 4 | import { 5 | WEBSOCKET_MESSAGE_COUNT_QUERY, 6 | WEBSOCKET_MESSAGE_QUERY, 7 | WEBSOCKET_STREAMS_QUERY, 8 | } from "../graphql/queries"; 9 | 10 | export const list_websocket_streams = async (sdk: SDK, input: any) => { 11 | try { 12 | const limit = input.limit || 50; 13 | const offset = input.offset || 0; 14 | const scopeId = input.scope_id; 15 | const order = input.order || { by: "ID", ordering: "DESC" }; 16 | 17 | // Use imported GraphQL query for WebSocket streams 18 | const query = WEBSOCKET_STREAMS_QUERY; 19 | 20 | const variables = { 21 | limit: limit, 22 | offset: offset, 23 | scopeId: scopeId, 24 | order: order, 25 | }; 26 | 27 | const result = await executeGraphQLQueryviaSDK(sdk, { 28 | query, 29 | variables, 30 | operationName: "websocketStreamsByOffset", 31 | }); 32 | 33 | if (!result.success || !result.data) { 34 | return { 35 | success: false, 36 | error: result.error || "Failed to fetch WebSocket streams", 37 | summary: "Failed to retrieve WebSocket streams from Caido", 38 | }; 39 | } 40 | 41 | const streams = result.data.streamsByOffset.edges.map( 42 | (edge: any) => edge.node, 43 | ); 44 | const pageInfo = result.data.streamsByOffset.pageInfo; 45 | 46 | const streamsSummary = streams 47 | .map( 48 | (stream: any) => 49 | `ID: ${stream.id} | Host: ${stream.host}:${stream.port} | Path: ${stream.path} | Direction: ${stream.direction} | TLS: ${stream.isTls ? "Yes" : "No"} | Created: ${stream.createdAt}`, 50 | ) 51 | .join("\n"); 52 | 53 | return { 54 | success: true, 55 | streams: streams, 56 | count: streams.length, 57 | pageInfo: pageInfo, 58 | summary: `Retrieved ${streams.length} WebSocket streams (offset: ${offset}, limit: ${limit}):\n\n${streamsSummary}`, 59 | totalAvailable: pageInfo.hasNextPage ? "More available" : "All retrieved", 60 | }; 61 | } catch (error) { 62 | sdk.console.error("Error listing WebSocket streams:", error); 63 | return { 64 | success: false, 65 | error: `Failed to list WebSocket streams: ${error}`, 66 | details: error instanceof Error ? error.message : String(error), 67 | summary: "Failed to retrieve WebSocket streams due to unexpected error", 68 | }; 69 | } 70 | }; 71 | 72 | export const get_websocket_message_count = async (sdk: SDK, input: any) => { 73 | try { 74 | const streamId = input.stream_id; 75 | 76 | if (!streamId) { 77 | return { 78 | success: false, 79 | error: "Stream ID is required", 80 | summary: "Please provide a stream ID to get message count", 81 | }; 82 | } 83 | 84 | // Use imported GraphQL query for WebSocket message count 85 | const query = WEBSOCKET_MESSAGE_COUNT_QUERY; 86 | 87 | const variables = { 88 | streamId: streamId, 89 | }; 90 | 91 | const result = await executeGraphQLQueryviaSDK(sdk, { 92 | query, 93 | variables, 94 | operationName: "websocketMessageCount", 95 | }); 96 | 97 | if (!result.success || !result.data) { 98 | return { 99 | success: false, 100 | error: result.error || "Failed to get WebSocket message count", 101 | summary: `Failed to get message count for stream: ${streamId}`, 102 | }; 103 | } 104 | 105 | const countData = result.data.streamWsMessages.count; 106 | 107 | if (!countData) { 108 | return { 109 | success: false, 110 | error: "No count data returned", 111 | summary: `No count data returned for stream: ${streamId}`, 112 | }; 113 | } 114 | 115 | const summary = `WebSocket Stream ${streamId} Message Count: 116 | Total Messages: ${countData.value} 117 | Snapshot: ${countData.snapshot}`; 118 | 119 | return { 120 | success: true, 121 | streamId: streamId, 122 | count: countData.value, 123 | snapshot: countData.snapshot, 124 | summary: summary, 125 | message: `Successfully retrieved message count for stream ${streamId}`, 126 | }; 127 | } catch (error) { 128 | sdk.console.error("Error getting WebSocket message count:", error); 129 | return { 130 | success: false, 131 | error: `Failed to get WebSocket message count: ${error}`, 132 | details: error instanceof Error ? error.message : String(error), 133 | summary: "Failed to get WebSocket message count due to unexpected error", 134 | }; 135 | } 136 | }; 137 | 138 | export const get_websocket_message = async (sdk: SDK, input: any) => { 139 | try { 140 | const messageId = input.id; 141 | 142 | if (!messageId) { 143 | return { 144 | success: false, 145 | error: "Message ID is required", 146 | summary: "Please provide a message ID to retrieve", 147 | }; 148 | } 149 | 150 | // Use imported GraphQL query for WebSocket message details 151 | const query = WEBSOCKET_MESSAGE_QUERY; 152 | 153 | const variables = { 154 | id: messageId, 155 | }; 156 | 157 | const result = await executeGraphQLQueryviaSDK(sdk, { 158 | query, 159 | variables, 160 | operationName: "websocketMessageEdit", 161 | }); 162 | 163 | if (!result.success || !result.data) { 164 | return { 165 | success: false, 166 | error: result.error || "Failed to fetch WebSocket message", 167 | summary: `Failed to retrieve WebSocket message with ID: ${messageId}`, 168 | }; 169 | } 170 | 171 | const message = result.data.streamWsMessageEdit; 172 | 173 | if (!message) { 174 | return { 175 | success: false, 176 | error: "Message not found", 177 | summary: `No WebSocket message found with ID: ${messageId}`, 178 | }; 179 | } 180 | 181 | let decodedRaw = "N/A"; 182 | let rawPreview = "N/A"; 183 | 184 | if (message.raw) { 185 | try { 186 | decodedRaw = Buffer.from(message.raw, "base64").toString("utf8"); 187 | rawPreview = decodedRaw; 188 | } catch (decodeError) { 189 | decodedRaw = "Failed to decode base64 data"; 190 | rawPreview = "Decoding failed"; 191 | } 192 | } 193 | 194 | const messageSummary = `WebSocket Message Details: 195 | ID: ${message.id} 196 | Length: ${message.length} bytes 197 | Direction: ${message.direction} 198 | Format: ${message.format} 199 | Alteration: ${message.alteration} 200 | Created: ${message.createdAt} 201 | Raw Data (decoded): ${rawPreview}`; 202 | 203 | return { 204 | success: true, 205 | message: message, 206 | summary: messageSummary, 207 | details: { 208 | id: message.id, 209 | length: message.length, 210 | direction: message.direction, 211 | format: message.format, 212 | alteration: message.alteration, 213 | createdAt: message.createdAt, 214 | raw: message.raw, 215 | raw_decoded: decodedRaw, 216 | }, 217 | }; 218 | } catch (error) { 219 | sdk.console.error("Error getting WebSocket message:", error); 220 | return { 221 | success: false, 222 | error: `Failed to get WebSocket message: ${error}`, 223 | details: error instanceof Error ? error.message : String(error), 224 | summary: "Failed to retrieve WebSocket message due to unexpected error", 225 | }; 226 | } 227 | }; 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ebka AI 2 | 3 |
4 | 5 | _A powerful AI-powered assistant for Caido web application security testing, built with Claude AI_ 6 | 7 | [![GitHub forks](https://img.shields.io/github/forks/Slonser/Ebka-Caido-AI?style=social)](https://github.com/Slonser/Ebka-Caido-AI/network/members) 8 | [![GitHub issues](https://img.shields.io/github/issues/Slonser/Ebka-Caido-AI)](https://github.com/Slonser/Ebka-Caido-AI/issues) 9 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/Slonser/Ebka-Caido-AI)](https://github.com/Slonser/Ebka-Caido-AI/releases) 10 | [![GitHub stars](https://img.shields.io/github/stars/Slonser/Ebka-Caido-AI?style=social)](https://github.com/Slonser/Ebka-Caido-AI/stargazers) 11 | [![License](https://img.shields.io/github/license/Slonser/Ebka-Caido-AI?branch=main)](https://github.com/Slonser/Ebka-Caido-AI/blob/main/LICENSE) 12 | 13 | [Report Bug](https://github.com/Slonser/Ebka-Caido-AI/issues) • 14 | [Request Feature](https://github.com/Slonser/Ebka-Caido-AI/issues) 15 | 16 | ![Claude Desktop Integration](./static/claude-desktop.jpg) 17 | *Claude Desktop Integration* 18 | 19 |
20 | 21 | --- 22 | 23 | - [Ebka AI](#ebka-ai) 24 | - [Overview](#overview) 25 | - [Features](#features) 26 | - [**Request Analysis \& Search**](#request-analysis--search) 27 | - [**Replay Session Management**](#replay-session-management) 28 | - [**Security Findings Management**](#security-findings-management) 29 | - [**Match/Replace Rules**](#matchreplace-rules) 30 | - [**Filters**](#filters) 31 | - [**Scopes**](#scopes) 32 | - [**AI-Powered Intelligence**](#ai-powered-intelligence) 33 | - [Prerequisites](#prerequisites) 34 | - [Getting Started](#getting-started) 35 | - [Method 1 - Claude Desktop (Extension Required)](#method-1---claude-desktop-extension-required) 36 | - [Method 2 - Direct API Access (Requires API Key)](#method-2---direct-api-access-requires-api-key) 37 | - [Installation](#installation) 38 | - [Prerequisites](#prerequisites-1) 39 | - [Install from source:](#install-from-source) 40 | - [For Claude Desktop Users](#for-claude-desktop-users) 41 | - [Usage](#usage) 42 | - [Contributing](#contributing) 43 | - [License](#license) 44 | 45 | ## Overview 46 | 47 | Ebka AI is an AI-powered assistant that integrates seamlessly with Caido, providing intelligent security testing capabilities through natural language commands and automated workflows. Built with Claude AI, it offers advanced HTTPQL query search, match/replace operations, replay session management, and AI-powered security analysis. 48 | 49 | --- 50 | 51 | ## Features 52 | 53 | Ebka AI provides **30+ powerful Claude tools** for Caido: 54 | 55 | ### **Request Analysis & Search** 56 | - **HTTPQL Query Search**: Advanced filtering and analysis using HTTPQL syntax 57 | - **Request/Response Viewing**: Inspect individual requests and responses by ID 58 | - **Custom Request Sending**: Send HTTP requests with full control over headers, body, and parameters 59 | - **Wesocket stream managment**: Read and analyze websocket streams 60 | 61 | ### **Replay Session Management** 62 | - **Replay Collections**: Create, list, and manage replay session collections 63 | - **Session Operations**: Rename sessions, move between collections, and execute automated testing 64 | - **Connection Management**: Monitor and analyze replay connections and requests 65 | 66 | ### **Security Findings Management** 67 | - **Findings CRUD**: Create, read, update, and delete security findings 68 | - **Advanced Filtering**: List findings with pagination, filtering, and sorting 69 | - **Comprehensive Data**: Access detailed finding information including request/response bodies 70 | 71 | ### **Match/Replace Rules** 72 | - **Tamper Rule Collections**: Organize and manage rule collections 73 | - **Rule Management**: Create, update, and manage sophisticated find/replace operations 74 | - **Advanced Filtering**: Search and filter rules by collection and criteria 75 | 76 | ### **Filters** 77 | - **Create and update Filters** 78 | 79 | ### **Scopes** 80 | - **Full scope management**: CRUD scopes, use created scopes in HTTPQL 81 | 82 | ### **AI-Powered Intelligence** 83 | - **Claude Integration**: Leverage Claude AI for intelligent security insights 84 | - **Natural Language**: Interact with security tools using natural language commands 85 | - **Automated Workflows**: Streamline security testing with AI-assisted automation 86 | 87 | ## Prerequisites 88 | 89 | - [Caido](https://caido.io/) web application security testing platform 90 | - For Direct Usage: Claude API key from [Anthropic Console](https://console.anthropic.com/settings/keys) 91 | 92 | --- 93 | 94 | ## Getting Started 95 | 96 | There are two ways to interact with the Caido AI Assistant: 97 | 98 | ### Method 1 - Claude Desktop (Extension Required) 99 | 100 | **Prerequisites:** 101 | - [Node.js](https://nodejs.org/) (version 16 or higher) 102 | - npm or pnpm package manager 103 | 104 | **Setup Steps:** 105 | 1. **Install dependencies:** 106 | ```bash 107 | cd claude-mcp-server 108 | npm install 109 | # or if using pnpm: 110 | # pnpm install 111 | ``` 112 | 113 | 2. **Build the MCP server:** 114 | ```bash 115 | npm run build 116 | # or if using pnpm: 117 | # pnpm build 118 | ``` 119 | 120 | 3. **Add to claude_desktop_config**: 121 | ```json 122 | { 123 | "mcpServers": { 124 | "caido": { 125 | "command": "node", 126 | "args": ["/path/to/claude-mcp-server/build/index.js"] 127 | } 128 | } 129 | } 130 | ``` 131 | **Note:** Replace `/path/to/` with the actual path to your project directory 132 | 133 | 4. **Click "Copy MCP Request"** in the Caido plugin tab 134 | 5. **Paste the request** in Claude to set the accessKey and API URL 135 | 6. **Congratulations!** You can now communicate with Caido through Claude 136 | 137 | ![Claude Desktop Integration](./static/claude-init.png) 138 | *Claude Desktop Integration* 139 | 140 | ### Method 2 - Direct API Access (Requires API Key) 141 | 142 | 1. **Enter your API KEY** in the plugin tab 143 | 2. **Use the functionality directly** from Caido without Claude Desktop 144 | 145 | ![Direct Caido Integration](./static/claude-caido.png) 146 | *Direct Caido Integration* 147 | 148 | --- 149 | 150 | ## Installation 151 | 152 | ### Prerequisites 153 | 154 | - [Caido](https://caido.io/) (latest version) 155 | - [Node.js](https://nodejs.org/) (version 16 or higher) 156 | - npm or pnpm package manager 157 | 158 | ### Install from source: 159 | 160 | 1. **Clone the repository:** 161 | ```bash 162 | git clone https://github.com/Slonser/Ebka-Caido-AI.git 163 | cd Ebka-Caido-AI 164 | ``` 165 | 166 | 2. **Install dependencies:** 167 | ```bash 168 | pnpm install 169 | # or if using npm: 170 | # npm install 171 | ``` 172 | 173 | 3. **Build the project:** 174 | ```bash 175 | pnpm build 176 | # or if using npm: 177 | # npm run build 178 | ``` 179 | 180 | 4. **Install in Caido:** 181 | - Open Caido 182 | - Go to Settings > Plugins 183 | - Click "Install from file" 184 | - Select the built plugin file from the appropriate directory 185 | 186 | ### For Claude Desktop Users 187 | 188 | If you're using Claude Desktop, you'll also need to build the MCP server: 189 | 190 | ```bash 191 | cd claude-mcp-server 192 | npm install 193 | npm run build 194 | ``` 195 | 196 | Then update your `claude_desktop_config` with the correct path to the built server. 197 | 198 | --- 199 | 200 | ## Usage 201 | 202 | 1. **Access Ebka AI:** 203 | - After installation, find "Ebka AI" in your Caido sidebar 204 | - Click to open the AI assistant interface 205 | 206 | 2. **Configure your settings:** 207 | - Enter your Claude API key for direct usage 208 | - Configure Claude Desktop integration if using the extension 209 | - Set up your preferred security testing workflows 210 | 211 | 3. **Use AI-powered features:** 212 | - Ask natural language questions about your security testing 213 | - Use HTTPQL queries to search through requests 214 | - Create and manage match/replace rules 215 | - Execute replay sessions and collections 216 | - Generate security findings with AI assistance 217 | 218 | --- 219 | 220 | ## Contributing 221 | 222 | 1. Fork the repository 223 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 224 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 225 | 4. Push to the branch (`git push origin feature/amazing-feature`) 226 | 5. Open a Pull Request 227 | 228 | --- 229 | 230 | ## License 231 | 232 | This project is licensed under the GPL-3.0 License - see the [LICENSE](LICENSE) file for details. 233 | 234 | --- 235 | 236 |
237 | Made with ❤️ for the Caido community and security researchers 238 |
239 | 240 | 241 | -------------------------------------------------------------------------------- /packages/backend/src/database.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | 3 | export const initializeDatabase = async (sdk: SDK) => { 4 | try { 5 | sdk.console.log("Starting database initialization..."); 6 | 7 | const db = await sdk.meta.db(); 8 | sdk.console.log("Database connection obtained"); 9 | 10 | // Create api_keys table 11 | const createApiKeysTableStmt = await db.prepare(` 12 | CREATE TABLE IF NOT EXISTS api_keys ( 13 | key_name TEXT PRIMARY KEY, 14 | key_value TEXT NOT NULL 15 | ) 16 | `); 17 | sdk.console.log("CREATE TABLE statement prepared for api_keys"); 18 | 19 | await createApiKeysTableStmt.run(); 20 | sdk.console.log("Database table api_keys created/verified successfully"); 21 | 22 | // Create sessions table 23 | const createSessionsTableStmt = await db.prepare(` 24 | CREATE TABLE IF NOT EXISTS chat_sessions ( 25 | id INTEGER PRIMARY KEY AUTOINCREMENT, 26 | name TEXT NOT NULL, 27 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 28 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 29 | ) 30 | `); 31 | sdk.console.log("CREATE TABLE statement prepared for chat_sessions"); 32 | 33 | await createSessionsTableStmt.run(); 34 | sdk.console.log( 35 | "Database table chat_sessions created/verified successfully", 36 | ); 37 | 38 | // Create messages table 39 | const createMessagesTableStmt = await db.prepare(` 40 | CREATE TABLE IF NOT EXISTS chat_messages ( 41 | id INTEGER PRIMARY KEY AUTOINCREMENT, 42 | session_id INTEGER NOT NULL, 43 | role TEXT NOT NULL, 44 | content TEXT NOT NULL, 45 | timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, 46 | FOREIGN KEY (session_id) REFERENCES chat_sessions (id) ON DELETE CASCADE 47 | ) 48 | `); 49 | sdk.console.log("CREATE TABLE statement prepared for chat_messages"); 50 | 51 | await createMessagesTableStmt.run(); 52 | sdk.console.log( 53 | "Database table chat_messages created/verified successfully", 54 | ); 55 | 56 | // Create program_results table 57 | const createProgramResultsTableStmt = await db.prepare(` 58 | CREATE TABLE IF NOT EXISTS program_results ( 59 | id INTEGER PRIMARY KEY AUTOINCREMENT, 60 | session_id INTEGER NOT NULL, 61 | tool_name TEXT NOT NULL, 62 | input_data TEXT NOT NULL, 63 | result_data TEXT NOT NULL, 64 | summary TEXT, 65 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 66 | FOREIGN KEY (session_id) REFERENCES chat_sessions (id) ON DELETE CASCADE 67 | ) 68 | `); 69 | sdk.console.log("CREATE TABLE statement prepared for program_results"); 70 | 71 | await createProgramResultsTableStmt.run(); 72 | sdk.console.log( 73 | "Database table program_results created/verified successfully", 74 | ); 75 | 76 | // Create default session if none exists 77 | const checkSessionsStmt = await db.prepare( 78 | "SELECT COUNT(*) as count FROM chat_sessions", 79 | ); 80 | const sessionCount = await checkSessionsStmt.get(); 81 | 82 | if (sessionCount && (sessionCount as any).count === 0) { 83 | const createDefaultSessionStmt = await db.prepare(` 84 | INSERT INTO chat_sessions (name) VALUES ('New Chat') 85 | `); 86 | await createDefaultSessionStmt.run(); 87 | sdk.console.log("Default session created"); 88 | } 89 | } catch (error) { 90 | sdk.console.error("Failed to create database tables:" + error); 91 | } 92 | }; 93 | 94 | export const setClaudeApiKey = async (sdk: SDK, apiKey: string) => { 95 | try { 96 | sdk.console.log("Starting to set Claude API key..."); 97 | 98 | const db = await sdk.meta.db(); 99 | 100 | sdk.console.log("Database connection established"); 101 | 102 | const stmt = await db.prepare( 103 | `INSERT OR REPLACE INTO api_keys (key_name, key_value) VALUES (?, ?)`, 104 | ); 105 | sdk.console.log("SQL statement prepared"); 106 | 107 | await stmt.run("claude-api-key", apiKey); 108 | sdk.console.log("API Key saved to database successfully"); 109 | 110 | sdk.console.log(`Claude API Key saved: ${apiKey.substring(0, 8)}...`); 111 | 112 | // Trigger request-auth-token event after setting API key 113 | try { 114 | sdk.console.log( 115 | "🔐 Triggering request-auth-token event after API key setup...", 116 | ); 117 | // @ts-ignore 118 | sdk.api.send("request-auth-token", { 119 | source: "setClaudeApiKey", 120 | timestamp: Date.now(), 121 | message: "Requesting auth token after API key setup", 122 | }); 123 | } catch (eventError) { 124 | sdk.console.log("Note: Could not trigger request-auth-token event"); 125 | } 126 | 127 | return { success: true, message: "API Key saved successfully" }; 128 | } catch (error) { 129 | return { 130 | success: true, 131 | message: `Failed to save API key: ${error instanceof Error ? error.message : "Unknown error"}`, 132 | }; 133 | } 134 | }; 135 | 136 | export const getClaudeApiKey = async (sdk: SDK) => { 137 | try { 138 | const db = await sdk.meta.db(); 139 | const stmt = await db.prepare( 140 | "SELECT key_value FROM api_keys WHERE key_name = ?", 141 | ); 142 | const result = await stmt.get("claude-api-key"); 143 | 144 | if (!result || typeof result !== "object" || !("key_value" in result)) { 145 | return null; 146 | } 147 | 148 | return (result as any).key_value; 149 | } catch (error) { 150 | sdk.console.error("Error getting API key:", error); 151 | return null; 152 | } 153 | }; 154 | 155 | export const getDefaultSessionId = async (sdk: SDK): Promise => { 156 | try { 157 | const db = await sdk.meta.db(); 158 | const defaultSessionStmt = await db.prepare( 159 | "SELECT id FROM chat_sessions ORDER BY created_at ASC LIMIT 1", 160 | ); 161 | const defaultSession = await defaultSessionStmt.get(); 162 | return (defaultSession as any).id; 163 | } catch (error) { 164 | sdk.console.error("Error getting default session ID:", error); 165 | throw error; 166 | } 167 | }; 168 | 169 | export const saveProgramResult = async ( 170 | sdk: SDK, 171 | sessionId: number, 172 | toolName: string, 173 | inputData: any, 174 | resultData: any, 175 | summary?: string, 176 | ) => { 177 | try { 178 | const db = await sdk.meta.db(); 179 | const stmt = await db.prepare(` 180 | INSERT INTO program_results (session_id, tool_name, input_data, result_data, summary) 181 | VALUES (?, ?, ?, ?, ?) 182 | `); 183 | 184 | const result = await stmt.run( 185 | sessionId, 186 | toolName, 187 | JSON.stringify(inputData), 188 | JSON.stringify(resultData), 189 | summary || null, 190 | ); 191 | 192 | sdk.console.log(`Program result saved with ID: ${result.lastInsertRowid}`); 193 | return result.lastInsertRowid; 194 | } catch (error) { 195 | sdk.console.error("Error saving program result:", error); 196 | throw error; 197 | } 198 | }; 199 | 200 | export const getProgramResult = async (sdk: SDK, resultId: number) => { 201 | try { 202 | const db = await sdk.meta.db(); 203 | const stmt = await db.prepare(` 204 | SELECT * FROM program_results WHERE id = ? 205 | `); 206 | const result = await stmt.get(resultId); 207 | 208 | if (!result) { 209 | return null; 210 | } 211 | 212 | return { 213 | ...result, 214 | input_data: JSON.parse((result as any).input_data), 215 | result_data: JSON.parse((result as any).result_data), 216 | }; 217 | } catch (error) { 218 | sdk.console.error("Error getting program result:", error); 219 | return null; 220 | } 221 | }; 222 | 223 | export const sendAuthToken = async ( 224 | sdk: SDK, 225 | accessToken: string, 226 | apiEndpoint?: string, 227 | ) => { 228 | try { 229 | // Store the access token in the database for future use 230 | const db = await sdk.meta.db(); 231 | 232 | const stmt = await db.prepare( 233 | `INSERT OR REPLACE INTO api_keys (key_name, key_value) VALUES (?, ?)`, 234 | ); 235 | await stmt.run("caido-auth-token", accessToken); 236 | 237 | // Store the API endpoint if provided 238 | if (apiEndpoint) { 239 | const endpointStmt = await db.prepare( 240 | `INSERT OR REPLACE INTO api_keys (key_name, key_value) VALUES (?, ?)`, 241 | ); 242 | await endpointStmt.run("caido-api-endpoint", apiEndpoint); 243 | sdk.console.log(`Caido API endpoint saved: ${apiEndpoint}`); 244 | } 245 | 246 | sdk.console.log( 247 | `Caido auth token saved: ${accessToken.substring(0, 8)}...`, 248 | ); 249 | return { 250 | success: true, 251 | message: "Auth token and API endpoint saved successfully", 252 | }; 253 | } catch (error) { 254 | sdk.console.error("Error saving auth token:", error); 255 | return { 256 | success: false, 257 | message: `Failed to save auth token: ${error instanceof Error ? error.message : "Unknown error"}`, 258 | }; 259 | } 260 | }; 261 | -------------------------------------------------------------------------------- /packages/backend/src/tools_handlers/scopes.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | 3 | import { executeGraphQLQueryviaSDK } from "../graphql"; 4 | import { 5 | CREATE_SCOPE_MUTATION, 6 | DELETE_SCOPE_MUTATION, 7 | SCOPES_QUERY, 8 | UPDATE_SCOPE_MUTATION, 9 | } from "../graphql/queries"; 10 | 11 | export const list_scopes = async (sdk: SDK, input: any) => { 12 | try { 13 | // Use imported GraphQL query for listing scopes 14 | const query = SCOPES_QUERY; 15 | 16 | const result = await executeGraphQLQueryviaSDK(sdk, { 17 | query, 18 | operationName: "scopes", 19 | }); 20 | 21 | if (!result.success || !result.data) { 22 | return { 23 | success: false, 24 | error: result.error || "Failed to fetch scopes", 25 | summary: "Failed to retrieve scopes from Caido", 26 | }; 27 | } 28 | 29 | const scopes = result.data.scopes || []; 30 | 31 | // Формируем подробный summary с данными каждого scope 32 | const scopesSummary = scopes 33 | .map( 34 | (scope: any) => 35 | `ID: ${scope.id} | Name: ${scope.name} | Allowlist: [${scope.allowlist.join(", ") || "Empty"}] | Denylist: [${scope.denylist.join(", ") || "Empty"}]`, 36 | ) 37 | .join("\n"); 38 | 39 | return { 40 | success: true, 41 | scopes: scopes, 42 | count: scopes.length, 43 | summary: `Retrieved ${scopes.length} scopes:\n\n${scopesSummary}`, 44 | message: `Successfully retrieved ${scopes.length} scopes`, 45 | }; 46 | } catch (error) { 47 | sdk.console.error("Error listing scopes:", error); 48 | return { 49 | success: false, 50 | error: `Failed to list scopes: ${error}`, 51 | details: error instanceof Error ? error.message : String(error), 52 | summary: "Failed to retrieve scopes due to unexpected error", 53 | }; 54 | } 55 | }; 56 | 57 | export const create_scope = async (sdk: SDK, input: any) => { 58 | try { 59 | const { name, allowlist, denylist } = input; 60 | 61 | if (!name) { 62 | return { 63 | success: false, 64 | error: "Scope name is required", 65 | summary: "Please provide a name for the scope", 66 | }; 67 | } 68 | 69 | // Use imported GraphQL mutation for creating scope 70 | const query = CREATE_SCOPE_MUTATION; 71 | 72 | const variables = { 73 | input: { 74 | name: name, 75 | allowlist: allowlist || [], 76 | denylist: denylist || [], 77 | }, 78 | }; 79 | 80 | const result = await executeGraphQLQueryviaSDK(sdk, { 81 | query, 82 | variables, 83 | operationName: "createScope", 84 | }); 85 | 86 | if (!result.success || !result.data) { 87 | return { 88 | success: false, 89 | error: result.error || "Failed to create scope", 90 | summary: `Failed to create scope: ${name}`, 91 | }; 92 | } 93 | 94 | const createResult = result.data.createScope; 95 | 96 | if (createResult.error) { 97 | return { 98 | success: false, 99 | error: `Creation failed with error code: ${createResult.error.code}`, 100 | summary: `Failed to create scope: ${createResult.error.code}`, 101 | details: createResult.error, 102 | }; 103 | } 104 | 105 | const scope = createResult.scope; 106 | 107 | if (!scope) { 108 | return { 109 | success: false, 110 | error: "No scope returned after creation", 111 | summary: `Creation completed but no scope data returned for: ${name}`, 112 | }; 113 | } 114 | 115 | const summary = `Successfully created scope: 116 | ID: ${scope.id} 117 | Name: ${scope.name} 118 | Allowlist: [${scope.allowlist.join(", ") || "Empty"}] 119 | Denylist: [${scope.denylist.join(", ") || "Empty"}]`; 120 | 121 | return { 122 | success: true, 123 | scope: scope, 124 | summary: summary, 125 | message: `Scope "${name}" created successfully`, 126 | }; 127 | } catch (error) { 128 | sdk.console.error("Error creating scope:", error); 129 | return { 130 | success: false, 131 | error: `Failed to create scope: ${error}`, 132 | details: error instanceof Error ? error.message : String(error), 133 | summary: "Failed to create scope due to unexpected error", 134 | }; 135 | } 136 | }; 137 | 138 | export const update_scope = async (sdk: SDK, input: any) => { 139 | try { 140 | const { id, name, allowlist, denylist } = input; 141 | 142 | if (!id) { 143 | return { 144 | success: false, 145 | error: "Scope ID is required", 146 | summary: "Please provide a scope ID to update", 147 | }; 148 | } 149 | 150 | if (!name && allowlist === undefined && denylist === undefined) { 151 | return { 152 | success: false, 153 | error: "At least one field to update is required", 154 | summary: "Please provide name, allowlist, or denylist to update", 155 | }; 156 | } 157 | 158 | // Use imported GraphQL mutation for updating scope 159 | const query = UPDATE_SCOPE_MUTATION; 160 | 161 | const updateInput: any = {}; 162 | if (name !== undefined) updateInput.name = name; 163 | if (allowlist !== undefined) updateInput.allowlist = allowlist; 164 | if (denylist !== undefined) updateInput.denylist = denylist; 165 | 166 | const variables = { 167 | id: id, 168 | input: updateInput, 169 | }; 170 | 171 | const result = await executeGraphQLQueryviaSDK(sdk, { 172 | query, 173 | variables, 174 | operationName: "updateScope", 175 | }); 176 | 177 | if (!result.success || !result.data) { 178 | return { 179 | success: false, 180 | error: result.error || "Failed to update scope", 181 | summary: `Failed to update scope with ID: ${id}`, 182 | }; 183 | } 184 | 185 | const updateResult = result.data.updateScope; 186 | 187 | if (updateResult.error) { 188 | return { 189 | success: false, 190 | error: `Update failed with error code: ${updateResult.error.code}`, 191 | summary: `Failed to update scope: ${updateResult.error.code}`, 192 | details: updateResult.error, 193 | }; 194 | } 195 | 196 | const scope = updateResult.scope; 197 | 198 | if (!scope) { 199 | return { 200 | success: false, 201 | error: "No scope returned after update", 202 | summary: `Update completed but no scope data returned for ID: ${id}`, 203 | }; 204 | } 205 | 206 | const summary = `Successfully updated scope: 207 | ID: ${scope.id} 208 | Name: ${scope.name} 209 | Allowlist: [${scope.allowlist.join(", ") || "Empty"}] 210 | Denylist: [${scope.denylist.join(", ") || "Empty"}]`; 211 | 212 | return { 213 | success: true, 214 | scope: scope, 215 | summary: summary, 216 | message: `Scope ${id} updated successfully`, 217 | }; 218 | } catch (error) { 219 | sdk.console.error("Error updating scope:", error); 220 | return { 221 | success: false, 222 | error: `Failed to update scope: ${error}`, 223 | details: error instanceof Error ? error.message : String(error), 224 | summary: "Failed to update scope due to unexpected error", 225 | }; 226 | } 227 | }; 228 | 229 | export const delete_scope = async (sdk: SDK, input: any) => { 230 | try { 231 | const scopeId = input.id; 232 | 233 | if (!scopeId) { 234 | return { 235 | success: false, 236 | error: "Scope ID is required", 237 | summary: "Please provide a scope ID to delete", 238 | }; 239 | } 240 | 241 | // Use imported GraphQL mutation for deleting scope 242 | const query = DELETE_SCOPE_MUTATION; 243 | 244 | const variables = { 245 | id: scopeId, 246 | }; 247 | 248 | const result = await executeGraphQLQueryviaSDK(sdk, { 249 | query, 250 | variables, 251 | operationName: "deleteScope", 252 | }); 253 | 254 | if (!result.success || !result.data) { 255 | return { 256 | success: false, 257 | error: result.error || "Failed to delete scope", 258 | summary: `Failed to delete scope with ID: ${scopeId}`, 259 | }; 260 | } 261 | 262 | const deleteResult = result.data.deleteScope; 263 | const deletedId = deleteResult.deletedId; 264 | 265 | if (!deletedId) { 266 | return { 267 | success: false, 268 | error: "No scope was deleted", 269 | summary: `No scope was deleted. ID: ${scopeId}`, 270 | }; 271 | } 272 | 273 | const summary = `Successfully deleted scope: 274 | Deleted ID: ${deletedId}`; 275 | 276 | return { 277 | success: true, 278 | deletedId: deletedId, 279 | summary: summary, 280 | message: `Scope ${scopeId} deleted successfully`, 281 | }; 282 | } catch (error) { 283 | sdk.console.error("Error deleting scope:", error); 284 | return { 285 | success: false, 286 | error: `Failed to delete scope: ${error}`, 287 | details: error instanceof Error ? error.message : String(error), 288 | summary: "Failed to delete scope due to unexpected error", 289 | }; 290 | } 291 | }; 292 | -------------------------------------------------------------------------------- /packages/backend/src/tools_handlers/filters.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | 3 | import { executeGraphQLQueryviaSDK } from "../graphql"; 4 | import { 5 | CREATE_FILTER_PRESET_MUTATION, 6 | DELETE_FILTER_PRESET_MUTATION, 7 | FILTER_PRESETS_QUERY, 8 | UPDATE_FILTER_PRESET_MUTATION, 9 | } from "../graphql/queries"; 10 | 11 | export const list_filter_presets = async (sdk: SDK, input: any) => { 12 | try { 13 | // Use imported GraphQL query for listing filter presets 14 | const query = FILTER_PRESETS_QUERY; 15 | 16 | const result = await executeGraphQLQueryviaSDK(sdk, { 17 | query, 18 | operationName: "filterPresets", 19 | }); 20 | 21 | if (!result.success || !result.data) { 22 | return { 23 | success: false, 24 | error: result.error || "Failed to fetch filter presets", 25 | summary: "Failed to retrieve filter presets from Caido", 26 | }; 27 | } 28 | 29 | const filterPresets = result.data.filterPresets || []; 30 | 31 | const filtersSummary = filterPresets 32 | .map( 33 | (filter: any) => 34 | `ID: ${filter.id} | Alias: ${filter.alias} | Name: ${filter.name} | Clause: ${filter.clause || "Empty"}`, 35 | ) 36 | .join("\n"); 37 | 38 | return { 39 | success: true, 40 | filterPresets: filterPresets, 41 | count: filterPresets.length, 42 | summary: `Retrieved ${filterPresets.length} filter presets:\n\n${filtersSummary}`, 43 | message: `Successfully retrieved ${filterPresets.length} filter presets`, 44 | }; 45 | } catch (error) { 46 | sdk.console.error("Error listing filter presets:", error); 47 | return { 48 | success: false, 49 | error: `Failed to list filter presets: ${error}`, 50 | details: error instanceof Error ? error.message : String(error), 51 | summary: "Failed to retrieve filter presets due to unexpected error", 52 | }; 53 | } 54 | }; 55 | 56 | export const create_filter_preset = async (sdk: SDK, input: any) => { 57 | try { 58 | const { alias, name, clause } = input; 59 | 60 | if (!alias || !name) { 61 | return { 62 | success: false, 63 | error: "Alias and name are required", 64 | summary: "Please provide both alias and name for the filter preset", 65 | }; 66 | } 67 | 68 | // Use imported GraphQL mutation for creating filter preset 69 | const query = CREATE_FILTER_PRESET_MUTATION; 70 | 71 | const variables = { 72 | input: { 73 | alias: alias, 74 | name: name, 75 | clause: clause || "", 76 | }, 77 | }; 78 | 79 | const result = await executeGraphQLQueryviaSDK(sdk, { 80 | query, 81 | variables, 82 | operationName: "createFilterPreset", 83 | }); 84 | 85 | if (!result.success || !result.data) { 86 | return { 87 | success: false, 88 | error: result.error || "Failed to create filter preset", 89 | summary: `Failed to create filter preset: ${name}`, 90 | }; 91 | } 92 | 93 | const createResult = result.data.createFilterPreset; 94 | 95 | if (createResult.error) { 96 | return { 97 | success: false, 98 | error: `Creation failed with error code: ${createResult.error.code}`, 99 | summary: `Failed to create filter preset: ${createResult.error.code}`, 100 | details: createResult.error, 101 | }; 102 | } 103 | 104 | const filter = createResult.filter; 105 | 106 | if (!filter) { 107 | return { 108 | success: false, 109 | error: "No filter returned after creation", 110 | summary: `Creation completed but no filter data returned for: ${name}`, 111 | }; 112 | } 113 | 114 | const summary = `Successfully created filter preset: 115 | ID: ${filter.id} 116 | Alias: ${filter.alias} 117 | Name: ${filter.name} 118 | Clause: ${filter.clause || "Empty"}`; 119 | 120 | return { 121 | success: true, 122 | filter: filter, 123 | summary: summary, 124 | message: `Filter preset "${name}" created successfully`, 125 | }; 126 | } catch (error) { 127 | sdk.console.error("Error creating filter preset:", error); 128 | return { 129 | success: false, 130 | error: `Failed to create filter preset: ${error}`, 131 | details: error instanceof Error ? error.message : String(error), 132 | summary: "Failed to create filter preset due to unexpected error", 133 | }; 134 | } 135 | }; 136 | 137 | export const update_filter_preset = async (sdk: SDK, input: any) => { 138 | try { 139 | const { id, alias, name, clause } = input; 140 | 141 | if (!id) { 142 | return { 143 | success: false, 144 | error: "Filter ID is required", 145 | summary: "Please provide a filter ID to update", 146 | }; 147 | } 148 | 149 | if (!alias && !name && clause === undefined) { 150 | return { 151 | success: false, 152 | error: "At least one field to update is required", 153 | summary: "Please provide alias, name, or clause to update", 154 | }; 155 | } 156 | 157 | // Use imported GraphQL mutation for updating filter preset 158 | const query = UPDATE_FILTER_PRESET_MUTATION; 159 | 160 | const updateInput: any = {}; 161 | if (alias !== undefined) updateInput.alias = alias; 162 | if (name !== undefined) updateInput.name = name; 163 | if (clause !== undefined) updateInput.clause = clause; 164 | 165 | const variables = { 166 | id: id, 167 | input: updateInput, 168 | }; 169 | 170 | const result = await executeGraphQLQueryviaSDK(sdk, { 171 | query, 172 | variables, 173 | operationName: "updateFilterPreset", 174 | }); 175 | 176 | if (!result.success || !result.data) { 177 | return { 178 | success: false, 179 | error: result.error || "Failed to update filter preset", 180 | summary: `Failed to update filter preset with ID: ${id}`, 181 | }; 182 | } 183 | 184 | const updateResult = result.data.updateFilterPreset; 185 | 186 | if (updateResult.error) { 187 | return { 188 | success: false, 189 | error: `Update failed with error code: ${updateResult.error.code}`, 190 | summary: `Failed to update filter preset: ${updateResult.error.code}`, 191 | details: updateResult.error, 192 | }; 193 | } 194 | 195 | const filter = updateResult.filter; 196 | 197 | if (!filter) { 198 | return { 199 | success: false, 200 | error: "No filter returned after update", 201 | summary: `Update completed but no filter data returned for ID: ${id}`, 202 | }; 203 | } 204 | 205 | const summary = `Successfully updated filter preset: 206 | ID: ${filter.id} 207 | Alias: ${filter.alias} 208 | Name: ${filter.name} 209 | Clause: ${filter.clause || "Empty"}`; 210 | 211 | return { 212 | success: true, 213 | filter: filter, 214 | summary: summary, 215 | message: `Filter preset ${id} updated successfully`, 216 | }; 217 | } catch (error) { 218 | sdk.console.error("Error updating filter preset:", error); 219 | return { 220 | success: false, 221 | error: `Failed to update filter preset: ${error}`, 222 | details: error instanceof Error ? error.message : String(error), 223 | summary: "Failed to update filter preset due to unexpected error", 224 | }; 225 | } 226 | }; 227 | 228 | export const delete_filter_preset = async (sdk: SDK, input: any) => { 229 | try { 230 | const filterId = input.id; 231 | 232 | if (!filterId) { 233 | return { 234 | success: false, 235 | error: "Filter ID is required", 236 | summary: "Please provide a filter ID to delete", 237 | }; 238 | } 239 | 240 | // Use imported GraphQL mutation for deleting filter preset 241 | const query = DELETE_FILTER_PRESET_MUTATION; 242 | 243 | const variables = { 244 | id: filterId, 245 | }; 246 | 247 | const result = await executeGraphQLQueryviaSDK(sdk, { 248 | query, 249 | variables, 250 | operationName: "deleteFilterPreset", 251 | }); 252 | 253 | if (!result.success || !result.data) { 254 | return { 255 | success: false, 256 | error: result.error || "Failed to delete filter preset", 257 | summary: `Failed to delete filter preset with ID: ${filterId}`, 258 | }; 259 | } 260 | 261 | const deleteResult = result.data.deleteFilterPreset; 262 | const deletedId = deleteResult.deletedId; 263 | 264 | if (!deletedId) { 265 | return { 266 | success: false, 267 | error: "No filter was deleted", 268 | summary: `No filter preset was deleted. ID: ${filterId}`, 269 | }; 270 | } 271 | 272 | const summary = `Successfully deleted filter preset: 273 | Deleted ID: ${deletedId}`; 274 | 275 | return { 276 | success: true, 277 | deletedId: deletedId, 278 | summary: summary, 279 | message: `Filter preset ${filterId} deleted successfully`, 280 | }; 281 | } catch (error) { 282 | sdk.console.error("Error deleting filter preset:", error); 283 | return { 284 | success: false, 285 | error: `Failed to delete filter preset: ${error}`, 286 | details: error instanceof Error ? error.message : String(error), 287 | summary: "Failed to delete filter preset due to unexpected error", 288 | }; 289 | } 290 | }; 291 | -------------------------------------------------------------------------------- /packages/backend/src/chat.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | 3 | import { 4 | getClaudeApiKey, 5 | getDefaultSessionId, 6 | saveProgramResult, 7 | } from "./database"; 8 | import { handlers } from "./handlers"; 9 | import { createAnthropicClient } from "./models"; 10 | import { getConversationHistory, saveMessage } from "./sessions"; 11 | import { 12 | saveToolExecutionState, 13 | startToolExecution, 14 | stopToolExecution, 15 | } from "./tool-tracking"; 16 | import { tools_description } from "./tools"; 17 | 18 | /** 19 | * Recursively process Claude responses that may contain tool_use 20 | * This function handles chains of tool executions 21 | */ 22 | const processClaudeResponse = async ( 23 | sdk: SDK, 24 | anthropic: any, 25 | modelToUse: string, 26 | messages: any[], 27 | systemPrompt: string, 28 | currentSessionId: number, 29 | maxRecursionDepth: number = 5, 30 | ): Promise<{ 31 | finalResponse: string; 32 | toolResults: Array<{ name: string; resultId: number; summary: string }>; 33 | }> => { 34 | if (maxRecursionDepth <= 0) { 35 | throw new Error( 36 | "Maximum recursion depth exceeded for tool execution chain", 37 | ); 38 | } 39 | 40 | const response = await anthropic.messages.create({ 41 | model: modelToUse, 42 | max_tokens: 5000, 43 | messages: messages, 44 | stream: false, 45 | tools: tools_description as any, 46 | system: systemPrompt, 47 | }); 48 | 49 | let finalResponse = ""; 50 | let hasToolUse = false; 51 | const toolResults: Array<{ 52 | name: string; 53 | resultId: number; 54 | summary: string; 55 | }> = []; 56 | 57 | for (const contentItem of response.content) { 58 | if (contentItem.type === "text") { 59 | const textContent = contentItem as { type: "text"; text: string }; 60 | finalResponse += textContent.text; 61 | sdk.console.log( 62 | `Claude text response received: ${textContent.text.substring(0, 50)}...`, 63 | ); 64 | } else if (contentItem.type === "tool_use") { 65 | hasToolUse = true; 66 | const toolUse = contentItem; 67 | sdk.console.log( 68 | `Claude tool use (recursion level ${6 - maxRecursionDepth}): ${toolUse.name} with input: ${JSON.stringify(toolUse.input)}`, 69 | ); 70 | 71 | // Start tool execution tracking 72 | sdk.console.log( 73 | `Starting tool execution tracking for session ${currentSessionId}`, 74 | ); 75 | startToolExecution(currentSessionId, toolUse.name, toolUse.input); 76 | 77 | try { 78 | await saveMessage( 79 | sdk, 80 | currentSessionId, 81 | "assistant", 82 | `Using tool: ${toolUse.name}`, 83 | ); 84 | // @ts-ignore 85 | const handler = handlers[toolUse.name.replace("caido_", "")]; 86 | if (!handler) { 87 | throw new Error("Handler not found for tool: " + toolUse.name); 88 | } 89 | 90 | // Execute the tool 91 | sdk.console.log(`Executing tool ${toolUse.name}...`); 92 | // @ts-ignore 93 | const toolResult = await handler(sdk, toolUse.input); 94 | 95 | // Save the result to program_results table 96 | const resultId = await saveProgramResult( 97 | sdk, 98 | currentSessionId, 99 | toolUse.name, 100 | toolUse.input, 101 | toolResult, 102 | toolResult.summary, 103 | ); 104 | 105 | // Add tool result to tracking 106 | toolResults.push({ 107 | name: toolUse.name, 108 | resultId: resultId, 109 | summary: toolResult.summary, 110 | }); 111 | 112 | // Send tool result back to Claude for continuation 113 | const toolResultMessage = { 114 | role: "user" as const, 115 | content: `Tool ${toolUse.name} executed successfully. Results saved to id: ${resultId}. ${toolResult.summary}`, 116 | }; 117 | 118 | // Add tool result to conversation history 119 | messages.push(toolResultMessage); 120 | await saveMessage( 121 | sdk, 122 | currentSessionId, 123 | "assistant", 124 | toolResultMessage.content, 125 | ); 126 | 127 | // Recursively process the next response 128 | const nextSystemPrompt = `${systemPrompt}\n\nThe user has executed tools: ${toolResults.map((t) => t.name).join(", ")}. Please provide a helpful response based on all tool execution results.`; 129 | 130 | const recursiveResult = await processClaudeResponse( 131 | sdk, 132 | anthropic, 133 | modelToUse, 134 | messages, 135 | nextSystemPrompt, 136 | currentSessionId, 137 | maxRecursionDepth - 1, 138 | ); 139 | 140 | // Merge results 141 | finalResponse = recursiveResult.finalResponse; 142 | toolResults.push(...recursiveResult.toolResults); 143 | 144 | // Stop tool execution tracking 145 | stopToolExecution(currentSessionId); 146 | } catch (error) { 147 | // Stop tool execution tracking on error 148 | await new Promise((resolve) => setTimeout(resolve, 5000)); 149 | saveToolExecutionState(currentSessionId, toolUse.name, toolUse.input); 150 | throw error; 151 | } 152 | } 153 | } 154 | 155 | // If no tool use, return the text response 156 | if (!hasToolUse && finalResponse) { 157 | return { finalResponse, toolResults }; 158 | } else if (!hasToolUse) { 159 | // Fall back to tool results summary if no tool use and no text 160 | if (toolResults.length > 0) { 161 | const summaryMessage = `Tools executed: ${toolResults.map((t) => t.name).join(", ")}. Results saved to ids: ${toolResults.map((t) => t.resultId).join(", ")}.`; 162 | return { finalResponse: summaryMessage, toolResults }; 163 | } 164 | return { finalResponse: "No response generated", toolResults }; 165 | } 166 | 167 | return { finalResponse, toolResults }; 168 | }; 169 | 170 | export const sendMessage = async ( 171 | sdk: SDK, 172 | message: string, 173 | selectedModel?: string, 174 | sessionId?: number, 175 | ) => { 176 | try { 177 | // Trigger request-auth-token event before processing message 178 | try { 179 | sdk.console.log( 180 | "Triggering request-auth-token event before message processing...", 181 | ); 182 | // @ts-ignore - We know this method exists 183 | sdk.api.send("request-auth-token", { 184 | source: "sendMessage", 185 | timestamp: Date.now(), 186 | message: "Requesting auth token before message processing", 187 | }); 188 | } catch (eventError) { 189 | sdk.console.log("Note: Could not trigger request-auth-token event"); 190 | } 191 | 192 | const claudeApiKey = await getClaudeApiKey(sdk); 193 | 194 | if (!claudeApiKey) { 195 | return "Please set your Claude API Key first. Use the input field above to add your API key."; 196 | } 197 | 198 | // Initialize Anthropic client 199 | const anthropic = await createAnthropicClient(sdk); 200 | 201 | // Use selected model or default to claude-3-haiku-20240307 202 | const modelToUse = selectedModel || "claude-3-haiku-20240307"; 203 | 204 | // Get session ID (use provided or default to first session) 205 | let currentSessionId: number; 206 | if (sessionId) { 207 | currentSessionId = sessionId; 208 | } else { 209 | currentSessionId = await getDefaultSessionId(sdk); 210 | } 211 | 212 | // Get conversation history for this session 213 | const history = await getConversationHistory(sdk, currentSessionId); 214 | 215 | // Prepare messages array for Claude API 216 | const messages = history.map((msg: any) => ({ 217 | role: msg.role, 218 | content: msg.content, 219 | })); 220 | 221 | // Add current user message 222 | messages.push({ 223 | role: "user", 224 | content: message, 225 | }); 226 | 227 | // Save user message to database 228 | await saveMessage(sdk, currentSessionId, "user", message); 229 | 230 | // Use the recursive function to process Claude's response 231 | const systemPrompt = `Your AI assistant running in Caido. You are able to use provided tools to help user doing bugbounty`; 232 | const result = await processClaudeResponse( 233 | sdk, 234 | anthropic, 235 | modelToUse, 236 | messages, 237 | systemPrompt, 238 | currentSessionId, 239 | ); 240 | 241 | const finalResponse = result.finalResponse; 242 | const hasToolUse = result.toolResults.length > 0; 243 | 244 | // If we have a text response, save and return it 245 | if (finalResponse) { 246 | if (!hasToolUse) { 247 | // Only save text response if no tools were used (to avoid duplicate saves) 248 | await saveMessage(sdk, currentSessionId, "assistant", finalResponse); 249 | } 250 | return finalResponse; 251 | } else { 252 | const errorMessage = 253 | "Sorry, I received an unexpected response format from Claude."; 254 | sdk.console.error("Unexpected response format:", result); 255 | return errorMessage; 256 | } 257 | } catch (error) { 258 | sdk.console.error("Error in sendMessage:" + error); 259 | 260 | // Check if it's an API key error 261 | if (error instanceof Error && error.message.includes("401")) { 262 | return "Error: Invalid API key. Please check your Claude API key and try again."; 263 | } else if (error instanceof Error && error.message.includes("429")) { 264 | return "Error: Rate limit exceeded. Please wait a moment and try again."; 265 | } else if (error instanceof Error && error.message.includes("500")) { 266 | return "Error: Claude service is temporarily unavailable. Please try again later."; 267 | } else { 268 | return `Error: Unable to communicate with Claude. ${error instanceof Error ? error.message : "Unknown error"}`; 269 | } 270 | } 271 | }; 272 | -------------------------------------------------------------------------------- /claude-mcp-server/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | Tool, 9 | } from "@modelcontextprotocol/sdk/types.js"; 10 | import axios from "axios"; 11 | import { tools_description, tools_version } from "./tools.js"; 12 | import { writeFileSync, appendFileSync } from "fs"; 13 | 14 | // Configuration for connecting to Caido plugin 15 | let CAIDO_CONFIG = { 16 | baseUrl: process.env.CAIDO_BASE_URL || "http://localhost:8080", 17 | authToken: process.env.CAIDO_AUTH_TOKEN, 18 | }; 19 | 20 | const debug = false; 21 | // Logging function 22 | function logToFile(message: string, level: string = "INFO") { 23 | if (!debug) { 24 | return; 25 | } 26 | const timestamp = new Date().toISOString(); 27 | const logEntry = `[${timestamp}] [${level}] ${message}\n`; 28 | const logPath = "~/tmp/log-caido.txt"; 29 | 30 | try { 31 | appendFileSync(logPath, logEntry, { encoding: 'utf8' }); 32 | } catch (error) { 33 | console.error(`Failed to write to log file: ${error}`); 34 | } 35 | } 36 | 37 | // Function to get information about available plugins via GraphQL 38 | async function getPluginInfo(): Promise { 39 | try { 40 | logToFile("Getting plugin info via GraphQL"); 41 | const graphqlQuery = { 42 | operationName: "pluginPackages", 43 | query: `query pluginPackages { 44 | pluginPackages { 45 | ...pluginPackageFull 46 | } 47 | } 48 | fragment pluginAuthorFull on PluginAuthor { 49 | name 50 | email 51 | url 52 | } 53 | fragment pluginLinksFull on PluginLinks { 54 | sponsor 55 | } 56 | fragment pluginPackageMeta on PluginPackage { 57 | id 58 | name 59 | description 60 | author { 61 | ...pluginAuthorFull 62 | } 63 | links { 64 | ...pluginLinksFull 65 | } 66 | version 67 | installedAt 68 | manifestId 69 | } 70 | fragment pluginMeta on Plugin { 71 | __typename 72 | id 73 | name 74 | enabled 75 | manifestId 76 | package { 77 | id 78 | } 79 | } 80 | fragment pluginBackendMeta on PluginBackend { 81 | __typename 82 | id 83 | } 84 | fragment pluginFrontendFull on PluginFrontend { 85 | ...pluginMeta 86 | entrypoint 87 | style 88 | data 89 | backend { 90 | ...pluginBackendMeta 91 | } 92 | } 93 | fragment pluginBackendFull on PluginBackend { 94 | ...pluginMeta 95 | runtime 96 | state { 97 | error 98 | running 99 | } 100 | } 101 | fragment workflowMeta on Workflow { 102 | __typename 103 | id 104 | kind 105 | name 106 | enabled 107 | global 108 | readOnly 109 | } 110 | fragment pluginWorkflowFull on PluginWorkflow { 111 | ...pluginMeta 112 | name 113 | workflow { 114 | ...workflowMeta 115 | } 116 | } 117 | fragment pluginPackageFull on PluginPackage { 118 | ...pluginPackageMeta 119 | plugins { 120 | ... on PluginFrontend { 121 | ...pluginFrontendFull 122 | } 123 | ... on PluginBackend { 124 | ...pluginBackendFull 125 | } 126 | ... on PluginWorkflow { 127 | ...pluginWorkflowFull 128 | } 129 | } 130 | }`, 131 | variables: {} 132 | }; 133 | logToFile(`Sending request to ${CAIDO_CONFIG.baseUrl}/graphql`); 134 | const response = await axios.post(`${CAIDO_CONFIG.baseUrl}/graphql`, graphqlQuery, { 135 | headers: { 136 | "Content-Type": "application/json", 137 | "Authorization": `Bearer ${CAIDO_CONFIG.authToken}`, 138 | } 139 | }); 140 | 141 | logToFile(`Successfully retrieved ${response.data.data.pluginPackages?.length || 0} plugin packages`); 142 | return response.data.data.pluginPackages; 143 | } catch (error) { 144 | logToFile(`Failed to get plugin info: ${error}`, "ERROR"); 145 | throw new Error(`Failed to get plugin info: ${error}`); 146 | } 147 | } 148 | 149 | // Function to find Caido plugin by name 150 | async function findCaidoPlugin(): Promise { 151 | logToFile("Searching for Caido AI Assistant plugin"); 152 | const pluginPackages = await getPluginInfo(); 153 | if (pluginPackages && pluginPackages.length > 0) { 154 | const caidoPackage = pluginPackages.find((pkg: any) => 155 | pkg.name === "Ebka AI Assistant" || 156 | pkg.name?.includes("Ebka") 157 | ); 158 | 159 | if (caidoPackage) { 160 | logToFile(`Found Caido package: ${caidoPackage.name} (ID: ${caidoPackage.id})`); 161 | // Find the backend plugin 162 | const backendPlugin = caidoPackage.plugins.find((plugin: any) => 163 | plugin.__typename === "PluginBackend" 164 | ); 165 | 166 | if (backendPlugin) { 167 | logToFile(`Found backend plugin: ${backendPlugin.name} (ID: ${backendPlugin.id})`); 168 | return backendPlugin.id; 169 | } 170 | logToFile("Caido AI Assistant backend plugin not found", "ERROR"); 171 | throw new Error("Caido AI Assistant backend plugin not found"); 172 | } 173 | logToFile("Caido AI Assistant plugin package not found", "ERROR"); 174 | throw new Error("Caido AI Assistant plugin package not found"); 175 | } 176 | logToFile("No plugin packages found", "ERROR"); 177 | throw new Error("No plugin packages found"); 178 | } 179 | 180 | async function callCaidoFunction(functionName: string, args: any[]): Promise { 181 | logToFile(`Calling Caido function: ${functionName} with args: ${JSON.stringify(args)}`); 182 | const pluginId = await findCaidoPlugin(); 183 | const url = `${CAIDO_CONFIG.baseUrl}/plugin/backend/${pluginId}/function`; 184 | 185 | const response = await axios.post(url, { 186 | name: functionName, 187 | args: args, 188 | }, { 189 | headers: { 190 | "Content-Type": "application/json", 191 | "Authorization": `Bearer ${CAIDO_CONFIG.authToken}`, 192 | } 193 | }); 194 | 195 | logToFile(`Function ${functionName} executed successfully`); 196 | return response.data; 197 | } 198 | 199 | // Create MCP server 200 | const server = new Server( 201 | { 202 | name: "caido-mcp-server", 203 | version: "1.0.0", 204 | }, 205 | { 206 | capabilities: { 207 | tools: {}, 208 | }, 209 | } 210 | ); 211 | 212 | // Register tools from tools.ts 213 | server.setRequestHandler(ListToolsRequestSchema, async () => { 214 | logToFile("Listing available tools"); 215 | const tools: Tool[] = tools_description.map((tool: any) => ({ 216 | name: tool.name, 217 | description: tool.description, 218 | inputSchema: { 219 | type: "object" as const, 220 | properties: tool.input_schema.properties, 221 | required: tool.input_schema.required, 222 | }, 223 | })); 224 | 225 | // Add sendAuthToken tool directly here 226 | const sendAuthTokenTool: Tool = { 227 | name: "sendAuthToken", 228 | description: "Send authentication token and API endpoint to Caido plugin. This must be called first to configure the connection.", 229 | inputSchema: { 230 | type: "object", 231 | properties: { 232 | auth_token: { 233 | type: "string", 234 | description: "Authentication token for Caido API (from browser developer tools)", 235 | }, 236 | api_endpoint: { 237 | type: "string", 238 | description: "API endpoint URL for Caido (e.g., http://localhost:8080)", 239 | }, 240 | }, 241 | required: ["auth_token", "api_endpoint"], 242 | }, 243 | }; 244 | 245 | return { 246 | tools: [sendAuthTokenTool, ...tools], 247 | }; 248 | }); 249 | 250 | // Tool call handler 251 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 252 | const { name, arguments: args } = request.params; 253 | logToFile(`Tool call requested: ${name} with arguments: ${JSON.stringify(args)}`); 254 | 255 | try { 256 | let result: any; 257 | 258 | if (name === "sendAuthToken") { 259 | // In MCP calls, args already contains the parameters object 260 | const params = args as any || ""; 261 | logToFile(`Args: ${JSON.stringify(args)}`); 262 | logToFile(`Params: ${JSON.stringify(params)}`); 263 | 264 | // @ts-ignore 265 | const authToken = params.auth_token || ""; 266 | // @ts-ignore 267 | const apiEndpoint = params.api_endpoint || ""; 268 | 269 | logToFile(`Configuring connection with endpoint: ${apiEndpoint}`); 270 | // Update configuration 271 | CAIDO_CONFIG.authToken = authToken as string; 272 | // @ts-ignore 273 | CAIDO_CONFIG.baseUrl = apiEndpoint.replace(/\/graphql$/, "") as string; 274 | 275 | // Test connection by getting plugin info 276 | const pluginId = await findCaidoPlugin(); 277 | 278 | result = { 279 | success: true, 280 | message: "Auth token and API endpoint saved successfully", 281 | pluginId: pluginId, 282 | pluginName: "Ebka AI Assistant" 283 | }; 284 | 285 | logToFile(`Connection configured successfully. Plugin ID: ${pluginId}`); 286 | } else if (name === "get_plugin_info") { 287 | // Get plugin information 288 | result = await getPluginInfo(); 289 | } else { 290 | // Call regular function in Caido plugin 291 | // In MCP calls, args already contains the parameters object 292 | const params = JSON.stringify(args as any) || {}; 293 | result = await callCaidoFunction("claudeDesktop", [JSON.stringify(name),JSON.stringify(params)]); 294 | if (name === "get_tools_version") { 295 | result.client_info += `\nMCP tools version: ${tools_version}`; 296 | } 297 | } 298 | 299 | logToFile(`Tool ${name} executed successfully`); 300 | return { 301 | content: [ 302 | { 303 | type: "text", 304 | text: JSON.stringify(result, null, 2), 305 | }, 306 | ], 307 | }; 308 | } catch (error) { 309 | logToFile(`Error executing tool ${name}: ${error}`, "ERROR"); 310 | return { 311 | content: [ 312 | { 313 | type: "text", 314 | text: `Error calling Caido function ${name}: ${error}`, 315 | }, 316 | ], 317 | isError: true, 318 | }; 319 | } 320 | }); 321 | 322 | // Start server 323 | async function main() { 324 | logToFile("Starting Caido MCP Server"); 325 | const transport = new StdioServerTransport(); 326 | await server.connect(transport); 327 | logToFile("Caido MCP Server started successfully"); 328 | console.error("Caido MCP Server started"); 329 | } 330 | 331 | main().catch((error) => { 332 | logToFile(`Failed to start server: ${error}`, "ERROR"); 333 | console.error("Failed to start server:", error); 334 | process.exit(1); 335 | }); -------------------------------------------------------------------------------- /claude-mcp-server/build/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; 5 | import axios from "axios"; 6 | import { tools_description, tools_version } from "./tools.js"; 7 | import { appendFileSync } from "fs"; 8 | // Configuration for connecting to Caido plugin 9 | let CAIDO_CONFIG = { 10 | baseUrl: process.env.CAIDO_BASE_URL || "http://localhost:8080", 11 | authToken: process.env.CAIDO_AUTH_TOKEN, 12 | }; 13 | const debug = false; 14 | // Logging function 15 | function logToFile(message, level = "INFO") { 16 | if (!debug) { 17 | return; 18 | } 19 | const timestamp = new Date().toISOString(); 20 | const logEntry = `[${timestamp}] [${level}] ${message}\n`; 21 | const logPath = "~/tmp/log-caido.txt"; 22 | try { 23 | appendFileSync(logPath, logEntry, { encoding: 'utf8' }); 24 | } 25 | catch (error) { 26 | console.error(`Failed to write to log file: ${error}`); 27 | } 28 | } 29 | // Function to get information about available plugins via GraphQL 30 | async function getPluginInfo() { 31 | try { 32 | logToFile("Getting plugin info via GraphQL"); 33 | const graphqlQuery = { 34 | operationName: "pluginPackages", 35 | query: `query pluginPackages { 36 | pluginPackages { 37 | ...pluginPackageFull 38 | } 39 | } 40 | fragment pluginAuthorFull on PluginAuthor { 41 | name 42 | email 43 | url 44 | } 45 | fragment pluginLinksFull on PluginLinks { 46 | sponsor 47 | } 48 | fragment pluginPackageMeta on PluginPackage { 49 | id 50 | name 51 | description 52 | author { 53 | ...pluginAuthorFull 54 | } 55 | links { 56 | ...pluginLinksFull 57 | } 58 | version 59 | installedAt 60 | manifestId 61 | } 62 | fragment pluginMeta on Plugin { 63 | __typename 64 | id 65 | name 66 | enabled 67 | manifestId 68 | package { 69 | id 70 | } 71 | } 72 | fragment pluginBackendMeta on PluginBackend { 73 | __typename 74 | id 75 | } 76 | fragment pluginFrontendFull on PluginFrontend { 77 | ...pluginMeta 78 | entrypoint 79 | style 80 | data 81 | backend { 82 | ...pluginBackendMeta 83 | } 84 | } 85 | fragment pluginBackendFull on PluginBackend { 86 | ...pluginMeta 87 | runtime 88 | state { 89 | error 90 | running 91 | } 92 | } 93 | fragment workflowMeta on Workflow { 94 | __typename 95 | id 96 | kind 97 | name 98 | enabled 99 | global 100 | readOnly 101 | } 102 | fragment pluginWorkflowFull on PluginWorkflow { 103 | ...pluginMeta 104 | name 105 | workflow { 106 | ...workflowMeta 107 | } 108 | } 109 | fragment pluginPackageFull on PluginPackage { 110 | ...pluginPackageMeta 111 | plugins { 112 | ... on PluginFrontend { 113 | ...pluginFrontendFull 114 | } 115 | ... on PluginBackend { 116 | ...pluginBackendFull 117 | } 118 | ... on PluginWorkflow { 119 | ...pluginWorkflowFull 120 | } 121 | } 122 | }`, 123 | variables: {} 124 | }; 125 | logToFile(`Sending request to ${CAIDO_CONFIG.baseUrl}/graphql`); 126 | const response = await axios.post(`${CAIDO_CONFIG.baseUrl}/graphql`, graphqlQuery, { 127 | headers: { 128 | "Content-Type": "application/json", 129 | "Authorization": `Bearer ${CAIDO_CONFIG.authToken}`, 130 | } 131 | }); 132 | logToFile(`Successfully retrieved ${response.data.data.pluginPackages?.length || 0} plugin packages`); 133 | return response.data.data.pluginPackages; 134 | } 135 | catch (error) { 136 | logToFile(`Failed to get plugin info: ${error}`, "ERROR"); 137 | throw new Error(`Failed to get plugin info: ${error}`); 138 | } 139 | } 140 | // Function to find Caido plugin by name 141 | async function findCaidoPlugin() { 142 | logToFile("Searching for Caido AI Assistant plugin"); 143 | const pluginPackages = await getPluginInfo(); 144 | if (pluginPackages && pluginPackages.length > 0) { 145 | const caidoPackage = pluginPackages.find((pkg) => pkg.name === "Ebka AI Assistant" || 146 | pkg.name?.includes("Ebka")); 147 | if (caidoPackage) { 148 | logToFile(`Found Caido package: ${caidoPackage.name} (ID: ${caidoPackage.id})`); 149 | // Find the backend plugin 150 | const backendPlugin = caidoPackage.plugins.find((plugin) => plugin.__typename === "PluginBackend"); 151 | if (backendPlugin) { 152 | logToFile(`Found backend plugin: ${backendPlugin.name} (ID: ${backendPlugin.id})`); 153 | return backendPlugin.id; 154 | } 155 | logToFile("Caido AI Assistant backend plugin not found", "ERROR"); 156 | throw new Error("Caido AI Assistant backend plugin not found"); 157 | } 158 | logToFile("Caido AI Assistant plugin package not found", "ERROR"); 159 | throw new Error("Caido AI Assistant plugin package not found"); 160 | } 161 | logToFile("No plugin packages found", "ERROR"); 162 | throw new Error("No plugin packages found"); 163 | } 164 | async function callCaidoFunction(functionName, args) { 165 | logToFile(`Calling Caido function: ${functionName} with args: ${JSON.stringify(args)}`); 166 | const pluginId = await findCaidoPlugin(); 167 | const url = `${CAIDO_CONFIG.baseUrl}/plugin/backend/${pluginId}/function`; 168 | const response = await axios.post(url, { 169 | name: functionName, 170 | args: args, 171 | }, { 172 | headers: { 173 | "Content-Type": "application/json", 174 | "Authorization": `Bearer ${CAIDO_CONFIG.authToken}`, 175 | } 176 | }); 177 | logToFile(`Function ${functionName} executed successfully`); 178 | return response.data; 179 | } 180 | // Create MCP server 181 | const server = new Server({ 182 | name: "caido-mcp-server", 183 | version: "1.0.0", 184 | }, { 185 | capabilities: { 186 | tools: {}, 187 | }, 188 | }); 189 | // Register tools from tools.ts 190 | server.setRequestHandler(ListToolsRequestSchema, async () => { 191 | logToFile("Listing available tools"); 192 | const tools = tools_description.map((tool) => ({ 193 | name: tool.name, 194 | description: tool.description, 195 | inputSchema: { 196 | type: "object", 197 | properties: tool.input_schema.properties, 198 | required: tool.input_schema.required, 199 | }, 200 | })); 201 | // Add sendAuthToken tool directly here 202 | const sendAuthTokenTool = { 203 | name: "sendAuthToken", 204 | description: "Send authentication token and API endpoint to Caido plugin. This must be called first to configure the connection.", 205 | inputSchema: { 206 | type: "object", 207 | properties: { 208 | auth_token: { 209 | type: "string", 210 | description: "Authentication token for Caido API (from browser developer tools)", 211 | }, 212 | api_endpoint: { 213 | type: "string", 214 | description: "API endpoint URL for Caido (e.g., http://localhost:8080)", 215 | }, 216 | }, 217 | required: ["auth_token", "api_endpoint"], 218 | }, 219 | }; 220 | return { 221 | tools: [sendAuthTokenTool, ...tools], 222 | }; 223 | }); 224 | // Tool call handler 225 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 226 | const { name, arguments: args } = request.params; 227 | logToFile(`Tool call requested: ${name} with arguments: ${JSON.stringify(args)}`); 228 | try { 229 | let result; 230 | if (name === "sendAuthToken") { 231 | // In MCP calls, args already contains the parameters object 232 | const params = args || ""; 233 | logToFile(`Args: ${JSON.stringify(args)}`); 234 | logToFile(`Params: ${JSON.stringify(params)}`); 235 | // @ts-ignore 236 | const authToken = params.auth_token || ""; 237 | // @ts-ignore 238 | const apiEndpoint = params.api_endpoint || ""; 239 | logToFile(`Configuring connection with endpoint: ${apiEndpoint}`); 240 | // Update configuration 241 | CAIDO_CONFIG.authToken = authToken; 242 | // @ts-ignore 243 | CAIDO_CONFIG.baseUrl = apiEndpoint.replace(/\/graphql$/, ""); 244 | // Test connection by getting plugin info 245 | const pluginId = await findCaidoPlugin(); 246 | result = { 247 | success: true, 248 | message: "Auth token and API endpoint saved successfully", 249 | pluginId: pluginId, 250 | pluginName: "Ebka AI Assistant" 251 | }; 252 | logToFile(`Connection configured successfully. Plugin ID: ${pluginId}`); 253 | } 254 | else if (name === "get_plugin_info") { 255 | // Get plugin information 256 | result = await getPluginInfo(); 257 | } 258 | else { 259 | // Call regular function in Caido plugin 260 | // In MCP calls, args already contains the parameters object 261 | const params = JSON.stringify(args) || {}; 262 | result = await callCaidoFunction("claudeDesktop", [JSON.stringify(name), JSON.stringify(params)]); 263 | if (name === "get_tools_version") { 264 | result.client_info += `\nMCP tools version: ${tools_version}`; 265 | } 266 | } 267 | logToFile(`Tool ${name} executed successfully`); 268 | return { 269 | content: [ 270 | { 271 | type: "text", 272 | text: JSON.stringify(result, null, 2), 273 | }, 274 | ], 275 | }; 276 | } 277 | catch (error) { 278 | logToFile(`Error executing tool ${name}: ${error}`, "ERROR"); 279 | return { 280 | content: [ 281 | { 282 | type: "text", 283 | text: `Error calling Caido function ${name}: ${error}`, 284 | }, 285 | ], 286 | isError: true, 287 | }; 288 | } 289 | }); 290 | // Start server 291 | async function main() { 292 | logToFile("Starting Caido MCP Server"); 293 | const transport = new StdioServerTransport(); 294 | await server.connect(transport); 295 | logToFile("Caido MCP Server started successfully"); 296 | console.error("Caido MCP Server started"); 297 | } 298 | main().catch((error) => { 299 | logToFile(`Failed to start server: ${error}`, "ERROR"); 300 | console.error("Failed to start server:", error); 301 | process.exit(1); 302 | }); 303 | -------------------------------------------------------------------------------- /packages/backend/src/tools_handlers/findings.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | 3 | import { executeGraphQLQueryviaSDK } from "../graphql"; 4 | import { 5 | DELETE_FINDINGS_MUTATION, 6 | FINDINGS_BY_OFFSET_QUERY, 7 | GET_FINDING_BY_ID_QUERY, 8 | UPDATE_FINDING_MUTATION, 9 | } from "../graphql/queries"; 10 | 11 | export const update_finding = async (sdk: SDK, input: any) => { 12 | try { 13 | const findingId = input.id; 14 | const updateData = input.input; 15 | 16 | if (!findingId) { 17 | return { 18 | success: false, 19 | error: "Finding ID is required", 20 | summary: "Please provide a finding ID to update", 21 | }; 22 | } 23 | 24 | if (!updateData || Object.keys(updateData).length === 0) { 25 | return { 26 | success: false, 27 | error: "Update data is required", 28 | summary: "Please provide data to update the finding", 29 | }; 30 | } 31 | 32 | // Use imported GraphQL mutation for updating finding 33 | const query = UPDATE_FINDING_MUTATION; 34 | 35 | const variables = { 36 | id: findingId, 37 | input: updateData, 38 | }; 39 | 40 | const result = await executeGraphQLQueryviaSDK(sdk, { 41 | query, 42 | variables, 43 | operationName: "updateFinding", 44 | }); 45 | 46 | if (!result.success || !result.data) { 47 | return { 48 | success: false, 49 | error: result.error || "Failed to update finding", 50 | summary: `Failed to update finding with ID: ${findingId}`, 51 | }; 52 | } 53 | 54 | const updateResult = result.data.updateFinding; 55 | 56 | if (updateResult.error) { 57 | return { 58 | success: false, 59 | error: `Update failed with error code: ${updateResult.error.code}`, 60 | summary: `Failed to update finding: ${updateResult.error.code}`, 61 | }; 62 | } 63 | 64 | const updatedFinding = updateResult.finding; 65 | 66 | if (!updatedFinding) { 67 | return { 68 | success: false, 69 | error: "No finding returned after update", 70 | summary: `Update completed but no finding data returned for ID: ${findingId}`, 71 | }; 72 | } 73 | 74 | const findingSummary = `Successfully updated finding: 75 | ID: ${updatedFinding.id} 76 | Reporter: ${updatedFinding.reporter} 77 | Title: ${updatedFinding.title} 78 | Host: ${updatedFinding.host} 79 | Path: ${updatedFinding.path} 80 | Created: ${updatedFinding.createdAt} 81 | Request ID: ${updatedFinding.request?.id || "N/A"}`; 82 | 83 | return { 84 | success: true, 85 | finding: updatedFinding, 86 | summary: findingSummary, 87 | message: `Finding ${findingId} updated successfully`, 88 | }; 89 | } catch (error) { 90 | sdk.console.error("Error updating finding:", error); 91 | return { 92 | success: false, 93 | error: `Failed to update finding: ${error}`, 94 | details: error instanceof Error ? error.message : String(error), 95 | summary: "Failed to update finding due to unexpected error", 96 | }; 97 | } 98 | }; 99 | 100 | export const delete_findings = async (sdk: SDK, input: any) => { 101 | try { 102 | const findingIds = input.ids; 103 | 104 | if (!findingIds || !Array.isArray(findingIds) || findingIds.length === 0) { 105 | return { 106 | success: false, 107 | error: "Finding IDs array is required", 108 | summary: "Please provide an array of finding IDs to delete", 109 | }; 110 | } 111 | 112 | // Use imported GraphQL mutation for deleting findings 113 | const query = DELETE_FINDINGS_MUTATION; 114 | 115 | const variables = { 116 | input: { 117 | ids: findingIds, 118 | }, 119 | }; 120 | 121 | const result = await executeGraphQLQueryviaSDK(sdk, { 122 | query, 123 | variables, 124 | operationName: "deleteFindings", 125 | }); 126 | 127 | if (!result.success || !result.data) { 128 | return { 129 | success: false, 130 | error: result.error || "Failed to delete findings", 131 | summary: `Failed to delete findings: ${findingIds.join(", ")}`, 132 | }; 133 | } 134 | 135 | const deleteResult = result.data.deleteFindings; 136 | const deletedIds = deleteResult.deletedIds || []; 137 | 138 | if (deletedIds.length === 0) { 139 | return { 140 | success: false, 141 | error: "No findings were deleted", 142 | summary: `No findings were deleted. IDs: ${findingIds.join(", ")}`, 143 | }; 144 | } 145 | 146 | const summary = `Successfully deleted ${deletedIds.length} finding(s): 147 | Deleted IDs: ${deletedIds.join(", ")} 148 | 149 | Requested IDs: ${findingIds.join(", ")} 150 | ${findingIds.length !== deletedIds.length ? `Note: ${findingIds.length - deletedIds.length} ID(s) were not found or could not be deleted` : ""}`; 151 | 152 | return { 153 | success: true, 154 | deletedIds: deletedIds, 155 | requestedIds: findingIds, 156 | count: deletedIds.length, 157 | summary: summary, 158 | message: `Successfully deleted ${deletedIds.length} finding(s)`, 159 | }; 160 | } catch (error) { 161 | sdk.console.error("Error deleting findings:", error); 162 | return { 163 | success: false, 164 | error: `Failed to delete findings: ${error}`, 165 | details: error instanceof Error ? error.message : String(error), 166 | summary: "Failed to delete findings due to unexpected error", 167 | }; 168 | } 169 | }; 170 | 171 | export const list_findings = async (sdk: SDK, input: any) => { 172 | try { 173 | const limit = input.limit || 50; 174 | const offset = input.offset || 0; 175 | 176 | // Use imported GraphQL query for listing findings with pagination 177 | const query = FINDINGS_BY_OFFSET_QUERY; 178 | 179 | const variables = { 180 | filter: input.filter || {}, 181 | limit: limit, 182 | offset: offset, 183 | order: input.order || { by: "ID", ordering: "DESC" }, 184 | }; 185 | 186 | const result = await executeGraphQLQueryviaSDK(sdk, { 187 | query, 188 | variables, 189 | operationName: "getFindingsByOffset", 190 | }); 191 | 192 | if (!result.success || !result.data) { 193 | return { 194 | success: false, 195 | error: result.error || "Failed to fetch findings", 196 | summary: "Failed to retrieve findings from Caido", 197 | }; 198 | } 199 | 200 | const findings = result.data.findingsByOffset.edges.map((edge: any) => ({ 201 | id: edge.node.id, 202 | reporter: edge.node.reporter, 203 | title: edge.node.title, 204 | host: edge.node.host, 205 | path: edge.node.path, 206 | createdAt: edge.node.createdAt, 207 | requestId: edge.node.request?.id || "N/A", 208 | })); 209 | const pageInfo = result.data.findingsByOffset.pageInfo; 210 | 211 | const findingsSummary = findings 212 | .map( 213 | (finding: any) => 214 | `ID: ${finding.id} | Reporter: ${finding.reporter} | Title: ${finding.title} | Host: ${finding.host} | Path: ${finding.path} | Created: ${finding.createdAt} | Request ID: ${finding.requestId}`, 215 | ) 216 | .join("\n"); 217 | 218 | return { 219 | success: true, 220 | findings: findings, 221 | count: findings.length, 222 | pageInfo: pageInfo, 223 | summary: `Retrieved ${findings.length} findings (offset: ${offset}, limit: ${limit}):\n\n${findingsSummary}`, 224 | totalAvailable: pageInfo.hasNextPage ? "More available" : "All retrieved", 225 | }; 226 | } catch (error) { 227 | sdk.console.error("Error listing findings:", error); 228 | return { 229 | success: false, 230 | error: `Failed to list findings: ${error}`, 231 | details: error instanceof Error ? error.message : String(error), 232 | summary: "Failed to retrieve findings due to unexpected error", 233 | }; 234 | } 235 | }; 236 | 237 | export const get_finding_by_id = async (sdk: SDK, input: any) => { 238 | try { 239 | const findingId = input.id; 240 | 241 | if (!findingId) { 242 | return { 243 | success: false, 244 | error: "Finding ID is required", 245 | summary: "Please provide a finding ID to retrieve", 246 | }; 247 | } 248 | 249 | // Use imported GraphQL query for getting finding by ID 250 | const query = GET_FINDING_BY_ID_QUERY; 251 | 252 | const variables = { 253 | id: findingId, 254 | }; 255 | 256 | const result = await executeGraphQLQueryviaSDK(sdk, { 257 | query, 258 | variables, 259 | operationName: "getFindingById", 260 | }); 261 | 262 | if (!result.success || !result.data) { 263 | return { 264 | success: false, 265 | error: result.error || "Failed to fetch finding", 266 | summary: `Failed to retrieve finding with ID: ${findingId}`, 267 | }; 268 | } 269 | 270 | const finding = result.data.finding; 271 | 272 | if (!finding) { 273 | return { 274 | success: false, 275 | error: "Finding not found", 276 | summary: `No finding found with ID: ${findingId}`, 277 | }; 278 | } 279 | 280 | const findingSummary = `ID: ${finding.id} 281 | Reporter: ${finding.reporter} 282 | Title: ${finding.title} 283 | Host: ${finding.host} 284 | Path: ${finding.path} 285 | Created: ${finding.createdAt} 286 | Request ID: ${finding.request?.id || "N/A"} 287 | Description: ${finding.description} 288 | 289 | Request Body: 290 | ${finding.request?.raw || "N/A"} 291 | 292 | Response Body: 293 | ${finding.request?.response?.raw || "N/A"}`; 294 | 295 | return { 296 | success: true, 297 | finding: { 298 | id: finding.id, 299 | reporter: finding.reporter, 300 | title: finding.title, 301 | host: finding.host, 302 | path: finding.path, 303 | createdAt: finding.createdAt, 304 | requestId: finding.request?.id || "N/A", 305 | description: finding.description, 306 | requestBody: finding.request?.raw || "N/A", 307 | responseBody: finding.request?.response?.raw || "N/A", 308 | }, 309 | summary: findingSummary, 310 | details: { 311 | id: finding.id, 312 | title: finding.title, 313 | description: finding.description, 314 | reporter: finding.reporter, 315 | host: finding.host, 316 | path: finding.path, 317 | createdAt: finding.createdAt, 318 | requestId: finding.request?.id || "N/A", 319 | requestBody: finding.request?.raw || "N/A", 320 | responseBody: finding.request?.response?.raw || "N/A", 321 | }, 322 | }; 323 | } catch (error) { 324 | sdk.console.error("Error getting finding by ID:", error); 325 | return { 326 | success: false, 327 | error: `Failed to get finding: ${error}`, 328 | details: error instanceof Error ? error.message : String(error), 329 | summary: "Failed to retrieve finding due to unexpected error", 330 | }; 331 | } 332 | }; 333 | 334 | export const create_findings_from_requests = async (sdk: SDK, input: any) => { 335 | try { 336 | const title = input.title; 337 | const description = input.description; 338 | const reporter = input.reporter || "Ebka AI Assistant"; 339 | const requestId = input.request_id; 340 | const severity = input.severity || "medium"; 341 | const tags = input.tags || []; 342 | 343 | sdk.console.log(`Creating finding: "${title}" for request ${requestId}...`); 344 | 345 | // Validate required inputs 346 | if (!title.trim()) { 347 | return { 348 | error: "Title cannot be empty", 349 | summary: "Invalid finding title", 350 | }; 351 | } 352 | 353 | if (!description.trim()) { 354 | return { 355 | error: "Description cannot be empty", 356 | summary: "Invalid finding description", 357 | }; 358 | } 359 | 360 | if (!requestId) { 361 | return { 362 | error: "Request ID is required", 363 | summary: "Missing request ID", 364 | }; 365 | } 366 | 367 | // Validate severity level 368 | const validSeverities = ["low", "medium", "high", "critical"]; 369 | if (!validSeverities.includes(severity.toLowerCase())) { 370 | return { 371 | error: `Invalid severity level. Must be one of: ${validSeverities.join(", ")}`, 372 | summary: "Invalid severity level", 373 | }; 374 | } 375 | 376 | // Get the request to associate with the finding 377 | let request = null; 378 | try { 379 | request = await sdk.requests.get(requestId); 380 | if (!request) { 381 | return { 382 | error: `No request found with ID: ${requestId}`, 383 | summary: "Request not found", 384 | }; 385 | } 386 | sdk.console.log(`Found request: ${requestId}`); 387 | } catch (error) { 388 | sdk.console.error(`Error getting request ${requestId}: ${error}`); 389 | return { 390 | error: `Failed to get request ${requestId}: ${error instanceof Error ? error.message : "Unknown error"}`, 391 | summary: "Failed to retrieve request", 392 | }; 393 | } 394 | 395 | // Create the finding 396 | try { 397 | const finding = await sdk.findings.create({ 398 | title: title, 399 | description: description, 400 | reporter: reporter, 401 | request: request.request, 402 | }); 403 | 404 | sdk.console.log( 405 | `Successfully created finding with ID: ${finding.getId()}`, 406 | ); 407 | 408 | return { 409 | success: true, 410 | finding_id: finding.getId(), 411 | title: title, 412 | description: description, 413 | reporter: reporter, 414 | request_id: requestId, 415 | severity: severity, 416 | tags: tags, 417 | finding_details: { 418 | id: finding.getId(), 419 | title: finding.getTitle(), 420 | description: finding.getDescription(), 421 | reporter: finding.getReporter(), 422 | }, 423 | summary: `Finding "${title}" successfully created with ID ${finding.getId()}`, 424 | }; 425 | } catch (error) { 426 | sdk.console.error(`Error creating finding: ${error}`); 427 | return { 428 | error: `Failed to create finding: ${error instanceof Error ? error.message : "Unknown error"}`, 429 | summary: "Failed to create finding", 430 | }; 431 | } 432 | } catch (error) { 433 | sdk.console.error("Error in create_findings_from_requests:", error); 434 | return { 435 | error: `Failed to create finding: ${error instanceof Error ? error.message : "Unknown error"}`, 436 | summary: "Failed to create finding", 437 | }; 438 | } 439 | }; 440 | -------------------------------------------------------------------------------- /packages/backend/src/graphql/queries/tampers.ts: -------------------------------------------------------------------------------- 1 | // GraphQL queries and mutations for tamper operations 2 | 3 | // Fragments for tamper operations 4 | export const TAMPER_FRAGMENTS = { 5 | // Matcher fragments 6 | tamperMatcherValueFull: ` 7 | fragment tamperMatcherValueFull on TamperMatcherValue { 8 | __typename 9 | value 10 | } 11 | `, 12 | 13 | tamperMatcherRegexFull: ` 14 | fragment tamperMatcherRegexFull on TamperMatcherRegex { 15 | __typename 16 | regex 17 | } 18 | `, 19 | 20 | tamperMatcherNameFull: ` 21 | fragment tamperMatcherNameFull on TamperMatcherName { 22 | __typename 23 | name 24 | } 25 | `, 26 | 27 | tamperMatcherRawFull: ` 28 | fragment tamperMatcherRawFull on TamperMatcherRaw { 29 | __typename 30 | ... on TamperMatcherValue { 31 | ...tamperMatcherValueFull 32 | } 33 | ... on TamperMatcherRegex { 34 | ...tamperMatcherRegexFull 35 | } 36 | } 37 | `, 38 | 39 | // Replacer fragments 40 | tamperReplacerTermFull: ` 41 | fragment tamperReplacerTermFull on TamperReplacerTerm { 42 | __typename 43 | term 44 | } 45 | `, 46 | 47 | tamperReplacerWorkflowFull: ` 48 | fragment tamperReplacerWorkflowFull on TamperReplacerWorkflow { 49 | __typename 50 | id 51 | } 52 | `, 53 | 54 | tamperReplacerFull: ` 55 | fragment tamperReplacerFull on TamperReplacer { 56 | __typename 57 | ... on TamperReplacerTerm { 58 | ...tamperReplacerTermFull 59 | } 60 | ... on TamperReplacerWorkflow { 61 | ...tamperReplacerWorkflowFull 62 | } 63 | } 64 | `, 65 | 66 | // Operation fragments 67 | tamperOperationPathRawFull: ` 68 | fragment tamperOperationPathRawFull on TamperOperationPathRaw { 69 | __typename 70 | matcher { 71 | ...tamperMatcherRawFull 72 | } 73 | replacer { 74 | ...tamperReplacerFull 75 | } 76 | } 77 | `, 78 | 79 | tamperOperationPathFull: ` 80 | fragment tamperOperationPathFull on TamperOperationPath { 81 | __typename 82 | ... on TamperOperationPathRaw { 83 | ...tamperOperationPathRawFull 84 | } 85 | } 86 | `, 87 | 88 | tamperOperationMethodUpdateFull: ` 89 | fragment tamperOperationMethodUpdateFull on TamperOperationMethodUpdate { 90 | __typename 91 | replacer { 92 | ...tamperReplacerFull 93 | } 94 | } 95 | `, 96 | 97 | tamperOperationMethodFull: ` 98 | fragment tamperOperationMethodFull on TamperOperationMethod { 99 | __typename 100 | ... on TamperOperationMethodUpdate { 101 | ...tamperOperationMethodUpdateFull 102 | } 103 | } 104 | `, 105 | 106 | tamperOperationQueryRawFull: ` 107 | fragment tamperOperationQueryRawFull on TamperOperationQueryRaw { 108 | __typename 109 | matcher { 110 | ...tamperMatcherRawFull 111 | } 112 | replacer { 113 | ...tamperReplacerFull 114 | } 115 | } 116 | `, 117 | 118 | tamperOperationQueryUpdateFull: ` 119 | fragment tamperOperationQueryUpdateFull on TamperOperationQueryUpdate { 120 | __typename 121 | matcher { 122 | ...tamperMatcherNameFull 123 | } 124 | replacer { 125 | ...tamperReplacerFull 126 | } 127 | } 128 | `, 129 | 130 | tamperOperationQueryAddFull: ` 131 | fragment tamperOperationQueryAddFull on TamperOperationQueryAdd { 132 | __typename 133 | matcher { 134 | ...tamperMatcherNameFull 135 | } 136 | replacer { 137 | ...tamperReplacerFull 138 | } 139 | } 140 | `, 141 | 142 | tamperOperationQueryRemoveFull: ` 143 | fragment tamperOperationQueryRemoveFull on TamperOperationQueryRemove { 144 | __typename 145 | matcher { 146 | ...tamperMatcherNameFull 147 | } 148 | } 149 | `, 150 | 151 | tamperOperationQueryFull: ` 152 | fragment tamperOperationQueryFull on TamperOperationQuery { 153 | __typename 154 | ... on TamperOperationQueryRaw { 155 | ...tamperOperationQueryRawFull 156 | } 157 | ... on TamperOperationQueryUpdate { 158 | ...tamperOperationQueryUpdateFull 159 | } 160 | ... on TamperOperationQueryAdd { 161 | ...tamperOperationQueryAddFull 162 | } 163 | ... on TamperOperationQueryRemove { 164 | ...tamperOperationQueryRemoveFull 165 | } 166 | } 167 | `, 168 | 169 | tamperOperationFirstLineRawFull: ` 170 | fragment tamperOperationFirstLineRawFull on TamperOperationFirstLineRaw { 171 | __typename 172 | matcher { 173 | ...tamperMatcherRawFull 174 | } 175 | replacer { 176 | ...tamperReplacerFull 177 | } 178 | } 179 | `, 180 | 181 | tamperOperationFirstLineFull: ` 182 | fragment tamperOperationFirstLineFull on TamperOperationFirstLine { 183 | __typename 184 | ... on TamperOperationFirstLineRaw { 185 | ...tamperOperationFirstLineRawFull 186 | } 187 | } 188 | `, 189 | 190 | tamperOperationHeaderRawFull: ` 191 | fragment tamperOperationHeaderRawFull on TamperOperationHeaderRaw { 192 | __typename 193 | matcher { 194 | ...tamperMatcherRawFull 195 | } 196 | replacer { 197 | ...tamperReplacerFull 198 | } 199 | } 200 | `, 201 | 202 | tamperOperationHeaderUpdateFull: ` 203 | fragment tamperOperationHeaderUpdateFull on TamperOperationHeaderUpdate { 204 | __typename 205 | matcher { 206 | ...tamperMatcherNameFull 207 | } 208 | replacer { 209 | ...tamperReplacerFull 210 | } 211 | } 212 | `, 213 | 214 | tamperOperationHeaderAddFull: ` 215 | fragment tamperOperationHeaderAddFull on TamperOperationHeaderAdd { 216 | __typename 217 | matcher { 218 | ...tamperMatcherNameFull 219 | } 220 | replacer { 221 | ...tamperReplacerFull 222 | } 223 | } 224 | `, 225 | 226 | tamperOperationHeaderRemoveFull: ` 227 | fragment tamperOperationHeaderRemoveFull on TamperOperationHeaderRemove { 228 | __typename 229 | matcher { 230 | ...tamperMatcherNameFull 231 | } 232 | } 233 | `, 234 | 235 | tamperOperationHeaderFull: ` 236 | fragment tamperOperationHeaderFull on TamperOperationHeader { 237 | __typename 238 | ... on TamperOperationHeaderRaw { 239 | ...tamperOperationHeaderRawFull 240 | } 241 | ... on TamperOperationHeaderUpdate { 242 | ...tamperOperationHeaderUpdateFull 243 | } 244 | ... on TamperOperationHeaderAdd { 245 | ...tamperOperationHeaderAddFull 246 | } 247 | ... on TamperOperationHeaderRemove { 248 | ...tamperOperationHeaderRemoveFull 249 | } 250 | } 251 | `, 252 | 253 | tamperOperationBodyRawFull: ` 254 | fragment tamperOperationBodyRawFull on TamperOperationBodyRaw { 255 | __typename 256 | matcher { 257 | ...tamperMatcherRawFull 258 | } 259 | replacer { 260 | ...tamperReplacerFull 261 | } 262 | } 263 | `, 264 | 265 | tamperOperationBodyFull: ` 266 | fragment tamperOperationBodyFull on TamperOperationBody { 267 | __typename 268 | ... on TamperOperationBodyRaw { 269 | ...tamperOperationBodyRawFull 270 | } 271 | } 272 | `, 273 | 274 | tamperOperationStatusCodeUpdateFull: ` 275 | fragment tamperOperationStatusCodeUpdateFull on TamperOperationStatusCodeUpdate { 276 | __typename 277 | replacer { 278 | ...tamperReplacerFull 279 | } 280 | } 281 | `, 282 | 283 | tamperOperationStatusCodeFull: ` 284 | fragment tamperOperationStatusCodeFull on TamperOperationStatusCode { 285 | __typename 286 | ... on TamperOperationStatusCodeUpdate { 287 | ...tamperOperationStatusCodeUpdateFull 288 | } 289 | } 290 | `, 291 | 292 | // Section fragment 293 | tamperSectionFull: ` 294 | fragment tamperSectionFull on TamperSection { 295 | __typename 296 | ... on TamperSectionRequestPath { 297 | operation { 298 | ...tamperOperationPathFull 299 | } 300 | } 301 | ... on TamperSectionRequestMethod { 302 | operation { 303 | ...tamperOperationMethodFull 304 | } 305 | } 306 | ... on TamperSectionRequestQuery { 307 | operation { 308 | ...tamperOperationQueryFull 309 | } 310 | } 311 | ... on TamperSectionRequestFirstLine { 312 | operation { 313 | ...tamperOperationFirstLineFull 314 | } 315 | } 316 | ... on TamperSectionRequestHeader { 317 | operation { 318 | ...tamperOperationHeaderFull 319 | } 320 | } 321 | ... on TamperSectionRequestBody { 322 | operation { 323 | ...tamperOperationBodyFull 324 | } 325 | } 326 | ... on TamperSectionResponseFirstLine { 327 | operation { 328 | ...tamperOperationFirstLineFull 329 | } 330 | } 331 | ... on TamperSectionResponseStatusCode { 332 | operation { 333 | ...tamperOperationStatusCodeFull 334 | } 335 | } 336 | ... on TamperSectionResponseHeader { 337 | operation { 338 | ...tamperOperationHeaderFull 339 | } 340 | } 341 | ... on TamperSectionResponseBody { 342 | operation { 343 | ...tamperOperationBodyFull 344 | } 345 | } 346 | } 347 | `, 348 | 349 | // Rule fragment 350 | tamperRuleFull: ` 351 | fragment tamperRuleFull on TamperRule { 352 | __typename 353 | id 354 | name 355 | section { 356 | ...tamperSectionFull 357 | } 358 | enable { 359 | rank 360 | } 361 | condition 362 | collection { 363 | id 364 | name 365 | } 366 | } 367 | `, 368 | 369 | // Collection fragment 370 | tamperRuleCollectionFull: ` 371 | fragment tamperRuleCollectionFull on TamperRuleCollection { 372 | __typename 373 | id 374 | name 375 | rules { 376 | ...tamperRuleFull 377 | } 378 | } 379 | `, 380 | 381 | // Error fragments 382 | userErrorFull: ` 383 | fragment userErrorFull on UserError { 384 | __typename 385 | code 386 | } 387 | `, 388 | 389 | invalidRegexUserErrorFull: ` 390 | fragment invalidRegexUserErrorFull on InvalidRegexUserError { 391 | ...userErrorFull 392 | term 393 | } 394 | `, 395 | 396 | invalidHTTPQLUserErrorFull: ` 397 | fragment invalidHTTPQLUserErrorFull on InvalidHTTPQLUserError { 398 | ...userErrorFull 399 | query 400 | } 401 | `, 402 | 403 | otherUserErrorFull: ` 404 | fragment otherUserErrorFull on OtherUserError { 405 | ...userErrorFull 406 | } 407 | `, 408 | }; 409 | 410 | // Complete fragments string for mutations 411 | export const getCompleteFragments = () => { 412 | return Object.values(TAMPER_FRAGMENTS).join("\n"); 413 | }; 414 | 415 | // Get fragments needed for collection creation (no error handling needed) 416 | export const getCollectionFragments = () => { 417 | return [ 418 | TAMPER_FRAGMENTS.tamperMatcherValueFull, 419 | TAMPER_FRAGMENTS.tamperMatcherRegexFull, 420 | TAMPER_FRAGMENTS.tamperMatcherRawFull, 421 | TAMPER_FRAGMENTS.tamperReplacerTermFull, 422 | TAMPER_FRAGMENTS.tamperReplacerWorkflowFull, 423 | TAMPER_FRAGMENTS.tamperReplacerFull, 424 | TAMPER_FRAGMENTS.tamperOperationPathRawFull, 425 | TAMPER_FRAGMENTS.tamperOperationPathFull, 426 | TAMPER_FRAGMENTS.tamperOperationMethodUpdateFull, 427 | TAMPER_FRAGMENTS.tamperOperationMethodFull, 428 | TAMPER_FRAGMENTS.tamperOperationQueryRawFull, 429 | TAMPER_FRAGMENTS.tamperMatcherNameFull, 430 | TAMPER_FRAGMENTS.tamperOperationQueryUpdateFull, 431 | TAMPER_FRAGMENTS.tamperOperationQueryAddFull, 432 | TAMPER_FRAGMENTS.tamperOperationQueryRemoveFull, 433 | TAMPER_FRAGMENTS.tamperOperationQueryFull, 434 | TAMPER_FRAGMENTS.tamperOperationFirstLineRawFull, 435 | TAMPER_FRAGMENTS.tamperOperationFirstLineFull, 436 | TAMPER_FRAGMENTS.tamperOperationHeaderRawFull, 437 | TAMPER_FRAGMENTS.tamperOperationHeaderUpdateFull, 438 | TAMPER_FRAGMENTS.tamperOperationHeaderAddFull, 439 | TAMPER_FRAGMENTS.tamperOperationHeaderRemoveFull, 440 | TAMPER_FRAGMENTS.tamperOperationHeaderFull, 441 | TAMPER_FRAGMENTS.tamperOperationBodyRawFull, 442 | TAMPER_FRAGMENTS.tamperOperationBodyFull, 443 | TAMPER_FRAGMENTS.tamperOperationStatusCodeUpdateFull, 444 | TAMPER_FRAGMENTS.tamperOperationStatusCodeFull, 445 | TAMPER_FRAGMENTS.tamperSectionFull, 446 | TAMPER_FRAGMENTS.tamperRuleFull, 447 | TAMPER_FRAGMENTS.tamperRuleCollectionFull, 448 | ].join("\n"); 449 | }; 450 | 451 | // Get fragments needed for rule creation and updates (includes error handling) 452 | export const getRuleFragments = () => { 453 | return [ 454 | TAMPER_FRAGMENTS.tamperMatcherValueFull, 455 | TAMPER_FRAGMENTS.tamperMatcherRegexFull, 456 | TAMPER_FRAGMENTS.tamperMatcherRawFull, 457 | TAMPER_FRAGMENTS.tamperReplacerTermFull, 458 | TAMPER_FRAGMENTS.tamperReplacerWorkflowFull, 459 | TAMPER_FRAGMENTS.tamperReplacerFull, 460 | TAMPER_FRAGMENTS.tamperOperationPathRawFull, 461 | TAMPER_FRAGMENTS.tamperOperationPathFull, 462 | TAMPER_FRAGMENTS.tamperOperationMethodUpdateFull, 463 | TAMPER_FRAGMENTS.tamperOperationMethodFull, 464 | TAMPER_FRAGMENTS.tamperOperationQueryRawFull, 465 | TAMPER_FRAGMENTS.tamperMatcherNameFull, 466 | TAMPER_FRAGMENTS.tamperOperationQueryUpdateFull, 467 | TAMPER_FRAGMENTS.tamperOperationQueryAddFull, 468 | TAMPER_FRAGMENTS.tamperOperationQueryRemoveFull, 469 | TAMPER_FRAGMENTS.tamperOperationQueryFull, 470 | TAMPER_FRAGMENTS.tamperOperationFirstLineRawFull, 471 | TAMPER_FRAGMENTS.tamperOperationFirstLineFull, 472 | TAMPER_FRAGMENTS.tamperOperationHeaderRawFull, 473 | TAMPER_FRAGMENTS.tamperOperationHeaderUpdateFull, 474 | TAMPER_FRAGMENTS.tamperOperationHeaderAddFull, 475 | TAMPER_FRAGMENTS.tamperOperationHeaderRemoveFull, 476 | TAMPER_FRAGMENTS.tamperOperationHeaderFull, 477 | TAMPER_FRAGMENTS.tamperOperationBodyRawFull, 478 | TAMPER_FRAGMENTS.tamperOperationBodyFull, 479 | TAMPER_FRAGMENTS.tamperOperationStatusCodeUpdateFull, 480 | TAMPER_FRAGMENTS.tamperOperationStatusCodeFull, 481 | TAMPER_FRAGMENTS.tamperSectionFull, 482 | TAMPER_FRAGMENTS.tamperRuleFull, 483 | // Error handling fragments 484 | TAMPER_FRAGMENTS.userErrorFull, 485 | TAMPER_FRAGMENTS.invalidRegexUserErrorFull, 486 | TAMPER_FRAGMENTS.invalidHTTPQLUserErrorFull, 487 | TAMPER_FRAGMENTS.otherUserErrorFull, 488 | ].join("\n"); 489 | }; 490 | 491 | // Get fragments needed for reading rules (no error handling, no collection fragments) 492 | export const getReadRuleFragments = () => { 493 | return [ 494 | TAMPER_FRAGMENTS.tamperMatcherValueFull, 495 | TAMPER_FRAGMENTS.tamperMatcherRegexFull, 496 | TAMPER_FRAGMENTS.tamperMatcherRawFull, 497 | TAMPER_FRAGMENTS.tamperReplacerTermFull, 498 | TAMPER_FRAGMENTS.tamperReplacerWorkflowFull, 499 | TAMPER_FRAGMENTS.tamperReplacerFull, 500 | TAMPER_FRAGMENTS.tamperOperationPathRawFull, 501 | TAMPER_FRAGMENTS.tamperOperationPathFull, 502 | TAMPER_FRAGMENTS.tamperOperationMethodUpdateFull, 503 | TAMPER_FRAGMENTS.tamperOperationMethodFull, 504 | TAMPER_FRAGMENTS.tamperOperationQueryRawFull, 505 | TAMPER_FRAGMENTS.tamperMatcherNameFull, 506 | TAMPER_FRAGMENTS.tamperOperationQueryUpdateFull, 507 | TAMPER_FRAGMENTS.tamperOperationQueryAddFull, 508 | TAMPER_FRAGMENTS.tamperOperationQueryRemoveFull, 509 | TAMPER_FRAGMENTS.tamperOperationQueryFull, 510 | TAMPER_FRAGMENTS.tamperOperationFirstLineRawFull, 511 | TAMPER_FRAGMENTS.tamperOperationFirstLineFull, 512 | TAMPER_FRAGMENTS.tamperOperationHeaderRawFull, 513 | TAMPER_FRAGMENTS.tamperOperationHeaderUpdateFull, 514 | TAMPER_FRAGMENTS.tamperOperationHeaderAddFull, 515 | TAMPER_FRAGMENTS.tamperOperationHeaderRemoveFull, 516 | TAMPER_FRAGMENTS.tamperOperationHeaderFull, 517 | TAMPER_FRAGMENTS.tamperOperationBodyRawFull, 518 | TAMPER_FRAGMENTS.tamperOperationBodyFull, 519 | TAMPER_FRAGMENTS.tamperOperationStatusCodeUpdateFull, 520 | TAMPER_FRAGMENTS.tamperOperationStatusCodeFull, 521 | TAMPER_FRAGMENTS.tamperSectionFull, 522 | TAMPER_FRAGMENTS.tamperRuleFull, 523 | ].join("\n"); 524 | }; 525 | 526 | // GraphQL Mutations 527 | export const CREATE_TAMPER_RULE_COLLECTION = ` 528 | mutation createTamperRuleCollection($input: CreateTamperRuleCollectionInput!) { 529 | createTamperRuleCollection(input: $input) { 530 | collection { 531 | ...tamperRuleCollectionFull 532 | } 533 | } 534 | } 535 | `; 536 | 537 | export const CREATE_TAMPER_RULE = ` 538 | mutation createTamperRule($input: CreateTamperRuleInput!) { 539 | createTamperRule(input: $input) { 540 | error { 541 | ... on InvalidRegexUserError { 542 | ...invalidRegexUserErrorFull 543 | } 544 | ... on InvalidHTTPQLUserError { 545 | ...invalidHTTPQLUserErrorFull 546 | } 547 | ... on OtherUserError { 548 | ...otherUserErrorFull 549 | } 550 | } 551 | rule { 552 | ...tamperRuleFull 553 | } 554 | } 555 | } 556 | `; 557 | 558 | export const UPDATE_TAMPER_RULE = ` 559 | mutation updateTamperRule($id: ID!, $input: UpdateTamperRuleInput!) { 560 | updateTamperRule(id: $id, input: $input) { 561 | error { 562 | ... on InvalidRegexUserError { 563 | ...invalidRegexUserErrorFull 564 | } 565 | ... on InvalidHTTPQLUserError { 566 | ...invalidHTTPQLUserErrorFull 567 | } 568 | ... on OtherUserError { 569 | ...otherUserErrorFull 570 | } 571 | } 572 | rule { 573 | ...tamperRuleFull 574 | } 575 | } 576 | } 577 | `; 578 | 579 | // GraphQL Queries 580 | export const LIST_TAMPER_RULE_COLLECTIONS = ` 581 | query tamperRuleCollections { 582 | tamperRuleCollections { 583 | id 584 | name 585 | rules { 586 | id 587 | name 588 | section { 589 | __typename 590 | ... on TamperSectionRequestPath { 591 | operation { 592 | ... on TamperOperationPathRaw { 593 | matcher { 594 | ... on TamperMatcherValue { 595 | value 596 | } 597 | ... on TamperMatcherRegex { 598 | regex 599 | } 600 | } 601 | replacer { 602 | ... on TamperReplacerTerm { 603 | term 604 | } 605 | ... on TamperReplacerWorkflow { 606 | id 607 | } 608 | } 609 | } 610 | } 611 | } 612 | ... on TamperSectionRequestHeader { 613 | operation { 614 | ... on TamperOperationHeaderUpdate { 615 | matcher { 616 | ... on TamperMatcherName { 617 | name 618 | } 619 | } 620 | replacer { 621 | ... on TamperReplacerTerm { 622 | term 623 | } 624 | ... on TamperReplacerWorkflow { 625 | id 626 | } 627 | } 628 | } 629 | ... on TamperOperationHeaderAdd { 630 | matcher { 631 | ... on TamperMatcherName { 632 | name 633 | } 634 | } 635 | replacer { 636 | ... on TamperReplacerTerm { 637 | term 638 | } 639 | ... on TamperReplacerWorkflow { 640 | id 641 | } 642 | } 643 | } 644 | ... on TamperOperationHeaderRemove { 645 | matcher { 646 | ... on TamperMatcherName { 647 | name 648 | } 649 | } 650 | } 651 | } 652 | } 653 | ... on TamperSectionRequestBody { 654 | operation { 655 | ... on TamperOperationBodyRaw { 656 | matcher { 657 | ... on TamperMatcherValue { 658 | value 659 | } 660 | ... on TamperMatcherRegex { 661 | regex 662 | } 663 | } 664 | replacer { 665 | ... on TamperReplacerTerm { 666 | term 667 | } 668 | ... on TamperReplacerWorkflow { 669 | id 670 | } 671 | } 672 | } 673 | } 674 | } 675 | ... on TamperSectionResponseBody { 676 | operation { 677 | ... on TamperOperationBodyRaw { 678 | matcher { 679 | ... on TamperMatcherValue { 680 | value 681 | } 682 | ... on TamperMatcherRegex { 683 | regex 684 | } 685 | } 686 | replacer { 687 | ... on TamperReplacerTerm { 688 | term 689 | } 690 | ... on TamperReplacerWorkflow { 691 | id 692 | } 693 | } 694 | } 695 | } 696 | } 697 | ... on TamperSectionResponseHeader { 698 | operation { 699 | ... on TamperOperationHeaderUpdate { 700 | matcher { 701 | ... on TamperMatcherName { 702 | name 703 | } 704 | } 705 | replacer { 706 | ... on TamperReplacerTerm { 707 | term 708 | } 709 | ... on TamperReplacerWorkflow { 710 | id 711 | } 712 | } 713 | } 714 | ... on TamperOperationHeaderAdd { 715 | matcher { 716 | ... on TamperMatcherName { 717 | name 718 | } 719 | } 720 | replacer { 721 | ... on TamperReplacerTerm { 722 | term 723 | } 724 | ... on TamperReplacerWorkflow { 725 | id 726 | } 727 | } 728 | } 729 | ... on TamperOperationHeaderRemove { 730 | matcher { 731 | ... on TamperMatcherName { 732 | name 733 | } 734 | } 735 | } 736 | } 737 | } 738 | } 739 | enable { 740 | rank 741 | } 742 | condition 743 | collection { 744 | id 745 | name 746 | } 747 | } 748 | } 749 | } 750 | `; 751 | 752 | export const READ_TAMPER_RULE = ` 753 | query tamperRuleCollections { 754 | tamperRuleCollections{ 755 | rules{ 756 | ...tamperRuleFull 757 | } 758 | } 759 | } 760 | `; 761 | -------------------------------------------------------------------------------- /packages/backend/src/tools_handlers/tampers.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | 3 | import { executeGraphQLQueryviaSDK } from "../graphql"; 4 | import { 5 | CREATE_TAMPER_RULE, 6 | CREATE_TAMPER_RULE_COLLECTION, 7 | getCollectionFragments, 8 | getReadRuleFragments, 9 | getRuleFragments, 10 | LIST_TAMPER_RULE_COLLECTIONS, 11 | READ_TAMPER_RULE, 12 | UPDATE_TAMPER_RULE, 13 | } from "../graphql/queries"; 14 | 15 | export const create_tamper_rule_collection = async (sdk: SDK, input: any) => { 16 | try { 17 | const name = input.name; 18 | 19 | if (!name || typeof name !== "string") { 20 | return { 21 | success: false, 22 | error: "Collection name is required and must be a string", 23 | }; 24 | } 25 | 26 | sdk.console.log(`Creating tamper rule collection: ${name}`); 27 | 28 | // This mutation uses named fragments, so we need to include them 29 | const mutation = 30 | CREATE_TAMPER_RULE_COLLECTION + "\n" + getCollectionFragments(); 31 | 32 | const variables = { input: { name: name } }; 33 | 34 | const result = await executeGraphQLQueryviaSDK(sdk, { 35 | query: mutation, 36 | variables: variables, 37 | operationName: "createTamperRuleCollection", 38 | }); 39 | 40 | if (result.data && result.data.createTamperRuleCollection) { 41 | const collection = result.data.createTamperRuleCollection.collection; 42 | 43 | sdk.console.log( 44 | `Successfully created tamper rule collection: ${collection.name} (ID: ${collection.id})`, 45 | ); 46 | 47 | let summary = `Tamper rule collection created successfully:\n`; 48 | summary += `\n📁 Collection Details:`; 49 | summary += `\n - ID: ${collection.id}`; 50 | summary += `\n - Name: "${collection.name}"`; 51 | summary += `\n - Status: Active`; 52 | summary += `\n - Rules Count: ${collection.rules ? collection.rules.length : 0}`; 53 | summary += `\n - Type: Tamper Rule Collection`; 54 | 55 | return { 56 | success: true, 57 | collection_id: collection.id, 58 | collection_name: collection.name, 59 | rules_count: collection.rules ? collection.rules.length : 0, 60 | summary: summary, 61 | }; 62 | } else { 63 | sdk.console.error( 64 | "GraphQL response did not contain expected data:", 65 | result, 66 | ); 67 | return { 68 | success: false, 69 | error: "GraphQL response did not contain expected data", 70 | response: result, 71 | }; 72 | } 73 | } catch (error) { 74 | sdk.console.error("Error creating tamper rule collection:", error); 75 | return { 76 | success: false, 77 | error: `Failed to create tamper rule collection: ${error}`, 78 | details: error instanceof Error ? error.message : String(error), 79 | }; 80 | } 81 | }; 82 | 83 | export const create_tamper_rule = async (sdk: SDK, input: any) => { 84 | try { 85 | const collectionId = input.collection_id; 86 | const name = input.name; 87 | const section = input.section; 88 | const condition = input.condition; 89 | 90 | // Validate required inputs 91 | if (!collectionId || typeof collectionId !== "string") { 92 | return { 93 | success: false, 94 | error: "Collection ID is required and must be a string", 95 | }; 96 | } 97 | 98 | if (!name || typeof name !== "string") { 99 | return { 100 | success: false, 101 | error: "Rule name is required and must be a string", 102 | }; 103 | } 104 | 105 | if (!section || typeof section !== "object") { 106 | return { 107 | success: false, 108 | error: "Section configuration is required and must be an object", 109 | }; 110 | } 111 | 112 | sdk.console.log( 113 | `Creating tamper rule "${name}" in collection ${collectionId}...`, 114 | ); 115 | 116 | // This mutation uses named fragments including error handling, so we need to include them 117 | const mutation = CREATE_TAMPER_RULE + "\n" + getRuleFragments(); 118 | 119 | const variables = { 120 | input: { 121 | collectionId: collectionId, 122 | name: name, 123 | section: section, 124 | ...(condition && { condition: condition }), 125 | }, 126 | }; 127 | 128 | const result = await executeGraphQLQueryviaSDK(sdk, { 129 | query: mutation, 130 | variables: variables, 131 | operationName: "createTamperRule", 132 | }); 133 | 134 | if (result.data && result.data.createTamperRule) { 135 | const response = result.data.createTamperRule; 136 | 137 | // Check for errors 138 | if (response.error) { 139 | sdk.console.error("Error creating tamper rule:", response.error); 140 | return { 141 | success: false, 142 | error: `Failed to create tamper rule: ${response.error.code || "Unknown error"}`, 143 | error_details: response.error, 144 | summary: `Tamper rule creation failed: ${response.error.code || "Unknown error"}`, 145 | }; 146 | } 147 | 148 | // Check for successful rule creation 149 | if (response.rule) { 150 | const rule = response.rule; 151 | sdk.console.log( 152 | `Successfully created tamper rule: ${rule.name} (ID: ${rule.id})`, 153 | ); 154 | 155 | let summary = `Tamper rule created successfully:\n`; 156 | summary += `\n🔧 Rule Details:`; 157 | summary += `\n - ID: ${rule.id}`; 158 | summary += `\n - Name: "${rule.name}"`; 159 | summary += `\n - Collection ID: ${rule.collection?.id || collectionId}`; 160 | summary += `\n - Section Type: ${Object.keys(section)[0] || "unknown"}`; 161 | if (rule.condition) { 162 | summary += `\n - Condition: ${rule.condition}`; 163 | } 164 | summary += `\n - Status: Active`; 165 | 166 | return { 167 | success: true, 168 | rule_id: rule.id, 169 | rule_name: rule.name, 170 | collection_id: rule.collection?.id || collectionId, 171 | section_type: Object.keys(section)[0] || "unknown", 172 | condition: rule.condition, 173 | summary: summary, 174 | }; 175 | } else { 176 | sdk.console.error("No rule data returned from GraphQL mutation"); 177 | return { 178 | success: false, 179 | error: "No rule data returned from GraphQL mutation", 180 | response: response, 181 | summary: "Tamper rule creation failed - no rule data returned", 182 | }; 183 | } 184 | } else { 185 | sdk.console.error( 186 | "GraphQL response did not contain expected data:", 187 | result, 188 | ); 189 | return { 190 | success: false, 191 | error: "GraphQL response did not contain expected data", 192 | response: result, 193 | summary: "Tamper rule creation failed - invalid GraphQL response", 194 | }; 195 | } 196 | } catch (error) { 197 | sdk.console.error("Error creating tamper rule:", error); 198 | return { 199 | success: false, 200 | error: `Failed to create tamper rule: ${error}`, 201 | details: error instanceof Error ? error.message : String(error), 202 | summary: "Tamper rule creation failed due to unexpected error", 203 | }; 204 | } 205 | }; 206 | 207 | export const update_tamper_rule = async (sdk: SDK, input: any) => { 208 | try { 209 | const ruleId = input.rule_id; 210 | const name = input.name; 211 | const section = input.section; 212 | const condition = input.condition; 213 | 214 | // Validate required inputs 215 | if (!ruleId || typeof ruleId !== "string") { 216 | return { 217 | success: false, 218 | error: "Rule ID is required and must be a string", 219 | }; 220 | } 221 | 222 | // At least one field should be provided for update 223 | if (!name && !section && !condition) { 224 | return { 225 | success: false, 226 | error: 227 | "At least one field (name, section, or condition) must be provided for update", 228 | }; 229 | } 230 | 231 | sdk.console.log(`Updating tamper rule ${ruleId}...`); 232 | 233 | // This mutation uses named fragments including error handling, so we need to include them 234 | const mutation = UPDATE_TAMPER_RULE + "\n" + getRuleFragments(); 235 | 236 | // Build update input object with only provided fields 237 | const updateInput: any = {}; 238 | if (name) updateInput.name = name; 239 | if (section) updateInput.section = section; 240 | if (condition !== undefined) updateInput.condition = condition; 241 | 242 | const variables = { 243 | id: ruleId, 244 | input: updateInput, 245 | }; 246 | 247 | sdk.console.log(`Update input:`, JSON.stringify(updateInput, null, 2)); 248 | 249 | const result = await executeGraphQLQueryviaSDK(sdk, { 250 | query: mutation, 251 | variables: variables, 252 | operationName: "updateTamperRule", 253 | }); 254 | 255 | if (result.data && result.data.updateTamperRule) { 256 | const response = result.data.updateTamperRule; 257 | 258 | // Check for errors 259 | if (response.error) { 260 | sdk.console.error("Error updating tamper rule:", response.error); 261 | return { 262 | success: false, 263 | error: `Failed to update tamper rule: ${JSON.stringify(response.error) || "Unknown error"}`, 264 | error_details: response.error, 265 | summary: `Tamper rule update failed: ${JSON.stringify(response.error) || "Unknown error"}`, 266 | }; 267 | } 268 | 269 | // Check for successful rule update 270 | if (response.rule) { 271 | const rule = response.rule; 272 | sdk.console.log( 273 | `Successfully updated tamper rule: ${rule.name} (ID: ${rule.id})`, 274 | ); 275 | 276 | let summary = `Tamper rule updated successfully:\n`; 277 | summary += `\n🔧 Rule Details:`; 278 | summary += `\n - ID: ${rule.id}`; 279 | summary += `\n - Name: "${rule.name}"`; 280 | summary += `\n - Collection ID: ${rule.collection?.id || "unknown"}`; 281 | summary += `\n - Section Type: ${rule.section ? Object.keys(rule.section)[0] || "unknown" : "unknown"}`; 282 | if (rule.condition) { 283 | summary += `\n - Condition: ${rule.condition}`; 284 | } 285 | summary += `\n - Updated Fields: ${Object.keys(updateInput).join(", ")}`; 286 | summary += `\n - Status: Active`; 287 | 288 | return { 289 | success: true, 290 | rule_id: rule.id, 291 | rule_name: rule.name, 292 | collection_id: rule.collection?.id, 293 | section_type: rule.section 294 | ? Object.keys(rule.section)[0] || "unknown" 295 | : "unknown", 296 | condition: rule.condition, 297 | updated_fields: Object.keys(updateInput), 298 | summary: summary, 299 | }; 300 | } else { 301 | sdk.console.error("No rule data returned from GraphQL mutation"); 302 | return { 303 | success: false, 304 | error: "No rule data returned from GraphQL mutation", 305 | response: response, 306 | summary: "Tamper rule update failed - no rule data returned", 307 | }; 308 | } 309 | } else { 310 | sdk.console.error( 311 | "GraphQL response did not contain expected data:", 312 | result, 313 | ); 314 | return { 315 | success: false, 316 | error: "GraphQL response did not contain expected data", 317 | response: result, 318 | summary: "Tamper rule update failed - invalid GraphQL response", 319 | }; 320 | } 321 | } catch (error) { 322 | sdk.console.error("Error updating tamper rule:", error); 323 | return { 324 | success: false, 325 | error: `Failed to update tamper rule: ${error}`, 326 | details: error instanceof Error ? error.message : String(error), 327 | summary: "Tamper rule update failed due to unexpected error", 328 | }; 329 | } 330 | }; 331 | 332 | export const list_tamper_rule_collections = async (sdk: SDK, input: any) => { 333 | try { 334 | const collectionId = input.collection_id; 335 | 336 | sdk.console.log( 337 | `Listing tamper rule collections${collectionId ? ` for ID: ${collectionId}` : ""}...`, 338 | ); 339 | 340 | // Use a comprehensive GraphQL query to get matcher and replacer information 341 | // This query uses inline fragments, so no additional fragments are needed 342 | const query = LIST_TAMPER_RULE_COLLECTIONS; 343 | 344 | const variables = {}; 345 | 346 | sdk.console.log( 347 | "Executing GraphQL query with variables:", 348 | JSON.stringify(variables, null, 2), 349 | ); 350 | sdk.console.log("GraphQL query:", query.substring(0, 200) + "..."); 351 | 352 | const result = await executeGraphQLQueryviaSDK(sdk, { 353 | query: query, 354 | variables: variables, 355 | operationName: "tamperRuleCollections", 356 | }); 357 | 358 | if (result.data && result.data.tamperRuleCollections) { 359 | const collections = result.data.tamperRuleCollections; 360 | 361 | // Filter by collection ID if specified 362 | let targetCollections = collections; 363 | if (collectionId) { 364 | targetCollections = collections.filter( 365 | (collection: any) => collection.id === collectionId, 366 | ); 367 | } 368 | 369 | const collectionsData = targetCollections.map((collection: any) => ({ 370 | id: collection.id, 371 | name: collection.name, 372 | rules_count: collection.rules ? collection.rules.length : 0, 373 | rules: collection.rules 374 | ? collection.rules.map((rule: any) => { 375 | // Extract matcher and replacer information 376 | let matcherInfo = null; 377 | let replacerInfo = null; 378 | 379 | if (rule.section) { 380 | const sectionType = Object.keys(rule.section)[0]; 381 | // @ts-ignore 382 | const sectionData = rule.section[sectionType]; 383 | 384 | if (sectionData && sectionData.operation) { 385 | const operation = sectionData.operation; 386 | const operationType = Object.keys(operation)[0]; 387 | const operationData = operation[operationType]; 388 | 389 | if (operationData) { 390 | // Extract matcher info 391 | if (operationData.matcher) { 392 | matcherInfo = { 393 | has_matcher: true, 394 | matcher_data: operationData.matcher, 395 | }; 396 | } 397 | 398 | // Extract replacer info 399 | if (operationData.replacer) { 400 | replacerInfo = { 401 | has_replacer: true, 402 | replacer_data: operationData.replacer, 403 | }; 404 | } 405 | } 406 | } 407 | } 408 | 409 | return { 410 | id: rule.id, 411 | name: rule.name, 412 | section_type: rule.section 413 | ? Object.keys(rule.section)[0] || "unknown" 414 | : "unknown", 415 | matcher: matcherInfo, 416 | replacer: replacerInfo, 417 | condition: rule.condition, 418 | enabled: rule.enable ? rule.enable.rank > 0 : false, 419 | enable_rank: rule.enable ? rule.enable.rank : 0, 420 | collection_id: rule.collection?.id, 421 | collection_name: rule.collection?.name, 422 | }; 423 | }) 424 | : [], 425 | })); 426 | 427 | sdk.console.log( 428 | `Found ${collectionsData.length} tamper rule collection(s)`, 429 | ); 430 | 431 | // Create detailed summary with all rule information 432 | let summary = `Found ${collectionsData.length} tamper rule collection(s)${collectionId ? ` matching ID: ${collectionId}` : ""}`; 433 | 434 | if (collectionsData.length > 0) { 435 | summary += `:\n`; 436 | collectionsData.forEach((collection: any) => { 437 | summary += `\n📁 Collection: "${collection.name}" (ID: ${collection.id}) - ${collection.rules_count} rules`; 438 | if (collection.rules && collection.rules.length > 0) { 439 | collection.rules.forEach((rule: any) => { 440 | summary += `\n 🔧 Rule: "${rule.name}" (ID: ${rule.id})`; 441 | summary += `\n - Section: ${rule.section_type}`; 442 | summary += `\n - Enabled: ${rule.enabled ? "Yes" : "No"}`; 443 | summary += `\n - Priority: ${rule.enable_rank}`; 444 | if (rule.condition) { 445 | summary += `\n - Condition: ${rule.condition}`; 446 | } 447 | if (rule.matcher) { 448 | summary += `\n - Matcher: ${rule.matcher.type || "unknown"}`; 449 | if (rule.matcher.data) { 450 | if ( 451 | rule.matcher.type === "value" && 452 | rule.matcher.data.value 453 | ) { 454 | summary += ` (Value: "${rule.matcher.data.value}")`; 455 | } else if ( 456 | rule.matcher.type === "regex" && 457 | rule.matcher.data.regex 458 | ) { 459 | summary += ` (Regex: "${rule.matcher.data.regex}")`; 460 | } else if ( 461 | rule.matcher.type === "name" && 462 | rule.matcher.data.name 463 | ) { 464 | summary += ` (Name: "${rule.matcher.data.name}")`; 465 | } 466 | } 467 | } 468 | if (rule.replacer) { 469 | summary += `\n - Replacer: ${rule.replacer.type || "unknown"}`; 470 | if (rule.replacer.data) { 471 | if ( 472 | rule.replacer.type === "term" && 473 | rule.replacer.data.term 474 | ) { 475 | summary += ` (Term: "${rule.replacer.data.term}")`; 476 | } else if ( 477 | rule.replacer.type === "workflow" && 478 | rule.replacer.data.id 479 | ) { 480 | summary += ` (Workflow ID: "${rule.replacer.data.id}")`; 481 | } 482 | } 483 | } 484 | }); 485 | } 486 | }); 487 | } 488 | 489 | return { 490 | success: true, 491 | total_collections: collectionsData.length, 492 | collections: collectionsData, 493 | summary: summary, 494 | }; 495 | } else { 496 | sdk.console.error( 497 | "GraphQL response did not contain expected data:", 498 | result, 499 | ); 500 | sdk.console.error("Full result object:", JSON.stringify(result, null, 2)); 501 | return { 502 | success: false, 503 | error: "GraphQL response did not contain expected data", 504 | response: result, 505 | summary: 506 | "Failed to retrieve tamper rule collections - invalid GraphQL response", 507 | }; 508 | } 509 | } catch (error) { 510 | sdk.console.error("Error listing tamper rule collections:", error); 511 | return { 512 | success: false, 513 | error: `Failed to list tamper rule collections: ${error}`, 514 | details: error instanceof Error ? error.message : String(error), 515 | summary: "Failed to list tamper rule collections due to unexpected error", 516 | }; 517 | } 518 | }; 519 | 520 | export const list_tamper_rules = async (sdk: SDK, input: any) => { 521 | try { 522 | const collectionId = input.collection_id; 523 | const ruleId = input.rule_id; 524 | 525 | sdk.console.log( 526 | `Listing tamper rules${collectionId ? ` from collection: ${collectionId}` : ""}${ruleId ? ` for rule: ${ruleId}` : ""}...`, 527 | ); 528 | 529 | // If specific rule ID is provided, use read_tamper_rule instead 530 | if (ruleId) { 531 | return await read_tamper_rule(sdk, { rule_id: ruleId }); 532 | } 533 | 534 | // Use a comprehensive GraphQL query to get matcher and replacer information 535 | // This query uses inline fragments, so no additional fragments are needed 536 | const query = LIST_TAMPER_RULE_COLLECTIONS; 537 | 538 | const variables = {}; 539 | 540 | sdk.console.log( 541 | "Executing GraphQL query with variables:", 542 | JSON.stringify(variables, null, 2), 543 | ); 544 | sdk.console.log("GraphQL query:", query.substring(0, 200) + "..."); 545 | 546 | const result = await executeGraphQLQueryviaSDK(sdk, { 547 | query: query, 548 | variables: variables, 549 | operationName: "tamperRuleCollections", 550 | }); 551 | 552 | if (result.data && result.data.tamperRuleCollections) { 553 | const collections = result.data.tamperRuleCollections; 554 | 555 | // Filter by collection ID if specified 556 | let targetCollections = collections; 557 | if (collectionId) { 558 | targetCollections = collections.filter( 559 | (collection: any) => collection.id === collectionId, 560 | ); 561 | } 562 | 563 | const allRules: any[] = []; 564 | const collectionsData = targetCollections.map((collection: any) => { 565 | const rules = collection.rules 566 | ? collection.rules.map((rule: any) => { 567 | // Extract matcher and replacer information 568 | let matcherInfo = null; 569 | let replacerInfo = null; 570 | 571 | if (rule.section) { 572 | const sectionType = Object.keys(rule.section)[0]; 573 | // @ts-ignore 574 | const sectionData = rule.section[sectionType]; 575 | 576 | if (sectionData && sectionData.operation) { 577 | const operation = sectionData.operation; 578 | const operationType = Object.keys(operation)[0]; 579 | const operationData = operation[operationType]; 580 | 581 | if (operationData) { 582 | // Extract matcher info 583 | if (operationData.matcher) { 584 | matcherInfo = { 585 | has_matcher: true, 586 | matcher_data: operationData.matcher, 587 | }; 588 | } 589 | 590 | // Extract replacer info 591 | if (operationData.replacer) { 592 | replacerInfo = { 593 | has_replacer: true, 594 | replacer_data: operationData.replacer, 595 | }; 596 | } 597 | } 598 | } 599 | } 600 | 601 | return { 602 | id: rule.id, 603 | name: rule.name, 604 | collection_id: collection.id, 605 | collection_name: collection.name, 606 | section_type: rule.section 607 | ? Object.keys(rule.section)[0] || "unknown" 608 | : "unknown", 609 | matcher: matcherInfo, 610 | replacer: replacerInfo, 611 | condition: rule.condition, 612 | enabled: rule.enable ? rule.enable.rank > 0 : false, 613 | enable_rank: rule.enable ? rule.enable.rank : 0, 614 | }; 615 | }) 616 | : []; 617 | 618 | allRules.push(...rules); 619 | 620 | return { 621 | id: collection.id, 622 | name: collection.name, 623 | rules_count: rules.length, 624 | rules: rules, 625 | }; 626 | }); 627 | 628 | sdk.console.log( 629 | `Found ${allRules.length} tamper rule(s) across ${collectionsData.length} collection(s)`, 630 | ); 631 | 632 | // Create detailed summary with all rule information 633 | let summary = `Found ${allRules.length} tamper rule(s) across ${collectionsData.length} collection(s)${collectionId ? ` in collection: ${collectionId}` : ""}`; 634 | 635 | if (collectionsData.length > 0) { 636 | summary += `:\n`; 637 | collectionsData.forEach((collection: any) => { 638 | summary += `\n📁 Collection: "${collection.name}" (ID: ${collection.id}) - ${collection.rules.length} rules`; 639 | if (collection.rules && collection.rules.length > 0) { 640 | collection.rules.forEach((rule: any) => { 641 | summary += `\n 🔧 Rule: "${rule.name}" (ID: ${rule.id})`; 642 | summary += `\n - Section: ${rule.section_type}`; 643 | summary += `\n - Enabled: ${rule.enabled ? "Yes" : "No"}`; 644 | summary += `\n - Priority: ${rule.enable_rank}`; 645 | if (rule.condition) { 646 | summary += `\n - Condition: ${rule.condition}`; 647 | } 648 | if (rule.matcher) { 649 | summary += `\n - Matcher: ${rule.matcher.type || "unknown"}`; 650 | if (rule.matcher.data) { 651 | if ( 652 | rule.matcher.type === "value" && 653 | rule.matcher.data.value 654 | ) { 655 | summary += ` (Value: "${rule.matcher.data.value}")`; 656 | } else if ( 657 | rule.matcher.type === "regex" && 658 | rule.matcher.data.regex 659 | ) { 660 | summary += ` (Regex: "${rule.matcher.data.regex}")`; 661 | } else if ( 662 | rule.matcher.type === "name" && 663 | rule.matcher.data.name 664 | ) { 665 | summary += ` (Name: "${rule.matcher.data.name}")`; 666 | } 667 | } 668 | } 669 | if (rule.replacer) { 670 | summary += `\n - Replacer: ${rule.replacer.type || "unknown"}`; 671 | if (rule.replacer.data) { 672 | if ( 673 | rule.replacer.type === "term" && 674 | rule.replacer.data.term 675 | ) { 676 | summary += ` (Term: "${rule.replacer.data.term}")`; 677 | } else if ( 678 | rule.replacer.type === "workflow" && 679 | rule.replacer.data.id 680 | ) { 681 | summary += ` (Workflow ID: "${rule.replacer.data.id}")`; 682 | } 683 | } 684 | } 685 | }); 686 | } 687 | }); 688 | } 689 | 690 | return { 691 | success: true, 692 | total_collections: collectionsData.length, 693 | total_rules: allRules.length, 694 | collections: collectionsData, 695 | all_rules: allRules, 696 | summary: summary, 697 | }; 698 | } else { 699 | sdk.console.error( 700 | "GraphQL response did not contain expected data:", 701 | result, 702 | ); 703 | return { 704 | success: false, 705 | error: "GraphQL response did not contain expected data", 706 | response: result, 707 | summary: "Failed to retrieve tamper rules - invalid GraphQL response", 708 | }; 709 | } 710 | } catch (error) { 711 | sdk.console.error("Error listing tamper rules:", error); 712 | return { 713 | success: false, 714 | error: `Failed to list tamper rules: ${error}`, 715 | details: error instanceof Error ? error.message : String(error), 716 | summary: "Failed to list tamper rules due to unexpected error", 717 | }; 718 | } 719 | }; 720 | 721 | export const read_tamper_rule = async (sdk: SDK, input: any) => { 722 | try { 723 | const ruleId = input.rule_id; 724 | 725 | if (!ruleId || typeof ruleId !== "string") { 726 | return { 727 | success: false, 728 | error: "Rule ID is required and must be a string", 729 | }; 730 | } 731 | 732 | sdk.console.log(`Reading tamper rule: ${ruleId}...`); 733 | 734 | // This query uses named fragments, so we need to include them 735 | const query = READ_TAMPER_RULE + "\n" + getReadRuleFragments(); 736 | 737 | const result = await executeGraphQLQueryviaSDK(sdk, { 738 | query: query, 739 | variables: {}, 740 | operationName: "tamperRuleCollections", 741 | }); 742 | 743 | if (result.data && result.data.tamperRuleCollections) { 744 | const collections = result.data.tamperRuleCollections; 745 | 746 | // Find the specific rule across all collections 747 | let targetRule = null; 748 | let targetCollection = null; 749 | 750 | for (const collection of collections) { 751 | if (collection.rules) { 752 | const rule = collection.rules.find((r: any) => r.id === ruleId); 753 | if (rule) { 754 | targetRule = rule; 755 | targetCollection = collection; 756 | break; 757 | } 758 | } 759 | } 760 | 761 | if (targetRule && targetCollection) { 762 | sdk.console.log( 763 | `Found tamper rule: ${targetRule.name} (ID: ${targetRule.id}) in collection: ${targetCollection.name}`, 764 | ); 765 | 766 | // Extract matcher and replacer information 767 | const matcherInfo = null; 768 | const replacerInfo = null; 769 | 770 | sdk.console.log( 771 | "Target rule section:", 772 | JSON.stringify(targetRule.section, null, 2), 773 | ); 774 | // Create detailed summary with all rule information 775 | let summary = `Tamper rule "${targetRule.name}" found successfully:\n`; 776 | summary += `\n🔧 Rule Details:`; 777 | summary += `\n${JSON.stringify(targetRule, null, 2)}`; 778 | 779 | return { 780 | success: true, 781 | rule: { 782 | id: targetRule.id, 783 | name: targetRule.name, 784 | collection_id: targetCollection.id, 785 | collection_name: targetCollection.name, 786 | section_type: targetRule.section 787 | ? Object.keys(targetRule.section)[0] || "unknown" 788 | : "unknown", 789 | matcher: matcherInfo, 790 | replacer: replacerInfo, 791 | section_raw: targetRule.section, 792 | condition: targetRule.condition, 793 | enabled: targetRule.enable ? targetRule.enable.rank > 0 : false, 794 | enable_rank: targetRule.enable ? targetRule.enable.rank : 0, 795 | }, 796 | summary: summary, 797 | }; 798 | } else { 799 | sdk.console.error(`Tamper rule with ID ${ruleId} not found`); 800 | return { 801 | success: false, 802 | error: `Tamper rule with ID ${ruleId} not found`, 803 | rule_id: ruleId, 804 | summary: `Tamper rule with ID ${ruleId} not found in any collection`, 805 | }; 806 | } 807 | } else { 808 | sdk.console.error( 809 | "GraphQL response did not contain expected data:", 810 | result, 811 | ); 812 | return { 813 | success: false, 814 | error: "GraphQL response did not contain expected data", 815 | response: result, 816 | summary: "Failed to read tamper rule - invalid GraphQL response", 817 | }; 818 | } 819 | } catch (error) { 820 | sdk.console.error("Error reading tamper rule:", error); 821 | return { 822 | success: false, 823 | error: `Failed to read tamper rule: ${error}`, 824 | details: error instanceof Error ? error.message : String(error), 825 | summary: "Failed to read tamper rule due to unexpected error", 826 | }; 827 | } 828 | }; 829 | --------------------------------------------------------------------------------