Skip to content

Commit eae0f1b

Browse files
authored
Merge pull request #5 from caido-community/dev
V1.0.2
2 parents d58b87e + 8f85f01 commit eae0f1b

108 files changed

Lines changed: 12747 additions & 7737 deletions

File tree

Some content is hidden

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

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
workflow_dispatch:
9+
10+
jobs:
11+
check:
12+
name: Verification
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout Repository
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Tools (mise)
19+
uses: jdx/mise-action@v2
20+
21+
- name: Install Dependencies
22+
run: pnpm install --frozen-lockfile
23+
24+
- name: Lint
25+
run: pnpm lint
26+
27+
- name: Typecheck
28+
run: pnpm typecheck
29+
30+
- name: Knip (Dead Code)
31+
run: pnpm knip

.mise/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[tools]
2+
node = "22"
3+
pnpm = "9"

caido.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default defineConfig({
1212
id,
1313
name: "GraphQL Analyzer",
1414
description: "Plugin for GraphQL schema discovery, visualization, and advanced security",
15-
version: "1.0.1",
15+
version: "1.0.2",
1616
author: {
1717
name: "Amr Elsagaei",
1818
email: "info@amrelsagaei.com",

eslint.config.mjs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import { defaultConfig } from "@caido/eslint-config";
2+
import globals from "globals";
23

3-
/** @type {import('eslint').Linter.Config } */
44
export default [
5-
...defaultConfig(),
6-
]
5+
...defaultConfig({
6+
compat: false,
7+
}),
8+
{
9+
files: ["packages/frontend/**/*.{ts,vue}"],
10+
languageOptions: {
11+
globals: {
12+
...globals.browser,
13+
},
14+
},
15+
},
16+
];

knip.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { RawConfigurationOrFn } from "knip/dist/types/config.js";
2+
3+
const config: RawConfigurationOrFn = {
4+
workspaces: {
5+
".": {
6+
entry: ["caido.config.ts"],
7+
},
8+
"packages/backend": {
9+
project: ["src/**/*.ts"],
10+
ignoreDependencies: ["caido"],
11+
},
12+
"packages/frontend": {
13+
entry: ["src/index.ts"],
14+
project: ["src/**/*.{ts,tsx,vue}"],
15+
},
16+
},
17+
};
18+
19+
export default config;
20+

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"private": true,
55
"scripts": {
66
"typecheck": "pnpm -r typecheck",
7-
"lint": "eslint ./packages/**/src --fix",
7+
"lint": "eslint --fix packages/*/src",
8+
"knip": "knip",
89
"build": "caido-dev build",
910
"watch": "caido-dev watch"
1011
},
@@ -13,6 +14,9 @@
1314
"@caido/eslint-config": "^0.5.0",
1415
"@caido/tailwindcss": "0.0.1",
1516
"@vitejs/plugin-vue": "5.2.1",
17+
"eslint": "^9.36.0",
18+
"globals": "^16.5.0",
19+
"knip": "^5.73.4",
1620
"postcss-prefixwrap": "1.51.0",
1721
"tailwindcss": "3.4.13",
1822
"tailwindcss-primeui": "0.3.4",

packages/backend/ARCHITECTURE.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Backend Architecture
2+
3+
## Overview
4+
5+
The GraphQL Analyzer backend follows a layered architecture with clear separation of concerns:
6+
7+
```
8+
┌──────────────────────────────────────┐
9+
│ API Layer │ ← External interface
10+
├──────────────────────────────────────┤
11+
│ Services Layer │ ← Business logic
12+
├──────────────────────────────────────┤
13+
│ Models │ Validation │ ← Domain objects & schemas
14+
└──────────────────────────────────────┘
15+
```
16+
17+
## Directory Structure
18+
19+
```
20+
packages/backend/src/
21+
├── api/ # API handlers (thin layer)
22+
│ ├── attacks.ts # Attack execution APIs
23+
│ └── graphql.ts # GraphQL endpoint APIs
24+
├── models/ # Domain models
25+
│ └── AttackSession.ts # Discriminated union state types
26+
├── services/ # Business logic
27+
│ ├── attacks/ # Attack modules
28+
│ │ ├── AttackService.ts # Main orchestrator
29+
│ │ ├── IntrospectionAttack.ts # Schema introspection
30+
│ │ ├── DepthAttack.ts # Query depth limits
31+
│ │ ├── ComplexityAttack.ts # Query complexity
32+
│ │ ├── BatchAttack.ts # Batch queries
33+
│ │ ├── FieldSuggestionAttack.ts # Field suggestion disclosure
34+
│ │ ├── headerUtils.ts # Header utilities (SDK getHeaders)
35+
│ │ ├── sessionManager.ts # Attack session state
36+
│ │ └── types.ts # Shared attack types
37+
│ └── graphql/
38+
│ └── GraphQLService.ts # GraphQL introspection service
39+
├── validation/ # Zod schemas
40+
│ └── schemas.ts # JSON response validation
41+
├── index.ts # Main entry point
42+
├── sdk.ts # SDK singleton
43+
└── types.ts # Backend event types
44+
```
45+
46+
## Key Design Patterns
47+
48+
### Discriminated Unions
49+
50+
Attack session states use discriminated unions for type safety:
51+
52+
```typescript
53+
type AttackSessionState =
54+
| AttackSessionRunning // status: "running"
55+
| AttackSessionCompleted // status: "completed"
56+
| AttackSessionFailed // status: "failed"
57+
| AttackSessionCancelled; // status: "cancelled"
58+
```
59+
60+
Benefits:
61+
62+
- No optional fields
63+
- Type narrowing on status
64+
- Cleaner state transitions
65+
66+
### Zod Validation
67+
68+
JSON responses are validated using Zod schemas instead of type casting:
69+
70+
```typescript
71+
const parsed = parseGraphQLResponse(responseBody);
72+
if (parsed.kind === "Ok" && parsed.value.data) {
73+
// Type-safe access to validated data
74+
}
75+
```
76+
77+
### SDK Headers API
78+
79+
Uses the SDK's `getHeaders()` method instead of manually parsing raw requests:
80+
81+
```typescript
82+
const rawHeaders = result.request.getHeaders();
83+
for (const [name, values] of Object.entries(rawHeaders)) {
84+
headers[name] = values[0] ?? "";
85+
}
86+
```
87+
88+
## Data Flow
89+
90+
```
91+
1. API Request (index.ts)
92+
93+
2. API Handler (api/attacks.ts)
94+
95+
3. Attack Service (services/attacks/AttackService.ts)
96+
97+
4. Individual Attack Module (e.g., IntrospectionAttack.ts)
98+
99+
5. Response Validation (validation/schemas.ts)
100+
101+
6. Result returned
102+
```

packages/backend/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
"scripts": {
77
"typecheck": "tsc --noEmit"
88
},
9+
"dependencies": {
10+
"shared": "workspace:*",
11+
"zod": "4.3.6"
12+
},
913
"devDependencies": {
1014
"@caido/sdk-backend": "^0.51.0"
1115
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { SDK } from "caido:plugin";
2+
import type { AttackConfig, AttackResult, AttackType, Result } from "shared";
3+
4+
import {
5+
cancelAttackSession as cancelSession,
6+
executeAttacks as executeAttacksService,
7+
getAttackStatus as getStatus,
8+
generateAttackTemplates as getTemplates,
9+
startAttacksAsync as startAsync,
10+
} from "../services/attacks";
11+
12+
export async function executeGraphQLAttacks(
13+
sdk: SDK,
14+
config: AttackConfig,
15+
): Promise<Result<AttackResult[]>> {
16+
return executeAttacksService(sdk, config);
17+
}
18+
19+
export function startGraphQLAttacks(
20+
sdk: SDK,
21+
config: AttackConfig,
22+
): Promise<Result<string>> {
23+
return Promise.resolve(startAsync(sdk, config));
24+
}
25+
26+
export function getAttackStatus(
27+
_sdk: SDK,
28+
sessionId: string,
29+
): Promise<
30+
Result<{
31+
status: string;
32+
progress: number;
33+
results: AttackResult[];
34+
totalAttacks: number;
35+
completedAttacks: number;
36+
isComplete: boolean;
37+
}>
38+
> {
39+
return Promise.resolve(getStatus(sessionId));
40+
}
41+
42+
export function cancelAttackSession(
43+
_sdk: SDK,
44+
sessionId: string,
45+
): Promise<Result<void>> {
46+
return Promise.resolve(cancelSession(sessionId));
47+
}
48+
49+
export function getAttackTemplates(
50+
_sdk: SDK,
51+
): Record<AttackType, { name: string; description: string; query: string }> {
52+
return getTemplates();
53+
}
54+
55+
export async function createCaidoFinding(
56+
sdk: SDK,
57+
findingData: { title: string; description: string },
58+
requestId: string,
59+
): Promise<Result<void>> {
60+
try {
61+
if (!requestId) {
62+
return { kind: "Error", error: "No request ID provided" };
63+
}
64+
65+
const result = await sdk.requests.get(requestId);
66+
if (!result) {
67+
return { kind: "Error", error: "Request not found in Caido storage" };
68+
}
69+
70+
let dedupeKey = `graphql-${findingData.title}`;
71+
try {
72+
dedupeKey += `-${result.request.getUrl()}`;
73+
} catch {
74+
dedupeKey += `-${Date.now()}`;
75+
}
76+
77+
await sdk.findings.create({
78+
title: findingData.title,
79+
description: findingData.description,
80+
reporter: "GraphQL Analyzer",
81+
request: result.request,
82+
dedupeKey: dedupeKey,
83+
});
84+
85+
return { kind: "Ok", value: undefined };
86+
} catch (error) {
87+
return {
88+
kind: "Error",
89+
error: `Failed to create Caido finding: ${error instanceof Error ? error.message : String(error)}`,
90+
};
91+
}
92+
}

0 commit comments

Comments
 (0)