├── .gitignore ├── packages ├── shared │ ├── src │ │ ├── index.ts │ │ └── types.ts │ ├── tsconfig.json │ └── package.json ├── frontend │ ├── src │ │ ├── components │ │ │ ├── dashboard │ │ │ │ ├── ResultTabs │ │ │ │ │ ├── index.ts │ │ │ │ │ └── Container.vue │ │ │ │ ├── ResultRequest │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── Loading.vue │ │ │ │ │ ├── None.vue │ │ │ │ │ ├── Failed.vue │ │ │ │ │ ├── Container.vue │ │ │ │ │ └── Show.vue │ │ │ │ ├── ResultResponse │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── Loading.vue │ │ │ │ │ ├── Failed.vue │ │ │ │ │ ├── None.vue │ │ │ │ │ ├── Container.vue │ │ │ │ │ └── Show.vue │ │ │ │ ├── TemplateTable │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── RuleStatus.vue │ │ │ │ │ └── Container.vue │ │ │ │ └── index.ts │ │ │ └── users-roles │ │ │ │ ├── RoleList │ │ │ │ ├── index.ts │ │ │ │ ├── Container.vue │ │ │ │ └── RoleTable.vue │ │ │ │ ├── UserList │ │ │ │ ├── index.ts │ │ │ │ ├── Container.vue │ │ │ │ └── UserTable.vue │ │ │ │ ├── index.ts │ │ │ │ └── UserShow │ │ │ │ ├── index.ts │ │ │ │ ├── None.vue │ │ │ │ ├── Container.vue │ │ │ │ └── AttributeTable.vue │ │ ├── types │ │ │ ├── roles.ts │ │ │ ├── settings.ts │ │ │ ├── users.ts │ │ │ ├── index.ts │ │ │ ├── templates.ts │ │ │ └── analysis.ts │ │ ├── utils │ │ │ └── index.ts │ │ ├── stores │ │ │ ├── analysis │ │ │ │ ├── index.ts │ │ │ │ ├── useJobState.ts │ │ │ │ ├── useResultState.ts │ │ │ │ └── useSelectionState.ts │ │ │ ├── settings.ts │ │ │ ├── roles.ts │ │ │ ├── templates.ts │ │ │ └── users.ts │ │ ├── styles │ │ │ └── style.css │ │ ├── plugins │ │ │ └── sdk.ts │ │ ├── app.ts │ │ ├── repositories │ │ │ ├── settings.ts │ │ │ ├── analysis.ts │ │ │ ├── roles.ts │ │ │ ├── users.ts │ │ │ ├── template.ts │ │ │ └── templates.ts │ │ ├── index.ts │ │ ├── views │ │ │ ├── UsersRoles.vue │ │ │ ├── App.vue │ │ │ └── Dashboard.vue │ │ └── services │ │ │ ├── roles.ts │ │ │ ├── settings.ts │ │ │ ├── users.ts │ │ │ ├── analysis.ts │ │ │ └── templates.ts │ ├── tsconfig.json │ └── package.json └── backend │ ├── tsconfig.json │ ├── src │ ├── types.ts │ ├── services │ │ ├── settings.ts │ │ ├── roles.ts │ │ ├── users.ts │ │ ├── templates.ts │ │ └── analysis.ts │ ├── utils.ts │ ├── stores │ │ ├── roles.ts │ │ ├── users.ts │ │ ├── settings.ts │ │ ├── analysis.ts │ │ └── templates.ts │ └── index.ts │ └── package.json ├── pnpm-workspace.yaml ├── .markdownlint.json ├── biome.json ├── tsconfig.json ├── README.md ├── package.json ├── .github └── workflows │ ├── validate.yml │ └── release.yml ├── LICENSE └── caido.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-inline-html": false, 3 | "line-length": false, 4 | "first-line-h1": false 5 | } 6 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src/**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultTabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ResultTabs } from "./Container.vue"; 2 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/RoleList/index.ts: -------------------------------------------------------------------------------- 1 | export { default as RoleList } from "./Container.vue"; 2 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/UserList/index.ts: -------------------------------------------------------------------------------- 1 | export { default as UserList } from "./Container.vue"; 2 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultRequest/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ResultRequest } from "./Container.vue"; 2 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultResponse/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ResultResponse } from "./Container.vue"; 2 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/TemplateTable/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TemplateTable } from "./Container.vue"; 2 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RoleList"; 2 | export * from "./UserList"; 3 | export * from "./UserShow"; 4 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/UserShow/index.ts: -------------------------------------------------------------------------------- 1 | export { default as UserShow } from "./Container.vue"; 2 | export { default as UserShowNone } from "./None.vue"; 3 | -------------------------------------------------------------------------------- /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/components/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ResultRequest"; 2 | export * from "./ResultResponse"; 3 | export * from "./TemplateTable"; 4 | export * from "./ResultTabs"; 5 | -------------------------------------------------------------------------------- /packages/frontend/src/types/roles.ts: -------------------------------------------------------------------------------- 1 | import type { RoleDTO } from "shared"; 2 | 3 | export type RoleState = 4 | | { type: "Idle" } 5 | | { type: "Loading" } 6 | | { type: "Error"; error: string } 7 | | { type: "Success"; roles: RoleDTO[] }; 8 | -------------------------------------------------------------------------------- /packages/frontend/src/types/settings.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsDTO } from "shared"; 2 | 3 | export type SettingsState = 4 | | { type: "Idle" } 5 | | { type: "Loading" } 6 | | { type: "Error"; error: string } 7 | | { type: "Success"; settings: SettingsDTO }; 8 | -------------------------------------------------------------------------------- /packages/frontend/src/types/users.ts: -------------------------------------------------------------------------------- 1 | import type { UserDTO } from "shared"; 2 | 3 | export type UserState = 4 | | { type: "Idle" } 5 | | { type: "Loading" } 6 | | { type: "Error"; error: string } 7 | | { type: "Success"; users: UserDTO[]; selectedUserId: string | undefined }; 8 | -------------------------------------------------------------------------------- /packages/frontend/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const generateID = () => { 2 | return ( 3 | Date.now().toString(36) + 4 | Math.random().toString(36).substring(2, 12).padStart(12, "0") 5 | ); 6 | }; 7 | 8 | export const clone = (obj: T): T => { 9 | return JSON.parse(JSON.stringify(obj)); 10 | }; 11 | -------------------------------------------------------------------------------- /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/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Caido } from "@caido/sdk-frontend"; 2 | import type { API } from "backend"; 3 | 4 | export * from "./analysis"; 5 | export * from "./roles"; 6 | export * from "./settings"; 7 | export * from "./users"; 8 | export * from "./templates"; 9 | 10 | export type CaidoSDK = Caido; 11 | -------------------------------------------------------------------------------- /packages/frontend/src/types/templates.ts: -------------------------------------------------------------------------------- 1 | import type { Caido } from "@caido/sdk-frontend"; 2 | import type { API } from "backend"; 3 | import type { TemplateDTO } from "shared"; 4 | 5 | export type CaidoSDK = Caido; 6 | 7 | export type TemplateState = 8 | | { type: "Idle" } 9 | | { type: "Loading" } 10 | | { type: "Error"; error: string } 11 | | { type: "Success"; templates: TemplateDTO[] }; 12 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared", 3 | "version": "0.1.0", 4 | "description": "Shared types between frontend and backend", 5 | "author": "Caido Labs Inc. ", 6 | "license": "CC0-1.0", 7 | "type": "module", 8 | "types": "src/index.ts", 9 | "scripts": { 10 | "typecheck": "tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "zod": "3.23.8" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/frontend/src/stores/analysis/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useJobState } from "./useJobState"; 3 | import { useResultState } from "./useResultState"; 4 | import { useSelectionState } from "./useSelectionState"; 5 | 6 | export const useAnalysisStore = defineStore("stores.analysis", () => { 7 | return { 8 | ...useJobState(), 9 | ...useResultState(), 10 | ...useSelectionState(), 11 | }; 12 | }); 13 | -------------------------------------------------------------------------------- /packages/backend/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { DefineEvents } from "caido:plugin"; 2 | import type { AnalysisRequestDTO, TemplateDTO } from "shared"; 3 | 4 | export type BackendEvents = DefineEvents<{ 5 | "templates:created": (template: TemplateDTO) => void; 6 | "templates:updated": (template: TemplateDTO) => void; 7 | "templates:cleared": () => void; 8 | "results:created": (result: AnalysisRequestDTO) => void; 9 | "results:clear": () => void; 10 | }>; 11 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.0", 4 | "description": "Authmatrix backend", 5 | "author": "Caido Labs Inc. ", 6 | "license": "CC0-1.0", 7 | "type": "module", 8 | "types": "src/index.ts", 9 | "scripts": { 10 | "typecheck": "tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "shared": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | "@caido/sdk-backend": "0.47.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/frontend/src/styles/style.css: -------------------------------------------------------------------------------- 1 | 2 | @import "tailwindcss/utilities"; 3 | @import "tailwindcss/base"; 4 | @import "tailwindcss/components"; 5 | 6 | .status { 7 | padding: 0.25rem 0.5rem; 8 | font-size: 0.8rem; 9 | gap: 0.25rem; 10 | } 11 | 12 | .enforced-status { 13 | background: #498849 !important; 14 | } 15 | 16 | .bypassed-status { 17 | background: #df3257 !important; 18 | } 19 | 20 | .unexpected-status { 21 | background: #f1cb5a !important; 22 | } 23 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultRequest/Loading.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultResponse/Loading.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultRequest/None.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /packages/backend/src/services/settings.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | import type { SettingsDTO } from "shared"; 3 | import { SettingsStore } from "../stores/settings"; 4 | 5 | export const getSettings = () => { 6 | const store = SettingsStore.get(); 7 | return store.getSettings(); 8 | }; 9 | 10 | export const updateSettings = (_sdk: SDK, newSettings: SettingsDTO) => { 11 | const store = SettingsStore.get(); 12 | return store.updateSettings(newSettings); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultRequest/Failed.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultResponse/Failed.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/UserShow/None.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultResponse/None.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /packages/frontend/src/plugins/sdk.ts: -------------------------------------------------------------------------------- 1 | import type { Caido } from "@caido/sdk-frontend"; 2 | import type { API, BackendEvents } from "backend"; 3 | import { type InjectionKey, type Plugin, inject } from "vue"; 4 | 5 | type CaidoSDK = Caido; 6 | 7 | const KEY: InjectionKey = Symbol("CaidoSDK"); 8 | 9 | export const SDKPlugin: Plugin = (app, sdk: CaidoSDK) => { 10 | app.provide(KEY, sdk); 11 | }; 12 | 13 | 14 | export const useSDK = () => { 15 | return inject(KEY) as CaidoSDK; 16 | }; 17 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true 10 | } 11 | }, 12 | "formatter": { 13 | "indentStyle": "space" 14 | }, 15 | "overrides": [ 16 | { 17 | "include": ["*.vue"], 18 | "linter": { 19 | "rules": { 20 | "style": { 21 | "useConst": "off", 22 | "useImportType": "off" 23 | } 24 | } 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | image 3 | 4 |
5 |
6 | Github 7 |   •   8 | Documentation 9 |   •   10 | Discord 11 |
12 |
13 |
14 | 15 | # 🔰 AuthMatrix 16 | 17 | Grid-based authorization testing across multiple users and roles. 18 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultRequest/Container.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultResponse/Container.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /packages/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "description": "Authmatrix Frontend", 5 | "author": "Caido Labs Inc. ", 6 | "license": "CC0-1.0", 7 | "type": "module", 8 | "scripts": { 9 | "typecheck": "vue-tsc --noEmit" 10 | }, 11 | "dependencies": { 12 | "@caido/primevue": "0.1.10", 13 | "@caido/sdk-frontend": "0.47.1", 14 | "@fortawesome/fontawesome-free": "6.7.2", 15 | "@vueuse/core": "13.1.0", 16 | "pinia": "3.0.2", 17 | "primevue": "4.3.3", 18 | "shared": "workspace:*", 19 | "vue": "3.5.13" 20 | }, 21 | "devDependencies": { 22 | "@caido/sdk-backend": "0.47.1", 23 | "@codemirror/view": "6.36.5", 24 | "backend": "workspace:*", 25 | "vue-tsc": "2.2.8" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authmatrix", 3 | "version": "0.0.0", 4 | "description": "Caido plugin for grid-based authorization testing across multiple users and roles", 5 | "author": "Caido Labs Inc. ", 6 | "license": "MIT", 7 | "type": "module", 8 | "scripts": { 9 | "typecheck": "pnpm -r typecheck", 10 | "build": "caido-dev build", 11 | "watch": "caido-dev watch", 12 | "lint": "biome check --write ./packages/*/src" 13 | }, 14 | "devDependencies": { 15 | "@biomejs/biome": "1.9.4", 16 | "@caido-community/dev": "0.1.5", 17 | "@caido/tailwindcss": "0.0.1", 18 | "@vitejs/plugin-vue": "5.2.3", 19 | "postcss-prefixwrap": "1.55.0", 20 | "tailwindcss": "3.4.13", 21 | "tailwindcss-primeui": "0.6.1", 22 | "typescript": "5.8.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/backend/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "crypto"; 2 | 3 | export function sha256Hash(text: string): string { 4 | return createHash('sha256').update(text).digest('hex'); 5 | } 6 | 7 | export const generateID = () => { 8 | return ( 9 | Date.now().toString(36) + 10 | Math.random().toString(36).substring(2, 12).padStart(12, "0") 11 | ); 12 | }; 13 | 14 | export const Uint8ArrayToString = (data: Uint8Array) => { 15 | // Fallback to displaying as a binary string 16 | let output = ""; 17 | const chunkSize = 256; 18 | for (let i = 0; i < data.length; i += chunkSize) { 19 | output += String.fromCharCode(...data.subarray(i, i + chunkSize)); 20 | } 21 | 22 | return output; 23 | }; 24 | 25 | export const isPresent = (value: T | undefined): value is T => { 26 | return value !== undefined; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultRequest/Show.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultResponse/Show.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | 9 | env: 10 | NODE_VERSION: 20 11 | PNPM_VERSION: 9 12 | 13 | jobs: 14 | validate: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 10 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: false 31 | 32 | - name: Install dependencies 33 | run: pnpm install 34 | 35 | - name: Typecheck 36 | run: pnpm typecheck 37 | 38 | - name: Build 39 | run: pnpm build 40 | -------------------------------------------------------------------------------- /packages/backend/src/stores/roles.ts: -------------------------------------------------------------------------------- 1 | import type { RoleDTO } from "shared"; 2 | 3 | export class RoleStore { 4 | private static _store?: RoleStore; 5 | 6 | private roles: Map; 7 | 8 | private constructor() { 9 | this.roles = new Map(); 10 | } 11 | 12 | static get(): RoleStore { 13 | if (!RoleStore._store) { 14 | RoleStore._store = new RoleStore(); 15 | } 16 | 17 | return RoleStore._store; 18 | } 19 | 20 | getRoles() { 21 | return [...this.roles.values()]; 22 | } 23 | 24 | addRole(role: RoleDTO) { 25 | this.roles.set(role.id, role); 26 | } 27 | 28 | deleteRole(requestId: string) { 29 | this.roles.delete(requestId); 30 | } 31 | 32 | updateRole(id: string, fields: Omit) { 33 | const role = this.roles.get(id); 34 | if (role) { 35 | Object.assign(role, fields); 36 | return role; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/frontend/src/types/analysis.ts: -------------------------------------------------------------------------------- 1 | import type { AnalysisRequestDTO } from "shared"; 2 | 3 | export type AnalysisJobState = { type: "Idle" } | { type: "Analyzing" }; 4 | 5 | export type AnalysisResultState = 6 | | { type: "Idle" } 7 | | { type: "Loading" } 8 | | { type: "Error"; error: string } 9 | | { type: "Success"; results: AnalysisRequestDTO[] }; 10 | 11 | export type AnalysisSelectionState = 12 | | { type: "None" } 13 | | { type: "Loading"; templateId: string; userId: string | undefined } 14 | | { type: "Error"; templateId: string; userId: string | undefined } 15 | | { 16 | type: "Success"; 17 | templateId: string; 18 | userId: string | undefined; 19 | request: { 20 | id: string; 21 | raw: string; 22 | }; 23 | response: 24 | | { 25 | id: string; 26 | raw: string; 27 | } 28 | | undefined; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/frontend/src/app.ts: -------------------------------------------------------------------------------- 1 | import { Classic } from "@caido/primevue"; 2 | import PrimeVue from "primevue/config"; 3 | import Tooltip from "primevue/tooltip"; 4 | import { createApp } from "vue"; 5 | 6 | import App from "./views/App.vue"; 7 | 8 | import "@fortawesome/fontawesome-free/css/fontawesome.min.css"; 9 | import "@fortawesome/fontawesome-free/css/regular.min.css"; 10 | import "@fortawesome/fontawesome-free/css/solid.min.css"; 11 | import "./styles/style.css"; 12 | 13 | import { createPinia } from "pinia"; 14 | import { SDKPlugin } from "./plugins/sdk"; 15 | import type { CaidoSDK } from "./types"; 16 | 17 | export const defineApp = (sdk: CaidoSDK) => { 18 | const app = createApp(App); 19 | 20 | const pinia = createPinia(); 21 | app.use(pinia); 22 | 23 | app.use(PrimeVue, { 24 | unstyled: true, 25 | pt: Classic, 26 | }); 27 | 28 | app.directive("tooltip", Tooltip); 29 | 30 | app.use(SDKPlugin, sdk); 31 | 32 | return app; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/backend/src/stores/users.ts: -------------------------------------------------------------------------------- 1 | import type { UserDTO } from "shared"; 2 | 3 | export class UserStore { 4 | private static _store?: UserStore; 5 | 6 | private users: Map; 7 | 8 | private constructor() { 9 | this.users = new Map(); 10 | } 11 | 12 | static get(): UserStore { 13 | if (!UserStore._store) { 14 | UserStore._store = new UserStore(); 15 | } 16 | 17 | return UserStore._store; 18 | } 19 | 20 | getUser(id: string) { 21 | return this.users.get(id); 22 | } 23 | 24 | getUsers() { 25 | return [...this.users.values()]; 26 | } 27 | 28 | addUser(user: UserDTO) { 29 | this.users.set(user.id, user); 30 | } 31 | 32 | deleteUser(requestId: string) { 33 | this.users.delete(requestId); 34 | } 35 | 36 | updateUser(id: string, fields: Omit) { 37 | const user = this.users.get(id); 38 | if (user) { 39 | Object.assign(user, fields); 40 | return user; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/backend/src/services/roles.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | import type { RoleDTO } from "shared"; 3 | import { RoleStore } from "../stores/roles"; 4 | 5 | export const getRoles = (sdk: SDK): RoleDTO[] => { 6 | const store = RoleStore.get(); 7 | 8 | return store.getRoles(); 9 | }; 10 | 11 | export const addRole = (_sdk: SDK, name: string) => { 12 | const id = Date.now().toString(36) + Math.random().toString(36).substring(2); 13 | 14 | const role: RoleDTO = { 15 | id, 16 | name, 17 | description: "", 18 | }; 19 | 20 | const store = RoleStore.get(); 21 | store.addRole(role); 22 | 23 | return role; 24 | }; 25 | 26 | export const deleteRole = (_sdk: SDK, id: string) => { 27 | const store = RoleStore.get(); 28 | store.deleteRole(id); 29 | }; 30 | 31 | export const updateRole = ( 32 | _sdk: SDK, 33 | id: string, 34 | fields: Omit, 35 | ) => { 36 | const store = RoleStore.get(); 37 | return store.updateRole(id, fields); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/backend/src/services/users.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | import type { UserDTO } from "shared"; 3 | import { UserStore } from "../stores/users"; 4 | 5 | export const getUsers = (sdk: SDK) => { 6 | const store = UserStore.get(); 7 | return store.getUsers(); 8 | }; 9 | 10 | export const addUser = (sdk: SDK, name: string) => { 11 | const id = Date.now().toString(36) + Math.random().toString(36).substring(2); 12 | 13 | const user: UserDTO = { 14 | id, 15 | name, 16 | roleIds: [], 17 | attributes: [], 18 | }; 19 | 20 | const store = UserStore.get(); 21 | store.addUser(user); 22 | 23 | return user; 24 | }; 25 | 26 | export const deleteUser = (sdk: SDK, id: string) => { 27 | const store = UserStore.get(); 28 | store.deleteUser(id); 29 | }; 30 | 31 | export const updateUser = ( 32 | sdk: SDK, 33 | id: string, 34 | fields: Omit, 35 | ) => { 36 | const store = UserStore.get(); 37 | return store.updateUser(id, fields); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/frontend/src/repositories/settings.ts: -------------------------------------------------------------------------------- 1 | import { useSDK } from "@/plugins/sdk"; 2 | import type { SettingsDTO } from "shared"; 3 | 4 | export const useSettingsRepository = () => { 5 | const sdk = useSDK(); 6 | const getSettings = async () => { 7 | try { 8 | const settings = await sdk.backend.getSettings(); 9 | return { 10 | type: "Ok" as const, 11 | settings, 12 | }; 13 | } catch { 14 | return { 15 | type: "Err" as const, 16 | error: "Failed to get settings", 17 | }; 18 | } 19 | }; 20 | 21 | const updateSettings = async (newSettings: SettingsDTO) => { 22 | try { 23 | const settings = await sdk.backend.updateSettings(newSettings); 24 | return { 25 | type: "Ok" as const, 26 | settings, 27 | }; 28 | } catch { 29 | return { 30 | type: "Err" as const, 31 | error: "Failed to update settings", 32 | }; 33 | } 34 | }; 35 | 36 | return { 37 | getSettings, 38 | updateSettings, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Caido 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/TemplateTable/RuleStatus.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/RoleList/Container.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 38 | -------------------------------------------------------------------------------- /packages/shared/src/types.ts: -------------------------------------------------------------------------------- 1 | export type RuleStatusDTO = "Untested" | "Enforced" | "Bypassed" | "Unexpected"; 2 | export type RoleRuleDTO = { 3 | type: "RoleRule"; 4 | roleId: string; 5 | hasAccess: boolean; 6 | status: RuleStatusDTO; 7 | }; 8 | 9 | export type UserRuleDTO = { 10 | type: "UserRule"; 11 | userId: string; 12 | hasAccess: boolean; 13 | status: RuleStatusDTO; 14 | }; 15 | 16 | export type TemplateDTO = { 17 | id: string; 18 | authSuccessRegex: string; 19 | rules: (RoleRuleDTO | UserRuleDTO)[]; 20 | requestId: string; 21 | meta: { 22 | host: string; 23 | port: number; 24 | path: string; 25 | isTls: boolean; 26 | method: string; 27 | }; 28 | }; 29 | 30 | export type AnalysisRequestDTO = { 31 | id: string; 32 | userId: string; 33 | requestId: string; 34 | templateId: string; 35 | }; 36 | 37 | export type RoleDTO = { 38 | id: string; 39 | name: string; 40 | description: string; 41 | }; 42 | 43 | export type SettingsDTO = { 44 | autoCaptureRequests: "off" | "all" | "inScope"; 45 | autoRunAnalysis: boolean; 46 | deDuplicateHeaders: string[]; 47 | defaultFilterHTTPQL: string; 48 | }; 49 | 50 | export type UserAttributeDTO = { 51 | id: string; 52 | name: string; 53 | value: string; 54 | kind: "Cookie" | "Header"; 55 | }; 56 | 57 | export type UserDTO = { 58 | id: string; 59 | name: string; 60 | roleIds: string[]; 61 | attributes: UserAttributeDTO[]; 62 | }; 63 | -------------------------------------------------------------------------------- /packages/frontend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { defineApp } from "./app"; 2 | import type { CaidoSDK } from "./types"; 3 | 4 | export const init = (sdk: CaidoSDK ) => { 5 | const app = defineApp(sdk); 6 | 7 | const root = document.createElement("div"); 8 | Object.assign(root.style, { 9 | height: "100%", 10 | width: "100%", 11 | }); 12 | 13 | app.mount(root); 14 | 15 | sdk.navigation.addPage("/authmatrix", { 16 | body: root, 17 | }); 18 | 19 | sdk.sidebar.registerItem("Authmatrix", "/authmatrix", { 20 | icon: "fas fa-user-shield", 21 | }); 22 | 23 | sdk.commands.register("send-to-authmatrix", { 24 | name: "Send to Authmatrix", 25 | run: (context) => { 26 | if (context.type === "RequestRowContext") { 27 | context.requests.forEach(async (request) => { 28 | if (request.id) { 29 | sdk.backend.addTemplateFromContext(request.id); 30 | } 31 | }); 32 | } else if (context.type === "RequestContext") { 33 | if (context.request.id) { 34 | sdk.backend.addTemplateFromContext(context.request.id); 35 | } 36 | } 37 | }, 38 | }); 39 | 40 | sdk.menu.registerItem({ 41 | type: "RequestRow", 42 | commandId: "send-to-authmatrix", 43 | leadingIcon: "fas fa-user-shield", 44 | }); 45 | 46 | sdk.menu.registerItem({ 47 | type: "Request", 48 | commandId: "send-to-authmatrix", 49 | leadingIcon: "fas fa-user-shield", 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/frontend/src/stores/analysis/useJobState.ts: -------------------------------------------------------------------------------- 1 | import type { AnalysisJobState } from "@/types"; 2 | import { reactive } from "vue"; 3 | 4 | type Context = { 5 | state: AnalysisJobState; 6 | }; 7 | 8 | type Message = { type: "Start" } | { type: "Done" }; 9 | 10 | export const useJobState = () => { 11 | const context: Context = reactive({ 12 | state: { type: "Idle" }, 13 | }); 14 | 15 | const getState = () => context.state; 16 | 17 | const send = (message: Message) => { 18 | const currState = context.state; 19 | 20 | switch (currState.type) { 21 | case "Idle": 22 | context.state = processIdle(currState, message); 23 | break; 24 | case "Analyzing": 25 | context.state = processAnalyzing(currState, message); 26 | break; 27 | } 28 | }; 29 | 30 | return { 31 | jobState: { 32 | getState, 33 | send, 34 | }, 35 | }; 36 | }; 37 | 38 | const processIdle = ( 39 | state: AnalysisJobState & { type: "Idle" }, 40 | message: Message, 41 | ): AnalysisJobState => { 42 | switch (message.type) { 43 | case "Start": 44 | return { type: "Analyzing" }; 45 | case "Done": 46 | return state; 47 | } 48 | }; 49 | 50 | const processAnalyzing = ( 51 | state: AnalysisJobState & { type: "Analyzing" }, 52 | message: Message, 53 | ): AnalysisJobState => { 54 | switch (message.type) { 55 | case "Done": 56 | return { type: "Idle" }; 57 | case "Start": 58 | return state; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /packages/frontend/src/views/UsersRoles.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 38 | -------------------------------------------------------------------------------- /packages/backend/src/stores/settings.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsDTO } from "shared"; 2 | 3 | const noStylingFilter: string = `req.ext.nlike:"%.css" AND req.ext.nlike:"%.woff" AND req.ext.nlike:"%.woff2" AND req.ext.nlike:"%.ttf" AND req.ext.nlike:"%.eot"`; 4 | const noImagesFilter: string = `req.ext.nlike:"%.apng" AND req.ext.nlike:"%.avif" AND req.ext.nlike:"%.gif" AND req.ext.nlike:"%.jpg" AND req.ext.nlike:"%.jpeg" AND req.ext.nlike:"%.pjpeg" AND req.ext.nlike:"%.pjp" AND req.ext.nlike:"%.png" AND req.ext.nlike:"%.svg" AND req.ext.nlike:"%.webp" AND req.ext.nlike:"%.bmp" AND req.ext.nlike:"%.ico" AND req.ext.nlike:"%.cur" AND req.ext.nlike:"%.tif" AND req.ext.nlike:"%.tiff"`; 5 | const noJSFilter: string = `req.ext.nlike:"%.js"` 6 | 7 | export class SettingsStore { 8 | private static _store?: SettingsStore; 9 | 10 | private settings: SettingsDTO; 11 | 12 | 13 | private constructor() { 14 | this.settings = { 15 | autoCaptureRequests: "off", 16 | autoRunAnalysis: true, 17 | deDuplicateHeaders: [], 18 | defaultFilterHTTPQL: `(${noStylingFilter} AND ${noImagesFilter} AND ${noJSFilter})`, 19 | }; 20 | } 21 | 22 | static get(): SettingsStore { 23 | if (!SettingsStore._store) { 24 | SettingsStore._store = new SettingsStore(); 25 | } 26 | 27 | return SettingsStore._store; 28 | } 29 | 30 | getSettings() { 31 | return { ...this.settings }; 32 | } 33 | 34 | updateSettings(newSettings: SettingsDTO) { 35 | this.settings = { ...newSettings }; 36 | return this.settings; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/frontend/src/repositories/analysis.ts: -------------------------------------------------------------------------------- 1 | import { useSDK } from "@/plugins/sdk"; 2 | 3 | export const useAnalysisRepository = () => { 4 | const sdk = useSDK(); 5 | 6 | const getAnalysisResults = async () => { 7 | try { 8 | const results = await sdk.backend.getResults(); 9 | return { 10 | type: "Ok" as const, 11 | results, 12 | }; 13 | } catch { 14 | return { 15 | type: "Err" as const, 16 | error: "Failed to get analysis results", 17 | }; 18 | } 19 | }; 20 | 21 | const runAnalysis = async () => { 22 | try { 23 | await sdk.backend.runAnalysis(); 24 | return { 25 | type: "Ok" as const, 26 | }; 27 | } catch { 28 | return { 29 | type: "Err" as const, 30 | error: "Failed to run analysis", 31 | }; 32 | } 33 | }; 34 | 35 | const getRequestResponse = async (requestId: string) => { 36 | try { 37 | const result = await sdk.backend.getRequestResponse(requestId); 38 | if (result.type === "Ok") { 39 | return { 40 | type: "Ok" as const, 41 | request: result.request, 42 | response: result.response, 43 | }; 44 | } 45 | 46 | return { 47 | type: "Err" as const, 48 | error: result.message, 49 | }; 50 | } catch { 51 | return { 52 | type: "Err" as const, 53 | error: "Failed to get request & response", 54 | }; 55 | } 56 | }; 57 | 58 | return { 59 | getAnalysisResults, 60 | runAnalysis, 61 | getRequestResponse, 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/UserShow/Container.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/UserList/Container.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 51 | -------------------------------------------------------------------------------- /packages/backend/src/stores/analysis.ts: -------------------------------------------------------------------------------- 1 | import type { AnalysisRequestDTO } from "shared"; 2 | 3 | export class AnalysisStore { 4 | private static _store?: AnalysisStore; 5 | 6 | private requests: Map; 7 | 8 | private analysisLookup: Map; 9 | 10 | private constructor() { 11 | this.requests = new Map(); 12 | this.analysisLookup = new Map(); 13 | } 14 | 15 | static get(): AnalysisStore { 16 | if (!AnalysisStore._store) { 17 | AnalysisStore._store = new AnalysisStore(); 18 | } 19 | 20 | return AnalysisStore._store; 21 | } 22 | 23 | getResults() { 24 | return [...this.requests.values()]; 25 | } 26 | 27 | getResultHash(templateId: string, userId: string): string { 28 | return `${templateId}-${userId}`; 29 | } 30 | 31 | resultExists(templateId: string, userId: string): boolean { 32 | return this.resultExistsByResultHash(this.getResultHash(templateId, userId)); 33 | } 34 | 35 | resultExistsByResultHash(resultHash: string): boolean { 36 | return this.analysisLookup.get(resultHash) === true; 37 | } 38 | 39 | addRequest(result: AnalysisRequestDTO) { 40 | let resultHash = this.getResultHash(result.templateId, result.userId); 41 | if (this.resultExistsByResultHash(resultHash)) { 42 | return; 43 | } 44 | this.requests.set(result.id, result); 45 | this.analysisLookup.set(resultHash, true); 46 | } 47 | 48 | deleteRequest(requestId: string) { 49 | let result = this.requests.get(requestId); 50 | if (!result) { 51 | return; 52 | } 53 | this.analysisLookup.delete(this.getResultHash(result.templateId, result.userId)); 54 | this.requests.delete(requestId); 55 | } 56 | 57 | clearRequests() { 58 | this.requests.clear(); 59 | this.analysisLookup.clear(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.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/frontend/src/repositories/roles.ts: -------------------------------------------------------------------------------- 1 | import { useSDK } from "@/plugins/sdk"; 2 | import type { RoleDTO } from "shared"; 3 | 4 | export const useRoleRepository = () => { 5 | const sdk = useSDK(); 6 | const getRoles = async () => { 7 | try { 8 | const roles = await sdk.backend.getRoles(); 9 | return { 10 | type: "Ok" as const, 11 | roles, 12 | }; 13 | } catch { 14 | return { 15 | type: "Err" as const, 16 | error: "Failed to get roles", 17 | }; 18 | } 19 | }; 20 | 21 | const addRole = async (name: string) => { 22 | try { 23 | const newRole = await sdk.backend.addRole(name); 24 | return { 25 | type: "Ok" as const, 26 | role: newRole, 27 | }; 28 | } catch { 29 | return { 30 | type: "Err" as const, 31 | error: "Failed to add role", 32 | }; 33 | } 34 | }; 35 | 36 | const updateRole = async (id: string, fields: Omit) => { 37 | try { 38 | const newRole = await sdk.backend.updateRole(id, fields); 39 | 40 | if (newRole) { 41 | return { 42 | type: "Ok" as const, 43 | role: newRole, 44 | }; 45 | } 46 | 47 | return { 48 | type: "Err" as const, 49 | error: "Role not found", 50 | }; 51 | } catch { 52 | return { 53 | type: "Err" as const, 54 | error: "Failed to update role", 55 | }; 56 | } 57 | }; 58 | 59 | const deleteRole = async (id: string) => { 60 | try { 61 | await sdk.backend.deleteRole(id); 62 | return { 63 | type: "Ok" as const, 64 | }; 65 | } catch { 66 | return { 67 | type: "Err" as const, 68 | error: "Failed to delete role", 69 | }; 70 | } 71 | }; 72 | 73 | return { 74 | getRoles, 75 | addRole, 76 | updateRole, 77 | deleteRole, 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /packages/frontend/src/repositories/users.ts: -------------------------------------------------------------------------------- 1 | import { useSDK } from "@/plugins/sdk"; 2 | import type { UserDTO } from "shared"; 3 | 4 | export const useUserRepository = () => { 5 | const sdk = useSDK(); 6 | const getUsers = async () => { 7 | try { 8 | const users = await sdk.backend.getUsers(); 9 | return { 10 | type: "Ok" as const, 11 | users, 12 | }; 13 | } catch { 14 | return { 15 | type: "Err" as const, 16 | error: "Failed to get users", 17 | }; 18 | } 19 | }; 20 | 21 | const addUser = async (name: string) => { 22 | try { 23 | const newUser = await sdk.backend.addUser(name); 24 | return { 25 | type: "Ok" as const, 26 | user: newUser, 27 | }; 28 | } catch { 29 | return { 30 | type: "Err" as const, 31 | error: "Failed to add user", 32 | }; 33 | } 34 | }; 35 | 36 | const updateUser = async (id: string, fields: Omit) => { 37 | try { 38 | const newUser = await sdk.backend.updateUser(id, fields); 39 | 40 | if (newUser) { 41 | return { 42 | type: "Ok" as const, 43 | user: newUser, 44 | }; 45 | } 46 | 47 | return { 48 | type: "Err" as const, 49 | error: "UserDTO not found", 50 | }; 51 | } catch { 52 | return { 53 | type: "Err" as const, 54 | error: "Failed to update user", 55 | }; 56 | } 57 | }; 58 | 59 | const deleteUser = async (id: string) => { 60 | try { 61 | await sdk.backend.deleteUser(id); 62 | return { 63 | type: "Ok" as const, 64 | }; 65 | } catch { 66 | return { 67 | type: "Err" as const, 68 | error: "Failed to delete user", 69 | }; 70 | } 71 | }; 72 | 73 | return { 74 | getUsers, 75 | addUser, 76 | updateUser, 77 | deleteUser, 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /packages/frontend/src/services/roles.ts: -------------------------------------------------------------------------------- 1 | import { useSDK } from "@/plugins/sdk"; 2 | import { useRoleRepository } from "@/repositories/roles"; 3 | import { useRoleStore } from "@/stores/roles"; 4 | import { defineStore } from "pinia"; 5 | import type { RoleDTO } from "shared"; 6 | 7 | export const useRoleService = defineStore("services.roles", () => { 8 | const sdk = useSDK(); 9 | const repository = useRoleRepository(); 10 | const store = useRoleStore(); 11 | 12 | const initialize = async () => { 13 | store.send({ type: "Start" }); 14 | const result = await repository.getRoles(); 15 | 16 | if (result.type === "Ok") { 17 | store.send({ type: "Success", roles: result.roles }); 18 | } else { 19 | store.send({ type: "Error", error: result.error }); 20 | } 21 | }; 22 | 23 | const addRole = async (name: string) => { 24 | const result = await repository.addRole(name); 25 | if (result.type === "Ok") { 26 | store.send({ type: "AddRole", role: result.role }); 27 | } else { 28 | sdk.window.showToast(result.error, { 29 | variant: "error", 30 | }); 31 | } 32 | }; 33 | 34 | const updateRole = async (id: string, fields: Omit) => { 35 | const result = await repository.updateRole(id, fields); 36 | 37 | if (result.type === "Ok") { 38 | store.send({ type: "UpdateRole", role: result.role }); 39 | } else { 40 | sdk.window.showToast(result.error, { 41 | variant: "error", 42 | }); 43 | } 44 | }; 45 | 46 | const deleteRole = async (id: string) => { 47 | const result = await repository.deleteRole(id); 48 | 49 | if (result.type === "Ok") { 50 | store.send({ type: "DeleteRole", id }); 51 | } else { 52 | sdk.window.showToast(result.error, { 53 | variant: "error", 54 | }); 55 | } 56 | }; 57 | 58 | const getState = () => store.getState(); 59 | 60 | return { 61 | initialize, 62 | getState, 63 | addRole, 64 | updateRole, 65 | deleteRole, 66 | }; 67 | }); 68 | -------------------------------------------------------------------------------- /packages/frontend/src/services/settings.ts: -------------------------------------------------------------------------------- 1 | import { useSDK } from "@/plugins/sdk"; 2 | import { useSettingsRepository } from "@/repositories/settings"; 3 | import { useSettingsStore } from "@/stores/settings"; 4 | import { defineStore } from "pinia"; 5 | 6 | export const useSettingsService = defineStore("services.settings", () => { 7 | const sdk = useSDK(); 8 | const repository = useSettingsRepository(); 9 | const store = useSettingsStore(); 10 | 11 | const getState = () => store.getState(); 12 | 13 | const initialize = async () => { 14 | store.send({ type: "Start" }); 15 | 16 | const result = await repository.getSettings(); 17 | 18 | if (result.type === "Ok") { 19 | store.send({ type: "Success", settings: result.settings }); 20 | } else { 21 | store.send({ type: "Error", error: result.error }); 22 | } 23 | }; 24 | 25 | const toggleAutoRunAnalysis = async () => { 26 | const currState = store.getState(); 27 | if (currState.type === "Success") { 28 | const result = await repository.updateSettings({ 29 | ...currState.settings, 30 | autoRunAnalysis: !currState.settings.autoRunAnalysis, 31 | }); 32 | 33 | if (result.type === "Ok") { 34 | store.send({ type: "UpdateSettings", settings: result.settings }); 35 | } else { 36 | sdk.window.showToast(result.error, { 37 | variant: "error", 38 | }); 39 | } 40 | } 41 | }; 42 | 43 | const setAutoCaptureRequests = async (value: "off" | "all" | "inScope") => { 44 | const currState = store.getState(); 45 | if (currState.type === "Success") { 46 | const result = await repository.updateSettings({ 47 | ...currState.settings, 48 | autoCaptureRequests: value, 49 | }); 50 | 51 | if (result.type === "Ok") { 52 | store.send({ type: "UpdateSettings", settings: result.settings }); 53 | } else { 54 | sdk.window.showToast(result.error, { 55 | variant: "error", 56 | }); 57 | } 58 | } 59 | }; 60 | 61 | return { 62 | getState, 63 | initialize, 64 | toggleAutoRunAnalysis, 65 | setAutoCaptureRequests, 66 | }; 67 | }); 68 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/RoleList/RoleTable.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 70 | -------------------------------------------------------------------------------- /caido.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@caido-community/dev'; 2 | import tailwindCaido from "@caido/tailwindcss"; 3 | import vue from '@vitejs/plugin-vue'; 4 | import path from "path"; 5 | import prefixwrap from "postcss-prefixwrap"; 6 | import tailwindcss from "tailwindcss"; 7 | import tailwindPrimeui from "tailwindcss-primeui"; 8 | 9 | export default defineConfig({ 10 | id: "authmatrix", 11 | name: "AuthMatrix", 12 | description: "Grid-based authorization testing across multiple users and roles.", 13 | version: "0.6.0", 14 | author: { 15 | name: "Caido Labs Inc.", 16 | email: "dev@caido.io", 17 | url: "https://caido.io", 18 | }, 19 | plugins: [ 20 | { 21 | kind: "backend", 22 | id: "backend", 23 | root: "packages/backend", 24 | }, 25 | { 26 | kind: 'frontend', 27 | id: "frontend", 28 | root: 'packages/frontend', 29 | backend: { 30 | id: "backend", 31 | }, 32 | vite: { 33 | plugins: [vue()], 34 | build: { 35 | rollupOptions: { 36 | external: ['@caido/frontend-sdk'] 37 | } 38 | }, 39 | resolve: { 40 | alias: [ 41 | { 42 | find: "@", 43 | replacement: path.resolve(__dirname, "packages/frontend/src"), 44 | }, 45 | ], 46 | }, 47 | css: { 48 | postcss: { 49 | plugins: [ 50 | prefixwrap("#plugin--authmatrix"), 51 | tailwindcss({ 52 | corePlugins: { 53 | preflight: false, 54 | }, 55 | content: [ 56 | './packages/frontend/src/**/*.{vue,ts}', 57 | './node_modules/@caido/primevue/dist/primevue.mjs' 58 | ], 59 | // Check the [data-mode="dark"] attribute on the element to determine the mode 60 | // This attribute is set in the Caido core application 61 | darkMode: ["selector", '[data-mode="dark"]'], 62 | plugins: [ 63 | 64 | // This plugin injects the necessary Tailwind classes for PrimeVue components 65 | tailwindPrimeui, 66 | 67 | // This plugin injects the necessary Tailwind classes for the Caido theme 68 | tailwindCaido, 69 | ], 70 | }) 71 | ] 72 | } 73 | } 74 | } 75 | } 76 | ] 77 | }); 78 | -------------------------------------------------------------------------------- /packages/frontend/src/views/App.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 79 | 80 | 85 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/UserList/UserTable.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 44 | 82 | -------------------------------------------------------------------------------- /packages/frontend/src/services/users.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "vue"; 2 | 3 | import { useSDK } from "@/plugins/sdk"; 4 | import { useUserRepository } from "@/repositories/users"; 5 | import { useUserStore } from "@/stores/users"; 6 | import { defineStore } from "pinia"; 7 | import type { UserDTO } from "shared"; 8 | 9 | export const useUserService = defineStore("services.users", () => { 10 | const sdk = useSDK(); 11 | const repository = useUserRepository(); 12 | const store = useUserStore(); 13 | 14 | const initialize = async () => { 15 | store.send({ type: "Start" }); 16 | 17 | const result = await repository.getUsers(); 18 | 19 | if (result.type === "Ok") { 20 | store.send({ type: "Success", users: result.users }); 21 | } else { 22 | store.send({ type: "Error", error: result.error }); 23 | } 24 | }; 25 | 26 | const getState = () => store.getState(); 27 | 28 | const addUser = async (name: string): Promise => { 29 | const result = await repository.addUser(name); 30 | 31 | if (result.type === "Ok") { 32 | store.send({ type: "AddUser", user: result.user }); 33 | return result.user; 34 | } else { 35 | sdk.window.showToast(result.error, { 36 | variant: "error", 37 | }); 38 | return undefined; 39 | } 40 | }; 41 | 42 | const updateUser = async (id: string, fields: Omit) => { 43 | const result = await repository.updateUser(id, fields); 44 | 45 | if (result.type === "Ok") { 46 | store.send({ type: "UpdateUser", user: result.user }); 47 | } else { 48 | sdk.window.showToast(result.error, { 49 | variant: "error", 50 | }); 51 | } 52 | }; 53 | 54 | const deleteUser = async (id: string) => { 55 | const result = await repository.deleteUser(id); 56 | 57 | if (result.type === "Ok") { 58 | store.send({ type: "DeleteUser", id }); 59 | } else { 60 | sdk.window.showToast(result.error, { 61 | variant: "error", 62 | }); 63 | } 64 | }; 65 | 66 | const userSelection = computed({ 67 | get: () => { 68 | const state = store.getState(); 69 | switch (state.type) { 70 | case "Success": 71 | return state.users.find((user) => user.id === state.selectedUserId); 72 | default: 73 | return undefined; 74 | } 75 | }, 76 | set: (user) => { 77 | store.send({ type: "SelectUser", id: user?.id }); 78 | }, 79 | }); 80 | 81 | return { 82 | initialize, 83 | getState, 84 | addUser, 85 | updateUser, 86 | deleteUser, 87 | userSelection, 88 | }; 89 | }); 90 | -------------------------------------------------------------------------------- /packages/frontend/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 71 | -------------------------------------------------------------------------------- /packages/backend/src/stores/templates.ts: -------------------------------------------------------------------------------- 1 | import type { RoleRuleDTO, TemplateDTO, UserRuleDTO } from "shared"; 2 | 3 | export class TemplateStore { 4 | private static _store?: TemplateStore; 5 | 6 | private templates: Map; 7 | 8 | private constructor() { 9 | this.templates = new Map(); 10 | } 11 | 12 | static get(): TemplateStore { 13 | if (!TemplateStore._store) { 14 | TemplateStore._store = new TemplateStore(); 15 | } 16 | 17 | return TemplateStore._store; 18 | } 19 | 20 | getTemplates() { 21 | return [...this.templates.values()]; 22 | } 23 | 24 | templateExists(id: string): boolean { 25 | return this.templates.get(id) !== undefined; 26 | } 27 | 28 | addTemplate(template: TemplateDTO) { 29 | this.templates.set(template.id, template); 30 | } 31 | 32 | updateTemplate(id: string, fields: Omit) { 33 | const template = this.templates.get(id); 34 | if (template) { 35 | Object.assign(template, fields); 36 | return template; 37 | } 38 | } 39 | 40 | deleteTemplate(templateId: string) { 41 | this.templates.delete(templateId); 42 | } 43 | 44 | toggleTemplateRole(templateId: string, roleId: string) { 45 | const template = this.templates.get(templateId); 46 | if (template) { 47 | const currRule = template.rules.find((rule) => { 48 | return rule.type === "RoleRule" && rule.roleId === roleId; 49 | }); 50 | 51 | if (currRule) { 52 | this.toggleRule(currRule); 53 | } else { 54 | template.rules.push({ 55 | type: "RoleRule", 56 | roleId, 57 | hasAccess: true, 58 | status: "Untested", 59 | }); 60 | } 61 | } 62 | 63 | return template; 64 | } 65 | 66 | toggleRule(currRule: RoleRuleDTO | UserRuleDTO) { 67 | currRule.hasAccess = !currRule.hasAccess; 68 | if (currRule.status === "Bypassed" && currRule.hasAccess) { 69 | currRule.status = "Enforced" 70 | } else if (currRule.status === "Enforced" && !currRule.hasAccess) { 71 | currRule.status = "Bypassed" 72 | } 73 | } 74 | 75 | toggleTemplateUser(templateId: string, userId: string) { 76 | const template = this.templates.get(templateId); 77 | if (template) { 78 | const currRule = template.rules.find((rule) => { 79 | return rule.type === "UserRule" && rule.userId === userId; 80 | }); 81 | 82 | if (currRule) { 83 | this.toggleRule(currRule); 84 | } else { 85 | template.rules.push({ 86 | type: "UserRule", 87 | userId, 88 | hasAccess: true, 89 | status: "Untested", 90 | }); 91 | } 92 | } 93 | 94 | return template; 95 | } 96 | 97 | clearTemplates() { 98 | this.templates.clear(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/frontend/src/stores/settings.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsState } from "@/types"; 2 | import { defineStore } from "pinia"; 3 | import type { SettingsDTO } from "shared"; 4 | import { reactive } from "vue"; 5 | 6 | type Context = { 7 | state: SettingsState; 8 | }; 9 | 10 | type Message = 11 | | { type: "Start" } 12 | | { type: "Error"; error: string } 13 | | { type: "Success"; settings: SettingsDTO } 14 | | { type: "UpdateSettings"; settings: SettingsDTO }; 15 | 16 | export const useSettingsStore = defineStore("stores.settings", () => { 17 | const context: Context = reactive({ 18 | state: { type: "Idle" }, 19 | }); 20 | 21 | const getState = () => context.state; 22 | 23 | const send = (message: Message) => { 24 | const currState = context.state; 25 | 26 | switch (currState.type) { 27 | case "Idle": 28 | context.state = processIdle(currState, message); 29 | break; 30 | case "Error": 31 | context.state = processError(currState, message); 32 | break; 33 | case "Success": 34 | context.state = processSuccess(currState, message); 35 | break; 36 | case "Loading": 37 | context.state = processLoading(currState, message); 38 | break; 39 | } 40 | }; 41 | 42 | return { getState, send }; 43 | }); 44 | 45 | const processIdle = ( 46 | state: SettingsState & { type: "Idle" }, 47 | message: Message, 48 | ): SettingsState => { 49 | switch (message.type) { 50 | case "Start": 51 | return { type: "Loading" }; 52 | case "Error": 53 | case "Success": 54 | case "UpdateSettings": 55 | return state; 56 | } 57 | }; 58 | 59 | const processError = ( 60 | state: SettingsState & { type: "Error" }, 61 | message: Message, 62 | ): SettingsState => { 63 | switch (message.type) { 64 | case "Start": 65 | return { type: "Loading" }; 66 | case "Error": 67 | case "Success": 68 | case "UpdateSettings": 69 | return state; 70 | } 71 | }; 72 | 73 | const processSuccess = ( 74 | state: SettingsState & { type: "Success" }, 75 | message: Message, 76 | ): SettingsState => { 77 | switch (message.type) { 78 | case "UpdateSettings": 79 | return { 80 | ...state, 81 | settings: message.settings, 82 | }; 83 | 84 | case "Start": 85 | case "Error": 86 | case "Success": 87 | return state; 88 | } 89 | }; 90 | 91 | const processLoading = ( 92 | state: SettingsState & { type: "Loading" }, 93 | message: Message, 94 | ): SettingsState => { 95 | switch (message.type) { 96 | case "Error": 97 | return { type: "Error", error: message.error }; 98 | case "Success": 99 | return { type: "Success", settings: message.settings }; 100 | case "Start": 101 | case "UpdateSettings": 102 | return state; 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/ResultTabs/Container.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 89 | -------------------------------------------------------------------------------- /packages/frontend/src/stores/analysis/useResultState.ts: -------------------------------------------------------------------------------- 1 | import type { AnalysisResultState } from "@/types"; 2 | import type { AnalysisRequestDTO } from "shared"; 3 | import { reactive } from "vue"; 4 | 5 | type Context = { 6 | state: AnalysisResultState; 7 | }; 8 | 9 | type Message = 10 | | { type: "Start" } 11 | | { type: "Error"; error: string } 12 | | { type: "Success"; results: AnalysisRequestDTO[] } 13 | | { type: "AddResult"; result: AnalysisRequestDTO } 14 | | { type: "Clear" }; 15 | 16 | export const useResultState = () => { 17 | const context: Context = reactive({ 18 | state: { type: "Idle" }, 19 | }); 20 | 21 | const getState = () => context.state; 22 | 23 | const send = (message: Message) => { 24 | const currState = context.state; 25 | 26 | switch (currState.type) { 27 | case "Idle": 28 | context.state = processIdle(currState, message); 29 | break; 30 | case "Loading": 31 | context.state = processLoading(currState, message); 32 | break; 33 | case "Error": 34 | context.state = processError(currState, message); 35 | break; 36 | case "Success": 37 | context.state = processSuccess(currState, message); 38 | break; 39 | } 40 | }; 41 | 42 | return { 43 | resultState: { 44 | getState, 45 | send, 46 | }, 47 | }; 48 | }; 49 | 50 | const processIdle = ( 51 | state: AnalysisResultState & { type: "Idle" }, 52 | message: Message, 53 | ): AnalysisResultState => { 54 | switch (message.type) { 55 | case "Start": 56 | return { type: "Loading" }; 57 | case "Error": 58 | case "Success": 59 | case "Clear": 60 | case "AddResult": 61 | return state; 62 | } 63 | }; 64 | 65 | const processLoading = ( 66 | state: AnalysisResultState & { type: "Loading" }, 67 | message: Message, 68 | ): AnalysisResultState => { 69 | switch (message.type) { 70 | case "Error": 71 | return { type: "Error", error: message.error }; 72 | case "Success": 73 | return { type: "Success", results: message.results }; 74 | case "Start": 75 | case "Clear": 76 | case "AddResult": 77 | return state; 78 | } 79 | }; 80 | 81 | const processError = ( 82 | state: AnalysisResultState & { type: "Error" }, 83 | message: Message, 84 | ): AnalysisResultState => { 85 | switch (message.type) { 86 | case "Start": 87 | return { type: "Loading" }; 88 | case "Error": 89 | case "Success": 90 | case "Clear": 91 | case "AddResult": 92 | return state; 93 | } 94 | }; 95 | 96 | const processSuccess = ( 97 | state: AnalysisResultState & { type: "Success" }, 98 | message: Message, 99 | ): AnalysisResultState => { 100 | switch (message.type) { 101 | case "AddResult": 102 | return { 103 | type: "Success", 104 | results: [...state.results, message.result], 105 | }; 106 | case "Clear": 107 | return { 108 | type: "Success", 109 | results: [], 110 | }; 111 | case "Start": 112 | case "Error": 113 | case "Success": 114 | return state; 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /packages/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { DefineAPI, SDK } from "caido:plugin"; 2 | import { 3 | getRequestResponse, 4 | getResults, 5 | runAnalysis, 6 | } from "./services/analysis"; 7 | import { addRole, deleteRole, getRoles, updateRole } from "./services/roles"; 8 | import { getSettings, updateSettings } from "./services/settings"; 9 | import { 10 | addTemplate, 11 | clearTemplates, 12 | deleteTemplate, 13 | getTemplates, 14 | registerTemplateEvents, 15 | addTemplateFromContext, 16 | toggleTemplateRole, 17 | toggleTemplateUser, 18 | updateTemplate, 19 | } from "./services/templates"; 20 | import { addUser, deleteUser, getUsers, updateUser } from "./services/users"; 21 | 22 | export type { BackendEvents } from "./types"; 23 | 24 | export type API = DefineAPI<{ 25 | // Role endpoints 26 | getRoles: typeof getRoles; 27 | addRole: typeof addRole; 28 | updateRole: typeof updateRole; 29 | deleteRole: typeof deleteRole; 30 | 31 | // User endpoints 32 | getUsers: typeof getUsers; 33 | addUser: typeof addUser; 34 | updateUser: typeof updateUser; 35 | deleteUser: typeof deleteUser; 36 | 37 | // Template endpoints 38 | getTemplates: typeof getTemplates; 39 | addTemplate: typeof addTemplate; 40 | updateTemplate: typeof updateTemplate; 41 | deleteTemplate: typeof deleteTemplate; 42 | clearTemplates: typeof clearTemplates; 43 | toggleTemplateRole: typeof toggleTemplateRole; 44 | toggleTemplateUser: typeof toggleTemplateUser; 45 | addTemplateFromContext: typeof addTemplateFromContext; 46 | 47 | // Settings endpoints 48 | getSettings: typeof getSettings; 49 | updateSettings: typeof updateSettings; 50 | 51 | // Analysis endpoints 52 | runAnalysis: typeof runAnalysis; 53 | getResults: typeof getResults; 54 | getRequestResponse: typeof getRequestResponse; 55 | }>; 56 | 57 | export function init(sdk: SDK) { 58 | // Role endpoints 59 | sdk.api.register("getRoles", getRoles); 60 | sdk.api.register("addRole", addRole); 61 | sdk.api.register("updateRole", updateRole); 62 | sdk.api.register("deleteRole", deleteRole); 63 | 64 | // User endpoints 65 | sdk.api.register("getUsers", getUsers); 66 | sdk.api.register("addUser", addUser); 67 | sdk.api.register("updateUser", updateUser); 68 | sdk.api.register("deleteUser", deleteUser); 69 | 70 | // Template endpoints 71 | sdk.api.register("getTemplates", getTemplates); 72 | sdk.api.register("addTemplate", addTemplate); 73 | sdk.api.register("updateTemplate", updateTemplate); 74 | sdk.api.register("deleteTemplate", deleteTemplate); 75 | sdk.api.register("toggleTemplateRole", toggleTemplateRole); 76 | sdk.api.register("toggleTemplateUser", toggleTemplateUser); 77 | sdk.api.register("clearTemplates", clearTemplates); 78 | sdk.api.register("addTemplateFromContext", addTemplateFromContext); 79 | 80 | // Settings endpoints 81 | sdk.api.register("getSettings", getSettings); 82 | sdk.api.register("updateSettings", updateSettings); 83 | 84 | // Analysis function 85 | sdk.api.register("runAnalysis", runAnalysis); 86 | sdk.api.register("getResults", getResults); 87 | sdk.api.register("getRequestResponse", getRequestResponse); 88 | 89 | // Events 90 | registerTemplateEvents(sdk); 91 | } 92 | -------------------------------------------------------------------------------- /packages/frontend/src/stores/roles.ts: -------------------------------------------------------------------------------- 1 | import type { RoleState } from "@/types"; 2 | import { defineStore } from "pinia"; 3 | import type { RoleDTO } from "shared"; 4 | import { reactive } from "vue"; 5 | 6 | type Context = { 7 | state: RoleState; 8 | }; 9 | 10 | type Message = 11 | | { type: "Start" } 12 | | { type: "Error"; error: string } 13 | | { type: "Success"; roles: RoleDTO[] } 14 | | { type: "AddRole"; role: RoleDTO } 15 | | { type: "UpdateRole"; role: RoleDTO } 16 | | { type: "DeleteRole"; id: string }; 17 | 18 | export const useRoleStore = defineStore("stores.roles", () => { 19 | const context: Context = reactive({ 20 | state: { type: "Idle" }, 21 | }); 22 | 23 | const getState = () => context.state; 24 | 25 | const send = (message: Message) => { 26 | const currState = context.state; 27 | 28 | switch (currState.type) { 29 | case "Idle": 30 | context.state = processIdle(currState, message); 31 | break; 32 | case "Error": 33 | context.state = processError(currState, message); 34 | break; 35 | case "Success": 36 | context.state = processSuccess(currState, message); 37 | break; 38 | case "Loading": 39 | context.state = processLoading(currState, message); 40 | break; 41 | } 42 | }; 43 | 44 | return { getState, send }; 45 | }); 46 | 47 | const processIdle = ( 48 | state: RoleState & { type: "Idle" }, 49 | message: Message, 50 | ): RoleState => { 51 | switch (message.type) { 52 | case "Start": 53 | return { type: "Loading" }; 54 | case "Error": 55 | case "Success": 56 | case "AddRole": 57 | case "UpdateRole": 58 | case "DeleteRole": 59 | return state; 60 | } 61 | }; 62 | 63 | const processError = ( 64 | state: RoleState & { type: "Error" }, 65 | message: Message, 66 | ): RoleState => { 67 | switch (message.type) { 68 | case "Start": 69 | return { type: "Loading" }; 70 | case "Error": 71 | case "Success": 72 | case "AddRole": 73 | case "UpdateRole": 74 | case "DeleteRole": 75 | return state; 76 | } 77 | }; 78 | 79 | const processSuccess = ( 80 | state: RoleState & { type: "Success" }, 81 | message: Message, 82 | ): RoleState => { 83 | switch (message.type) { 84 | case "AddRole": 85 | return { 86 | ...state, 87 | roles: [...state.roles, message.role], 88 | }; 89 | case "UpdateRole": 90 | return { 91 | ...state, 92 | roles: state.roles.map((role) => 93 | role.id === message.role.id ? message.role : role, 94 | ), 95 | }; 96 | case "DeleteRole": 97 | return { 98 | ...state, 99 | roles: state.roles.filter((role) => role.id !== message.id), 100 | }; 101 | 102 | case "Start": 103 | case "Error": 104 | case "Success": 105 | return state; 106 | } 107 | }; 108 | 109 | const processLoading = ( 110 | state: RoleState & { type: "Loading" }, 111 | message: Message, 112 | ): RoleState => { 113 | switch (message.type) { 114 | case "Error": 115 | return { type: "Error", error: message.error }; 116 | case "Success": 117 | return { type: "Success", roles: message.roles }; 118 | case "Start": 119 | case "AddRole": 120 | case "UpdateRole": 121 | case "DeleteRole": 122 | return state; 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /packages/frontend/src/components/users-roles/UserShow/AttributeTable.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 117 | -------------------------------------------------------------------------------- /packages/frontend/src/stores/analysis/useSelectionState.ts: -------------------------------------------------------------------------------- 1 | import type { AnalysisSelectionState } from "@/types"; 2 | import { reactive } from "vue"; 3 | 4 | type Context = { 5 | state: AnalysisSelectionState; 6 | }; 7 | 8 | type Message = 9 | | { type: "Reset" } 10 | | { type: "Start"; templateId: string; userId: string | undefined } 11 | | { 12 | type: "Error"; 13 | templateId: string; 14 | userId: string | undefined; 15 | error: string; 16 | } 17 | | { 18 | type: "Success"; 19 | templateId: string; 20 | userId: string | undefined; 21 | request: { id: string; raw: string }; 22 | response: { id: string; raw: string } | undefined; 23 | }; 24 | 25 | export const useSelectionState = () => { 26 | const context: Context = reactive({ 27 | state: { type: "None" }, 28 | }); 29 | 30 | const getState = () => context.state; 31 | 32 | const send = (message: Message) => { 33 | const currState = context.state; 34 | 35 | switch (currState.type) { 36 | case "None": 37 | context.state = processIdle(currState, message); 38 | break; 39 | case "Loading": 40 | context.state = processLoading(currState, message); 41 | break; 42 | case "Error": 43 | context.state = processError(currState, message); 44 | break; 45 | case "Success": 46 | context.state = processSuccess(currState, message); 47 | break; 48 | } 49 | }; 50 | 51 | return { 52 | selectionState: { 53 | getState, 54 | send, 55 | }, 56 | }; 57 | }; 58 | 59 | const processIdle = ( 60 | state: AnalysisSelectionState & { type: "None" }, 61 | message: Message, 62 | ): AnalysisSelectionState => { 63 | switch (message.type) { 64 | case "Start": 65 | return { 66 | type: "Loading", 67 | templateId: message.templateId, 68 | userId: message.userId, 69 | }; 70 | case "Reset": 71 | case "Error": 72 | case "Success": 73 | return state; 74 | } 75 | }; 76 | 77 | const processLoading = ( 78 | state: AnalysisSelectionState & { type: "Loading" }, 79 | message: Message, 80 | ): AnalysisSelectionState => { 81 | switch (message.type) { 82 | case "Error": 83 | return { 84 | type: "Error", 85 | templateId: message.templateId, 86 | userId: message.userId, 87 | }; 88 | case "Success": 89 | return { 90 | type: "Success", 91 | templateId: message.templateId, 92 | userId: message.userId, 93 | request: message.request, 94 | response: message.response, 95 | }; 96 | case "Reset": 97 | return { type: "None" }; 98 | case "Start": 99 | return state; 100 | } 101 | }; 102 | 103 | const processError = ( 104 | state: AnalysisSelectionState & { type: "Error" }, 105 | message: Message, 106 | ): AnalysisSelectionState => { 107 | switch (message.type) { 108 | case "Start": 109 | return { 110 | type: "Loading", 111 | templateId: message.templateId, 112 | userId: message.userId, 113 | }; 114 | case "Reset": 115 | return { type: "None" }; 116 | case "Error": 117 | case "Success": 118 | return state; 119 | } 120 | }; 121 | 122 | const processSuccess = ( 123 | state: AnalysisSelectionState & { type: "Success" }, 124 | message: Message, 125 | ): AnalysisSelectionState => { 126 | switch (message.type) { 127 | case "Start": 128 | return { 129 | type: "Loading", 130 | templateId: message.templateId, 131 | userId: message.userId, 132 | }; 133 | case "Reset": 134 | return { type: "None" }; 135 | case "Error": 136 | case "Success": 137 | return state; 138 | } 139 | }; 140 | -------------------------------------------------------------------------------- /packages/frontend/src/repositories/template.ts: -------------------------------------------------------------------------------- 1 | import { useSDK } from "@/plugins/sdk"; 2 | import type { TemplateDTO } from "shared"; 3 | 4 | export const useTemplateRepository = () => { 5 | const sdk = useSDK(); 6 | 7 | const getTemplates = async () => { 8 | try { 9 | const templates = await sdk.backend.getTemplates(); 10 | return { 11 | type: "Ok" as const, 12 | templates, 13 | }; 14 | } catch { 15 | return { 16 | type: "Err" as const, 17 | error: "Failed to get templates", 18 | }; 19 | } 20 | }; 21 | 22 | const toggleTemplateRole = async (templateId: string, roleId: string) => { 23 | try { 24 | const newTemplate = await sdk.backend.toggleTemplateRole( 25 | templateId, 26 | roleId, 27 | ); 28 | 29 | if (newTemplate) { 30 | return { 31 | type: "Ok" as const, 32 | template: newTemplate, 33 | }; 34 | } 35 | 36 | return { 37 | type: "Err" as const, 38 | error: "Template or role not found", 39 | }; 40 | } catch { 41 | return { 42 | type: "Err" as const, 43 | error: "Failed to update template", 44 | }; 45 | } 46 | }; 47 | 48 | const toggleTemplateUser = async (templateId: string, userId: string) => { 49 | try { 50 | const newTemplate = await sdk.backend.toggleTemplateUser( 51 | templateId, 52 | userId, 53 | ); 54 | 55 | if (newTemplate) { 56 | return { 57 | type: "Ok" as const, 58 | template: newTemplate, 59 | }; 60 | } 61 | 62 | return { 63 | type: "Err" as const, 64 | error: "TemplateDTO or user not found", 65 | }; 66 | } catch { 67 | return { 68 | type: "Err" as const, 69 | error: "Failed to update template", 70 | }; 71 | } 72 | }; 73 | 74 | const addTemplate = async () => { 75 | try { 76 | const newTemplate = await sdk.backend.addTemplate(); 77 | return { 78 | type: "Ok" as const, 79 | template: newTemplate, 80 | }; 81 | } catch { 82 | return { 83 | type: "Err" as const, 84 | error: "Failed to add template", 85 | }; 86 | } 87 | }; 88 | 89 | const deleteTemplate = async (id: string) => { 90 | try { 91 | await sdk.backend.deleteTemplate(id); 92 | return { 93 | type: "Ok" as const, 94 | }; 95 | } catch { 96 | return { 97 | type: "Err" as const, 98 | error: "Failed to delete template", 99 | }; 100 | } 101 | }; 102 | 103 | const updateTemplate = async ( 104 | id: string, 105 | fields: Omit, 106 | ) => { 107 | try { 108 | const newTemplate = await sdk.backend.updateTemplate(id, fields); 109 | if (newTemplate) { 110 | return { 111 | type: "Ok" as const, 112 | template: newTemplate, 113 | }; 114 | } 115 | 116 | return { 117 | type: "Err" as const, 118 | error: "TemplateDTO not found", 119 | }; 120 | } catch { 121 | return { 122 | type: "Err" as const, 123 | error: "Failed to update template", 124 | }; 125 | } 126 | }; 127 | 128 | const clearTemplates = async () => { 129 | try { 130 | await sdk.backend.clearTemplates(); 131 | return { 132 | type: "Ok" as const, 133 | }; 134 | } catch { 135 | return { 136 | type: "Err" as const, 137 | error: "Failed to clear templates", 138 | }; 139 | } 140 | }; 141 | 142 | return { 143 | getTemplates, 144 | toggleTemplateRole, 145 | toggleTemplateUser, 146 | addTemplate, 147 | updateTemplate, 148 | deleteTemplate, 149 | clearTemplates, 150 | }; 151 | }; 152 | -------------------------------------------------------------------------------- /packages/frontend/src/repositories/templates.ts: -------------------------------------------------------------------------------- 1 | import { useSDK } from "@/plugins/sdk"; 2 | import type { TemplateDTO } from "shared"; 3 | 4 | export const useTemplateRepository = () => { 5 | const sdk = useSDK(); 6 | 7 | const getTemplates = async () => { 8 | try { 9 | const templates = await sdk.backend.getTemplates(); 10 | return { 11 | type: "Ok" as const, 12 | templates, 13 | }; 14 | } catch { 15 | return { 16 | type: "Err" as const, 17 | error: "Failed to get templates", 18 | }; 19 | } 20 | }; 21 | 22 | const toggleTemplateRole = async (templateId: string, roleId: string) => { 23 | try { 24 | const newTemplate = await sdk.backend.toggleTemplateRole( 25 | templateId, 26 | roleId, 27 | ); 28 | 29 | if (newTemplate) { 30 | return { 31 | type: "Ok" as const, 32 | template: newTemplate, 33 | }; 34 | } 35 | 36 | return { 37 | type: "Err" as const, 38 | error: "Template or role not found", 39 | }; 40 | } catch { 41 | return { 42 | type: "Err" as const, 43 | error: "Failed to update template", 44 | }; 45 | } 46 | }; 47 | 48 | const toggleTemplateUser = async (templateId: string, userId: string) => { 49 | try { 50 | const newTemplate = await sdk.backend.toggleTemplateUser( 51 | templateId, 52 | userId, 53 | ); 54 | 55 | if (newTemplate) { 56 | return { 57 | type: "Ok" as const, 58 | template: newTemplate, 59 | }; 60 | } 61 | 62 | return { 63 | type: "Err" as const, 64 | error: "TemplateDTO or user not found", 65 | }; 66 | } catch { 67 | return { 68 | type: "Err" as const, 69 | error: "Failed to update template", 70 | }; 71 | } 72 | }; 73 | 74 | const addTemplate = async () => { 75 | try { 76 | const newTemplate = await sdk.backend.addTemplate(); 77 | return { 78 | type: "Ok" as const, 79 | template: newTemplate, 80 | }; 81 | } catch { 82 | return { 83 | type: "Err" as const, 84 | error: "Failed to add template", 85 | }; 86 | } 87 | }; 88 | 89 | const deleteTemplate = async (id: string) => { 90 | try { 91 | await sdk.backend.deleteTemplate(id); 92 | return { 93 | type: "Ok" as const, 94 | }; 95 | } catch { 96 | return { 97 | type: "Err" as const, 98 | error: "Failed to delete template", 99 | }; 100 | } 101 | }; 102 | 103 | const updateTemplate = async ( 104 | id: string, 105 | fields: Omit, 106 | ) => { 107 | try { 108 | const newTemplate = await sdk.backend.updateTemplate(id, fields); 109 | if (newTemplate) { 110 | return { 111 | type: "Ok" as const, 112 | template: newTemplate, 113 | }; 114 | } 115 | 116 | return { 117 | type: "Err" as const, 118 | error: "TemplateDTO not found", 119 | }; 120 | } catch { 121 | return { 122 | type: "Err" as const, 123 | error: "Failed to update template", 124 | }; 125 | } 126 | }; 127 | 128 | const clearTemplates = async () => { 129 | try { 130 | await sdk.backend.clearTemplates(); 131 | return { 132 | type: "Ok" as const, 133 | }; 134 | } catch { 135 | return { 136 | type: "Err" as const, 137 | error: "Failed to clear templates", 138 | }; 139 | } 140 | }; 141 | 142 | return { 143 | getTemplates, 144 | toggleTemplateRole, 145 | toggleTemplateUser, 146 | addTemplate, 147 | updateTemplate, 148 | deleteTemplate, 149 | clearTemplates, 150 | }; 151 | }; 152 | -------------------------------------------------------------------------------- /packages/frontend/src/stores/templates.ts: -------------------------------------------------------------------------------- 1 | import type { TemplateState } from "@/types"; 2 | import { defineStore } from "pinia"; 3 | import type { TemplateDTO } from "shared"; 4 | import { reactive } from "vue"; 5 | 6 | type Context = { 7 | state: TemplateState; 8 | }; 9 | 10 | type Message = 11 | | { type: "Start" } 12 | | { type: "Error"; error: string } 13 | | { type: "Success"; templates: TemplateDTO[] } 14 | | { type: "AddTemplate"; template: TemplateDTO } 15 | | { type: "UpdateTemplate"; template: TemplateDTO } 16 | | { type: "DeleteTemplate"; id: string } 17 | | { type: "ClearTemplates" }; 18 | 19 | export const useTemplateStore = defineStore("stores.templates", () => { 20 | const context: Context = reactive({ 21 | state: { type: "Idle" }, 22 | }); 23 | 24 | const getState = () => context.state; 25 | 26 | const send = (message: Message) => { 27 | const currState = context.state; 28 | 29 | switch (currState.type) { 30 | case "Idle": 31 | context.state = processIdle(currState, message); 32 | break; 33 | case "Error": 34 | context.state = processError(currState, message); 35 | break; 36 | case "Success": 37 | context.state = processSuccess(currState, message); 38 | break; 39 | case "Loading": 40 | context.state = processLoading(currState, message); 41 | break; 42 | } 43 | }; 44 | 45 | return { getState, send }; 46 | }); 47 | 48 | const processIdle = ( 49 | state: TemplateState & { type: "Idle" }, 50 | message: Message, 51 | ): TemplateState => { 52 | switch (message.type) { 53 | case "Start": 54 | return { type: "Loading" }; 55 | case "Error": 56 | case "Success": 57 | case "AddTemplate": 58 | case "UpdateTemplate": 59 | case "DeleteTemplate": 60 | case "ClearTemplates": 61 | return state; 62 | } 63 | }; 64 | 65 | const processError = ( 66 | state: TemplateState & { type: "Error" }, 67 | message: Message, 68 | ): TemplateState => { 69 | switch (message.type) { 70 | case "Start": 71 | return { type: "Loading" }; 72 | case "Error": 73 | case "Success": 74 | case "AddTemplate": 75 | case "UpdateTemplate": 76 | case "DeleteTemplate": 77 | case "ClearTemplates": 78 | return state; 79 | } 80 | }; 81 | 82 | const processSuccess = ( 83 | state: TemplateState & { type: "Success" }, 84 | message: Message, 85 | ): TemplateState => { 86 | switch (message.type) { 87 | case "AddTemplate": 88 | if ( 89 | state.templates.some((template) => template.id === message.template.id) 90 | ) { 91 | return state; 92 | } 93 | 94 | return { 95 | ...state, 96 | templates: [...state.templates, message.template], 97 | }; 98 | case "UpdateTemplate": 99 | return { 100 | ...state, 101 | templates: state.templates.map((template) => 102 | template.id === message.template.id ? message.template : template, 103 | ), 104 | }; 105 | case "DeleteTemplate": 106 | return { 107 | ...state, 108 | templates: state.templates.filter( 109 | (template) => template.id !== message.id, 110 | ), 111 | }; 112 | case "ClearTemplates": 113 | return { 114 | ...state, 115 | templates: [], 116 | }; 117 | 118 | case "Start": 119 | case "Error": 120 | case "Success": 121 | return state; 122 | } 123 | }; 124 | 125 | const processLoading = ( 126 | state: TemplateState & { type: "Loading" }, 127 | message: Message, 128 | ): TemplateState => { 129 | switch (message.type) { 130 | case "Error": 131 | return { type: "Error", error: message.error }; 132 | case "Success": 133 | return { type: "Success", templates: message.templates }; 134 | case "Start": 135 | case "AddTemplate": 136 | case "UpdateTemplate": 137 | case "DeleteTemplate": 138 | case "ClearTemplates": 139 | return state; 140 | } 141 | }; 142 | -------------------------------------------------------------------------------- /packages/frontend/src/stores/users.ts: -------------------------------------------------------------------------------- 1 | import type { UserState } from "@/types"; 2 | import { defineStore } from "pinia"; 3 | import type { UserDTO } from "shared"; 4 | import { reactive } from "vue"; 5 | 6 | type Context = { 7 | state: UserState; 8 | }; 9 | 10 | type Message = 11 | | { type: "Start" } 12 | | { type: "Error"; error: string } 13 | | { type: "Success"; users: UserDTO[] } 14 | | { type: "AddUser"; user: UserDTO } 15 | | { type: "UpdateUser"; user: UserDTO } 16 | | { type: "DeleteUser"; id: string } 17 | | { type: "SelectUser"; id: string | undefined }; 18 | 19 | export const useUserStore = defineStore("stores.users", () => { 20 | const context: Context = reactive({ 21 | state: { type: "Idle" }, 22 | }); 23 | 24 | const getState = () => context.state; 25 | 26 | const send = (message: Message) => { 27 | const currState = context.state; 28 | 29 | switch (currState.type) { 30 | case "Idle": 31 | context.state = processIdle(currState, message); 32 | break; 33 | case "Error": 34 | context.state = processError(currState, message); 35 | break; 36 | case "Success": 37 | context.state = processSuccess(currState, message); 38 | break; 39 | case "Loading": 40 | context.state = processLoading(currState, message); 41 | break; 42 | } 43 | }; 44 | 45 | return { getState, send }; 46 | }); 47 | 48 | const processIdle = ( 49 | state: UserState & { type: "Idle" }, 50 | message: Message, 51 | ): UserState => { 52 | switch (message.type) { 53 | case "Start": 54 | return { type: "Loading" }; 55 | case "Error": 56 | case "Success": 57 | case "AddUser": 58 | case "UpdateUser": 59 | case "DeleteUser": 60 | case "SelectUser": 61 | return state; 62 | } 63 | }; 64 | 65 | const processError = ( 66 | state: UserState & { type: "Error" }, 67 | message: Message, 68 | ): UserState => { 69 | switch (message.type) { 70 | case "Start": 71 | return { type: "Loading" }; 72 | case "Error": 73 | case "Success": 74 | case "AddUser": 75 | case "UpdateUser": 76 | case "DeleteUser": 77 | case "SelectUser": 78 | return state; 79 | } 80 | }; 81 | 82 | const processSuccess = ( 83 | state: UserState & { type: "Success" }, 84 | message: Message, 85 | ): UserState => { 86 | switch (message.type) { 87 | case "AddUser": { 88 | const selectedUserId = state.selectedUserId ?? message.user.id; 89 | return { 90 | ...state, 91 | selectedUserId, 92 | users: [...state.users, message.user], 93 | }; 94 | } 95 | case "UpdateUser": 96 | return { 97 | ...state, 98 | users: state.users.map((user) => 99 | user.id === message.user.id ? message.user : user, 100 | ), 101 | }; 102 | case "DeleteUser": { 103 | const selectedUserId = 104 | message.id === state.selectedUserId ? undefined : state.selectedUserId; 105 | return { 106 | ...state, 107 | selectedUserId, 108 | users: state.users.filter((user) => user.id !== message.id), 109 | }; 110 | } 111 | 112 | case "SelectUser": 113 | return { 114 | ...state, 115 | selectedUserId: message.id, 116 | }; 117 | 118 | case "Start": 119 | case "Error": 120 | case "Success": 121 | return state; 122 | } 123 | }; 124 | 125 | const processLoading = ( 126 | state: UserState & { type: "Loading" }, 127 | message: Message, 128 | ): UserState => { 129 | switch (message.type) { 130 | case "Error": 131 | return { type: "Error", error: message.error }; 132 | case "Success": 133 | return { 134 | type: "Success", 135 | users: message.users, 136 | selectedUserId: message.users[0]?.id, 137 | }; 138 | case "Start": 139 | case "AddUser": 140 | case "UpdateUser": 141 | case "DeleteUser": 142 | case "SelectUser": 143 | return state; 144 | } 145 | }; 146 | -------------------------------------------------------------------------------- /packages/frontend/src/services/analysis.ts: -------------------------------------------------------------------------------- 1 | import { useSDK } from "@/plugins/sdk"; 2 | import { useAnalysisRepository } from "@/repositories/analysis"; 3 | import { useAnalysisStore } from "@/stores/analysis"; 4 | import { useTemplateStore } from "@/stores/templates"; 5 | import { defineStore } from "pinia"; 6 | import { computed } from "vue"; 7 | 8 | export const useAnalysisService = defineStore("services.analysis", () => { 9 | const sdk = useSDK(); 10 | const store = useAnalysisStore(); 11 | const templateStore = useTemplateStore(); 12 | const repository = useAnalysisRepository(); 13 | 14 | const runAnalysis = async () => { 15 | store.jobState.send({ type: "Start" }); 16 | const result = await repository.runAnalysis(); 17 | 18 | if (result.type === "Ok") { 19 | store.jobState.send({ type: "Done" }); 20 | } else { 21 | sdk.window.showToast(result.error, { 22 | variant: "error", 23 | }); 24 | } 25 | }; 26 | 27 | const initialize = async () => { 28 | store.resultState.send({ type: "Start" }); 29 | const result = await repository.getAnalysisResults(); 30 | 31 | if (result.type === "Ok") { 32 | store.resultState.send({ type: "Success", results: result.results }); 33 | } else { 34 | store.resultState.send({ type: "Error", error: result.error }); 35 | } 36 | 37 | sdk.backend.onEvent("results:created", (result) => { 38 | store.resultState.send({ type: "AddResult", result }); 39 | }); 40 | 41 | sdk.backend.onEvent("results:clear", () => { 42 | store.resultState.send({ type: "Clear" }); 43 | }); 44 | }; 45 | 46 | const selectResult = async (templateId?: string, userId?: string) => { 47 | const resultState = store.resultState.getState(); 48 | const templateState = templateStore.getState(); 49 | 50 | if (resultState.type !== "Success" || templateState.type !== "Success") 51 | return; 52 | 53 | if (!templateId) { 54 | store.selectionState.send({ type: "Reset" }); 55 | return; 56 | } 57 | 58 | const analysisResult = resultState.results.find( 59 | (r) => r.templateId === templateId && r.userId === userId, 60 | ); 61 | 62 | if (analysisResult) { 63 | store.selectionState.send({ type: "Start", templateId, userId }); 64 | const result = await repository.getRequestResponse( 65 | analysisResult.requestId, 66 | ); 67 | 68 | if (result.type === "Ok") { 69 | store.selectionState.send({ 70 | type: "Success", 71 | templateId, 72 | userId, 73 | request: result.request, 74 | response: result.response, 75 | }); 76 | } else { 77 | store.selectionState.send({ 78 | type: "Error", 79 | templateId, 80 | userId, 81 | error: result.error, 82 | }); 83 | } 84 | 85 | return; 86 | } 87 | 88 | const template = templateState.templates.find((t) => t.id === templateId); 89 | if (template && !userId) { 90 | store.selectionState.send({ type: "Start", templateId, userId }); 91 | const result = await repository.getRequestResponse(template.requestId); 92 | 93 | if (result.type === "Ok") { 94 | store.selectionState.send({ 95 | type: "Success", 96 | templateId, 97 | userId, 98 | request: result.request, 99 | response: result.response, 100 | }); 101 | } else { 102 | store.selectionState.send({ 103 | type: "Error", 104 | templateId, 105 | userId, 106 | error: result.error, 107 | }); 108 | } 109 | } 110 | }; 111 | 112 | const selectionState = computed(() => store.selectionState.getState()); 113 | const jobState = computed(() => store.jobState.getState()); 114 | const resultState = computed(() => store.resultState.getState()); 115 | 116 | return { 117 | initialize, 118 | runAnalysis, 119 | selectResult, 120 | selectionState, 121 | resultState, 122 | jobState, 123 | }; 124 | }); 125 | -------------------------------------------------------------------------------- /packages/frontend/src/services/templates.ts: -------------------------------------------------------------------------------- 1 | import { useSDK } from "@/plugins/sdk"; 2 | import { useTemplateRepository } from "@/repositories/template"; 3 | import { useAnalysisStore } from "@/stores/analysis"; 4 | import { useTemplateStore } from "@/stores/templates"; 5 | import { defineStore } from "pinia"; 6 | import type { TemplateDTO } from "shared"; 7 | 8 | export const useTemplateService = defineStore("services.templates", () => { 9 | const sdk = useSDK(); 10 | const repository = useTemplateRepository(); 11 | const store = useTemplateStore(); 12 | 13 | const toggleTemplateRole = async (templateId: string, roleId: string) => { 14 | const result = await repository.toggleTemplateRole(templateId, roleId); 15 | 16 | if (result.type === "Ok") { 17 | store.send({ type: "UpdateTemplate", template: result.template }); 18 | } else { 19 | sdk.window.showToast(result.error, { 20 | variant: "error", 21 | }); 22 | } 23 | }; 24 | 25 | const toggleTemplateUser = async (templateId: string, userId: string) => { 26 | const result = await repository.toggleTemplateUser(templateId, userId); 27 | 28 | if (result.type === "Ok") { 29 | store.send({ type: "UpdateTemplate", template: result.template }); 30 | } else { 31 | sdk.window.showToast(result.error, { 32 | variant: "error", 33 | }); 34 | } 35 | }; 36 | 37 | const addTemplate = async () => { 38 | const result = await repository.addTemplate(); 39 | 40 | if (result.type === "Ok") { 41 | store.send({ type: "AddTemplate", template: result.template }); 42 | } else { 43 | sdk.window.showToast(result.error, { 44 | variant: "error", 45 | }); 46 | } 47 | }; 48 | 49 | const updateTemplate = async ( 50 | id: string, 51 | fields: Omit, 52 | ) => { 53 | const result = await repository.updateTemplate(id, fields); 54 | 55 | if (result.type === "Ok") { 56 | store.send({ type: "UpdateTemplate", template: result.template }); 57 | } else { 58 | sdk.window.showToast(result.error, { 59 | variant: "error", 60 | }); 61 | } 62 | }; 63 | 64 | const analysisStore = useAnalysisStore(); 65 | const deleteTemplate = async (id: string) => { 66 | const result = await repository.deleteTemplate(id); 67 | 68 | if (result.type === "Ok") { 69 | store.send({ type: "DeleteTemplate", id }); 70 | const analysisState = analysisStore.selectionState.getState(); 71 | 72 | if (analysisState.type !== "None" && analysisState.templateId === id) { 73 | analysisStore.selectionState.send({ type: "Reset" }); 74 | } 75 | } else { 76 | sdk.window.showToast(result.error, { 77 | variant: "error", 78 | }); 79 | } 80 | }; 81 | 82 | const clearTemplates = async () => { 83 | const result = await repository.clearTemplates(); 84 | 85 | if (result.type === "Ok") { 86 | store.send({ type: "ClearTemplates" }); 87 | analysisStore.selectionState.send({ type: "Reset" }); 88 | } else { 89 | sdk.window.showToast(result.error, { 90 | variant: "error", 91 | }); 92 | } 93 | }; 94 | 95 | const initialize = async () => { 96 | store.send({ type: "Start" }); 97 | 98 | const result = await repository.getTemplates(); 99 | 100 | if (result.type === "Ok") { 101 | store.send({ type: "Success", templates: result.templates }); 102 | } else { 103 | store.send({ type: "Error", error: result.error }); 104 | } 105 | 106 | sdk.backend.onEvent("templates:created", (template) => { 107 | store.send({ type: "AddTemplate", template }); 108 | }); 109 | 110 | sdk.backend.onEvent("templates:updated", (template) => { 111 | store.send({ type: "UpdateTemplate", template }); 112 | }); 113 | 114 | sdk.backend.onEvent("templates:cleared", () => { 115 | store.send({ type: "ClearTemplates" }); 116 | analysisStore.selectionState.send({ type: "Reset" }); 117 | }); 118 | }; 119 | 120 | const getState = () => store.getState(); 121 | 122 | return { 123 | getState, 124 | initialize, 125 | toggleTemplateRole, 126 | toggleTemplateUser, 127 | addTemplate, 128 | updateTemplate, 129 | deleteTemplate, 130 | clearTemplates, 131 | }; 132 | }); 133 | -------------------------------------------------------------------------------- /packages/backend/src/services/templates.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | import type { ID, Request, Response } from "caido:utils"; 3 | import type { TemplateDTO } from "shared"; 4 | 5 | import { TemplateStore } from "../stores/templates"; 6 | import { generateID, sha256Hash } from "../utils"; 7 | 8 | import { SettingsStore } from "../stores/settings"; 9 | import type { BackendEvents } from "../types"; 10 | import { runAnalysis } from "./analysis"; 11 | 12 | export const getTemplates = (_sdk: SDK): TemplateDTO[] => { 13 | const store = TemplateStore.get(); 14 | return store.getTemplates(); 15 | }; 16 | 17 | export const addTemplate = (sdk: SDK) => { 18 | const newTemplate: TemplateDTO = { 19 | id: generateID(), 20 | requestId: generateID(), 21 | authSuccessRegex: "HTTP/1[.]1 200", 22 | rules: [], 23 | meta: { 24 | host: "localhost", 25 | port: 10134, 26 | path: "/", 27 | isTls: false, 28 | method: "GET", 29 | }, 30 | }; 31 | 32 | const store = TemplateStore.get(); 33 | store.addTemplate(newTemplate); 34 | sdk.api.send("templates:created", newTemplate); 35 | 36 | return newTemplate; 37 | }; 38 | 39 | export const deleteTemplate = (_sdk: SDK, requestId: string) => { 40 | const store = TemplateStore.get(); 41 | store.deleteTemplate(requestId); 42 | }; 43 | 44 | export const updateTemplate = ( 45 | sdk: SDK, 46 | id: string, 47 | fields: Omit, 48 | ) => { 49 | const store = TemplateStore.get(); 50 | const newTemplate = store.updateTemplate(id, fields); 51 | sdk.api.send("templates:updated", newTemplate); 52 | return newTemplate; 53 | }; 54 | 55 | export const toggleTemplateRole = ( 56 | sdk: SDK, 57 | requestId: string, 58 | roleId: string, 59 | ) => { 60 | const store = TemplateStore.get(); 61 | const newTemplate = store.toggleTemplateRole(requestId, roleId); 62 | sdk.api.send("templates:updated", newTemplate); 63 | return newTemplate; 64 | }; 65 | 66 | export const toggleTemplateUser = ( 67 | sdk: SDK, 68 | requestId: string, 69 | userId: string, 70 | ) => { 71 | const store = TemplateStore.get(); 72 | const newTemplate = store.toggleTemplateUser(requestId, userId); 73 | sdk.api.send("templates:updated", newTemplate); 74 | return newTemplate; 75 | }; 76 | 77 | export const clearTemplates = (sdk: SDK) => { 78 | const store = TemplateStore.get(); 79 | store.clearTemplates(); 80 | sdk.api.send("templates:cleared"); 81 | }; 82 | 83 | export const onInterceptResponse = async ( 84 | sdk: SDK, 85 | request: Request, 86 | response: Response, 87 | ) => { 88 | const settingsStore = SettingsStore.get(); 89 | const settings = settingsStore.getSettings(); 90 | const store = TemplateStore.get(); 91 | 92 | if (settings.autoCaptureRequests == "off") { 93 | return; 94 | } 95 | 96 | if (!sdk.requests.matches(settings.defaultFilterHTTPQL, request)) { 97 | sdk.console.log(`Filtering: ${request.getUrl()}`) 98 | return; 99 | } 100 | 101 | const templateId = generateTemplateId(request, settings.deDuplicateHeaders); 102 | if (store.templateExists(templateId)) { 103 | return 104 | } 105 | 106 | switch (settings.autoCaptureRequests) { 107 | case "all": { 108 | const template = toTemplate(request, response, templateId); 109 | store.addTemplate(template); 110 | sdk.api.send("templates:created", template); 111 | break; 112 | } 113 | case "inScope": { 114 | if (sdk.requests.inScope(request)) { 115 | const template = toTemplate(request, response, templateId); 116 | store.addTemplate(template); 117 | sdk.api.send("templates:created", template); 118 | } 119 | break; 120 | } 121 | } 122 | if (settings.autoRunAnalysis) { 123 | runAnalysis(sdk); 124 | } 125 | }; 126 | 127 | export const addTemplateFromContext = async ( 128 | sdk: SDK, 129 | request_id: ID, 130 | ) => { 131 | const settingsStore = SettingsStore.get(); 132 | const settings = settingsStore.getSettings(); 133 | const store = TemplateStore.get(); 134 | 135 | const result = await sdk.requests.get(request_id.toString()) 136 | if (!result) { 137 | return; 138 | } 139 | const request = result.request; 140 | const response = result.response; 141 | if(!request || !response) { 142 | return; 143 | } 144 | 145 | const templateId = generateTemplateId(request, settings.deDuplicateHeaders); 146 | if (store.templateExists(templateId)) { 147 | return 148 | } 149 | 150 | const template = toTemplate(request, response, templateId); 151 | store.addTemplate(template); 152 | sdk.api.send("templates:created", template); 153 | } 154 | 155 | export const registerTemplateEvents = (sdk: SDK) => { 156 | sdk.events.onInterceptResponse(onInterceptResponse); 157 | }; 158 | 159 | 160 | const generateTemplateId = (request: Request, dedupeHeaders: string[] = []): string => { 161 | let body = request.getBody()?.toText(); 162 | if (!body) { 163 | body = ""; 164 | } 165 | const bodyHash = sha256Hash(body); 166 | let dedupe = `${request.getMethod}~${request.getUrl()}~${bodyHash}`; 167 | dedupeHeaders.forEach((h) => { 168 | dedupe += `~${request.getHeader(h)?.join("~")}` 169 | }) 170 | return sha256Hash(dedupe) 171 | } 172 | 173 | const toTemplate = (request: Request, response: Response, templateId: string = generateTemplateId(request)): TemplateDTO => { 174 | return { 175 | id: templateId, 176 | requestId: request.getId(), 177 | authSuccessRegex: `HTTP/1[.]1 ${response.getCode()}`, 178 | rules: [], 179 | meta: { 180 | host: request.getHost(), 181 | port: request.getPort(), 182 | method: request.getMethod(), 183 | isTls: request.getTls(), 184 | path: request.getPath(), 185 | }, 186 | }; 187 | }; 188 | -------------------------------------------------------------------------------- /packages/backend/src/services/analysis.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "caido:plugin"; 2 | import type { RequestSpec } from "caido:utils"; 3 | 4 | import { TemplateStore } from "../stores/templates"; 5 | import { UserStore } from "../stores/users"; 6 | 7 | import type { 8 | AnalysisRequestDTO, 9 | RuleStatusDTO, 10 | TemplateDTO, 11 | UserAttributeDTO, 12 | UserDTO, 13 | } from "shared"; 14 | import { AnalysisStore } from "../stores/analysis"; 15 | import { Uint8ArrayToString, isPresent } from "../utils"; 16 | 17 | import { RoleStore } from "../stores/roles"; 18 | import type { BackendEvents } from "../types"; 19 | 20 | export const getResults = (_sdk: SDK): AnalysisRequestDTO[] => { 21 | const store = AnalysisStore.get(); 22 | return store.getResults(); 23 | }; 24 | 25 | export const getRequestResponse = async (sdk: SDK, requestId: string) => { 26 | const result = await sdk.requests.get(requestId); 27 | 28 | if (!result) { 29 | return { type: "Err" as const, message: "Request not found" }; 30 | } 31 | 32 | const { request, response } = result; 33 | 34 | return { 35 | type: "Ok" as const, 36 | request: { 37 | id: request.getId(), 38 | raw: Uint8ArrayToString(request.toSpecRaw().getRaw()), 39 | }, 40 | response: response 41 | ? { 42 | id: response.getId(), 43 | raw: response.getRaw().toText(), 44 | } 45 | : undefined, 46 | }; 47 | }; 48 | 49 | export const runAnalysis = async (sdk: SDK) => { 50 | const templateStore = TemplateStore.get(); 51 | const userStore = UserStore.get(); 52 | const roleStore = RoleStore.get(); 53 | const analysisStore = AnalysisStore.get(); 54 | 55 | // Clear current results 56 | analysisStore.clearRequests(); 57 | sdk.api.send("results:clear"); 58 | 59 | // Send requests 60 | const templates = templateStore.getTemplates(); 61 | const users = userStore.getUsers(); 62 | 63 | sdk.console.debug( 64 | `Analyzing ${templates.length} templates with ${users.length} users`, 65 | ); 66 | 67 | for (const template of templates) { 68 | // Run each template async 69 | (async () => { 70 | for (const user of users) { 71 | if (analysisStore.resultExists(template.id, user.id)) { 72 | continue; 73 | } 74 | const analysisRequest = await sendRequest(sdk, template, user); 75 | if (analysisRequest) { 76 | analysisStore.addRequest(analysisRequest); 77 | sdk.api.send("results:created", analysisRequest); 78 | } 79 | } 80 | 81 | const newRules: TemplateDTO["rules"] = []; 82 | const roles = roleStore.getRoles(); 83 | const rolePromises = roles.map(async (role) => { 84 | const currentRule = template.rules.find( 85 | (rule) => rule.type === "RoleRule" && rule.roleId === role.id, 86 | ) ?? { 87 | type: "RoleRule", 88 | roleId: role.id, 89 | hasAccess: false, 90 | status: "Untested", 91 | }; 92 | 93 | const status = await generateRoleRuleStatus(sdk, template, role.id); 94 | return { ...currentRule, status }; 95 | }); 96 | 97 | const userPromises = users.map(async (user) => { 98 | const currentRule = template.rules.find( 99 | (rule) => rule.type === "UserRule" && rule.userId === user.id, 100 | ) ?? { 101 | type: "UserRule", 102 | userId: user.id, 103 | hasAccess: false, 104 | status: "Untested", 105 | }; 106 | 107 | const status = await generateUserRuleStatus(sdk, template, user); 108 | return { ...currentRule, status }; 109 | }); 110 | 111 | const roleResults = await Promise.all(rolePromises); 112 | const userResults = await Promise.all(userPromises); 113 | 114 | // Combine results 115 | newRules.push(...roleResults, ...userResults); 116 | 117 | template.rules = newRules; 118 | templateStore.updateTemplate(template.id, template); 119 | sdk.api.send("templates:updated", template); 120 | })() 121 | } 122 | }; 123 | 124 | const sendRequest = async (sdk: SDK, template: TemplateDTO, user: UserDTO) => { 125 | const { request: baseRequest } = 126 | (await sdk.requests.get(template.requestId)) ?? {}; 127 | 128 | if (!baseRequest) { 129 | sdk.console.error(`Request not found for template ${template.id}`); 130 | return; 131 | } 132 | 133 | const spec = baseRequest.toSpec(); 134 | setCookies(spec, user.attributes); 135 | setHeaders(spec, user.attributes); 136 | 137 | const { request } = await sdk.requests.send(spec); 138 | 139 | const requestId = request.getId(); 140 | const analysisRequest: AnalysisRequestDTO = { 141 | id: `${template.id}-${user.id}-${requestId}`, 142 | templateId: template.id, 143 | userId: user.id, 144 | requestId, 145 | }; 146 | 147 | return analysisRequest; 148 | }; 149 | 150 | const setCookies = (spec: RequestSpec, attributes: UserAttributeDTO[]) => { 151 | const newCookies = attributes.filter((attr) => attr.kind === "Cookie"); 152 | if (newCookies.length === 0) { 153 | return spec; 154 | } 155 | 156 | // Set cookies 157 | const cookies: Record = {}; 158 | const cookieString = spec.getHeader("Cookie")?.join("; ") ?? ""; 159 | 160 | for (const cookie of cookieString.split("; ")) { 161 | const [name, value] = cookie.split("=", 2); 162 | if (typeof name === "string" && typeof value === "string") { 163 | cookies[name] = value; 164 | } 165 | } 166 | 167 | for (const newCookie of newCookies) { 168 | cookies[newCookie.name] = newCookie.value; 169 | } 170 | 171 | const newCookieString = Object.entries(cookies) 172 | .map(([name, value]) => { 173 | return `${name}=${value}`; 174 | }) 175 | .join("; "); 176 | 177 | spec.setHeader("Cookie", newCookieString); 178 | 179 | return spec; 180 | }; 181 | 182 | const setHeaders = (spec: RequestSpec, attributes: UserAttributeDTO[]) => { 183 | const newHeaders = attributes.filter((attr) => attr.kind === "Header"); 184 | 185 | // Set headers 186 | for (const newHeader of newHeaders) { 187 | spec.setHeader(newHeader.name, newHeader.value); 188 | } 189 | 190 | return spec; 191 | }; 192 | 193 | const generateRoleRuleStatus = async ( 194 | sdk: SDK, 195 | template: TemplateDTO, 196 | roleId: string, 197 | ): Promise => { 198 | const store = AnalysisStore.get(); 199 | const userStore = UserStore.get(); 200 | 201 | // Get all users with the role 202 | const users = userStore 203 | .getUsers() 204 | .filter((user) => user.roleIds.includes(roleId)); 205 | 206 | // Get all results for this template and matching users 207 | const results = store.getResults(); 208 | const templateResults = results.filter((result) => { 209 | return ( 210 | result.templateId === template.id && 211 | users.some((user) => user.id === result.userId) 212 | ); 213 | }); 214 | 215 | // Get the rule for the role 216 | const rule = template.rules.find( 217 | (rule) => rule.type === "RoleRule" && rule.roleId === roleId, 218 | ) ?? { 219 | type: "RoleRule", 220 | roleId, 221 | hasAccess: false, 222 | status: "Untested", 223 | }; 224 | 225 | // Get all result responses 226 | const responses = await Promise.all( 227 | templateResults.map(async (result) => { 228 | const { response } = (await sdk.requests.get(result.requestId)) ?? {}; 229 | return response; 230 | }), 231 | ); 232 | 233 | // If any response is not found, return "Unexpected" 234 | const filteredResponses = responses.filter(isPresent); 235 | if (filteredResponses.length !== responses.length) { 236 | return "Unexpected"; 237 | } 238 | 239 | // Get all responses access state 240 | const regex = new RegExp(template.authSuccessRegex); 241 | const accessStates = filteredResponses.map((response) => { 242 | return regex.test(response.getRaw().toText()); 243 | }); 244 | 245 | // If all access states match the rule, return "Enforced" 246 | if (accessStates.every((hasAccess) => hasAccess === rule.hasAccess)) { 247 | return "Enforced"; 248 | } 249 | 250 | // If any access state is false, but the rule is true, return "Unexpected" 251 | if (rule.hasAccess && accessStates.some((hasAccess) => !hasAccess)) { 252 | return "Unexpected"; 253 | } 254 | 255 | // If any access state is true, but the rule is false, return "Bypassed" 256 | if (!rule.hasAccess && accessStates.some((hasAccess) => hasAccess)) { 257 | return "Bypassed"; 258 | } 259 | 260 | return "Unexpected"; 261 | }; 262 | 263 | const generateUserRuleStatus = async ( 264 | sdk: SDK, 265 | template: TemplateDTO, 266 | user: UserDTO, 267 | ): Promise => { 268 | const store = AnalysisStore.get(); 269 | 270 | // Get all results for this template and matching user 271 | const results = store.getResults().filter((result) => { 272 | return result.templateId === template.id && result.userId === user.id; 273 | }); 274 | 275 | // Check if user should have access to this resource 276 | const hasAccess = template.rules.some((rule) => { 277 | if (rule.type === "RoleRule") { 278 | return rule.hasAccess && user.roleIds.includes(rule.roleId); 279 | } 280 | 281 | return rule.hasAccess && rule.userId === user.id; 282 | }); 283 | 284 | // Get all result responses 285 | const responses = await Promise.all( 286 | results.map(async (result) => { 287 | const { response } = (await sdk.requests.get(result.requestId)) ?? {}; 288 | return response; 289 | }), 290 | ); 291 | 292 | // If any response is not found, return "Unexpected" 293 | const filteredResponses = responses.filter(isPresent); 294 | if (filteredResponses.length !== responses.length) { 295 | return "Unexpected"; 296 | } 297 | 298 | // Get all responses access state 299 | const regex = new RegExp(template.authSuccessRegex); 300 | const accessStates = filteredResponses.map((response) => { 301 | return regex.test(response.getRaw().toText()); 302 | }); 303 | 304 | // If all access states match the rule, return "Enforced" 305 | if (accessStates.every((resultAccess) => resultAccess === hasAccess)) { 306 | return "Enforced"; 307 | } 308 | 309 | // If any access state is false, but the rule is true, return "Unexpected" 310 | if (hasAccess && accessStates.some((hasAccess) => !hasAccess)) { 311 | return "Unexpected"; 312 | } 313 | 314 | // If any access state is true, but the rule is false, return "Bypassed" 315 | if (!hasAccess && accessStates.some((hasAccess) => hasAccess)) { 316 | return "Bypassed"; 317 | } 318 | 319 | return "Unexpected"; 320 | }; 321 | -------------------------------------------------------------------------------- /packages/frontend/src/components/dashboard/TemplateTable/Container.vue: -------------------------------------------------------------------------------- 1 | 149 | 328 | --------------------------------------------------------------------------------