Compare commits

...
7 changed files with 559 additions and 4 deletions
+1
View File
@@ -39,6 +39,7 @@ Requires Node.js 22+.
| Example | Description | Concepts |
|---------|-------------|----------|
| [code-review-bot](./code-review-bot) | AI code reviewer that reads git diffs and produces structured comments. | Multiple tools, `completesRun` lifecycle, `systemPrompt`, zod schemas |
| [security-review-bot](./security-review-bot) | AI application security reviewer that reads git diffs and produces structured security findings. | Specialized agent prompts, multiple tools, security schemas, `completesRun` lifecycle |
| [multi-agent](./multi-agent) | Web app that fans out to three specialist agents in parallel, streams results via SSE, then synthesizes a unified answer. | Concurrent agents, `Promise.all`, per-agent `subscribe()`, SSE streaming, agent composition |
### Advanced
@@ -167,7 +167,20 @@ fn push_notification(
}
fn resolve_sidecar_script(workspace_root: &str, launch_cwd: &str) -> Option<PathBuf> {
let candidates = [
sidecar_script_candidates(workspace_root, launch_cwd)
.into_iter()
.find(|p| p.exists())
}
fn sidecar_script_candidates(workspace_root: &str, launch_cwd: &str) -> Vec<PathBuf> {
vec![
PathBuf::from(workspace_root)
.join("sdk")
.join("apps")
.join("examples")
.join("menubar")
.join("sidecar")
.join("index.ts"),
PathBuf::from(workspace_root)
.join("sdk")
.join("apps")
@@ -182,8 +195,7 @@ fn resolve_sidecar_script(workspace_root: &str, launch_cwd: &str) -> Option<Path
.join("sidecar")
.join("index.ts"),
PathBuf::from(launch_cwd).join("sidecar").join("index.ts"),
];
candidates.into_iter().find(|p| p.exists())
]
}
fn sidecar_binary_name() -> String {
@@ -261,8 +273,13 @@ fn start_sidecar(
.current_dir(workspace_root);
c
} else {
let searched_paths = sidecar_script_candidates(workspace_root, launch_cwd)
.into_iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"menubar sidecar not found under workspace_root={workspace_root}"
"menubar sidecar not found under workspace_root={workspace_root}; searched: {searched_paths}"
));
};
@@ -0,0 +1,135 @@
# Security Review Bot on git diffs
An AI-powered application security review agent that reads a git diff, analyzes it for security risk, and produces structured findings with severity, category, CWE/OWASP metadata, exploit scenarios, remediation guidance, and confidence levels.
This example is based on the `code-review-bot` example, but uses the `ClineCore` runtime instead of the lightweight `Agent` runtime. That means it can use ClineCore's built-in workspace tools, including `read_files` and `search_codebase`, while still adding custom security review tools through `extraTools`.
## Getting started
Install dependencies:
```bash
bun install
bun run build:sdk
```
Set an API key:
```bash
export CLINE_API_KEY="cline_..."
```
Security review the last commit:
```bash
bun dev
```
Security review against a specific ref:
```bash
bun dev main
bun dev HEAD~5
bun dev abc123
```
Add run-specific review instructions with `--prompt` or `-p`:
```bash
bun dev main --prompt "Focus on authorization bypasses and SSRF"
bun dev HEAD~1 -p "Treat test-only secrets as low severity"
```
You can also pass extra positional text after the ref; it will be appended as additional review instructions:
```bash
bun dev main "Prioritize auth, secrets, and unsafe file access"
```
## Review a specific repo
The bot reviews the git repository in the current working directory. To review a different repo, run the example command from that repo's directory and pass the ref you want to compare against.
Using Bun directly from another repo:
```bash
cd /path/to/repo-you-want-to-review
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev main
```
With extra instructions:
```bash
cd /path/to/repo-you-want-to-review
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev main --prompt "Focus on payment and admin flows"
```
Or build the example once and run the compiled JavaScript from any git repo:
```bash
cd /path/to/cline/sdk/apps/examples/security-review-bot
bun run build
cd /path/to/repo-you-want-to-review
node /path/to/cline/sdk/apps/examples/security-review-bot/dist/index.js main
```
The argument is passed to `git diff <ref>` in the target repo. Common examples:
```bash
# Review all changes on your branch compared with main
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev main
# Review the last commit
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev HEAD~1
# Review changes since a release tag
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev v1.2.3
```
If you want to review uncommitted changes in a target repo, compare against the branch or commit you started from, for example:
```bash
cd /path/to/repo-you-want-to-review
bun --cwd /path/to/cline/sdk/apps/examples/security-review-bot dev main
```
## What it does
1. Reads a `git diff` against the specified ref (defaults to `HEAD~1`)
2. Starts a local `ClineCore` session with built-in tools enabled
3. Lets the model use ClineCore's `read_files` and `search_codebase` tools for surrounding file context
4. Adds two custom security review tools through `extraTools`:
- `add_security_finding` - records a structured security finding with severity, category, exploit scenario, remediation, and confidence
- `submit_security_review` - a completion tool that ends the run with a summary, overall risk rating, and merge-blocking decision
5. Prints findings grouped by severity (`critical`, `high`, `medium`, `low`, `info`)
## Finding schema
Each finding includes:
- `file` and `line` - where the issue appears
- `severity` - `critical`, `high`, `medium`, `low`, or `info`
- `category` - security category such as `injection`, `authorization`, `secrets`, `ssrf`, `xss`, or `cryptography`
- `cwe` and `owasp` - optional vulnerability taxonomy metadata
- `title` and `description` - concise summary and risk explanation
- `exploitScenario` - realistic abuse case
- `remediation` - concrete fix or mitigation
- `confidence` - `high`, `medium`, or `low`
## Concepts demonstrated
- Using `ClineCore.create()` and `cline.start()` for a local runtime session
- Enabling selected ClineCore built-in tools, including `read_files` and `search_codebase`
- Adding domain-specific custom tools with `extraTools`
- `lifecycle: { completesRun: true }` to make a tool end the agent loop
- Subscribing to `CoreSessionEvent` events with `cline.subscribe()`
- Rendering parsed `agent_event` text/tool events instead of printing serialized `chunk` payloads
- Cleaning up runtime resources with `cline.dispose()`
- Processing structured results after the run completes
## Notes
This is an AI-assisted review tool, not a replacement for SAST, dependency scanning, secret scanning, manual threat modeling, or human security review. Treat findings as review input and verify them before acting.
For a general-purpose reviewer, see [code-review-bot](../code-review-bot). For a simpler starting point, see [quickstart](../quickstart).
@@ -0,0 +1,22 @@
{
"name": "@cline/example-security-review-bot",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
"build:sdk": "bun run --cwd ../../.. build:sdk",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@cline/sdk": "workspace:*",
"zod": "^4.3.6"
},
"devDependencies": {
"typescript": "^5.9.3"
},
"engines": {
"node": ">=22"
}
}
@@ -0,0 +1,354 @@
import { execFileSync } from "node:child_process";
import {
type AgentEvent,
type AgentTool,
ClineCore,
createTool,
type ToolPolicy,
} from "@cline/sdk";
import { z } from "zod";
const SecurityFindingSchema = z.object({
file: z.string().describe("File path"),
line: z.number().describe("Line number (approximate is fine)"),
severity: z.enum(["critical", "high", "medium", "low", "info"]),
category: z
.enum([
"authentication",
"authorization",
"injection",
"secrets",
"cryptography",
"data_exposure",
"ssrf",
"xss",
"deserialization",
"path_traversal",
"dependency",
"logging_monitoring",
"configuration",
"other",
])
.describe("Security weakness category"),
cwe: z
.string()
.optional()
.describe("Relevant CWE identifier, such as CWE-89, if known"),
owasp: z.string().optional().describe("Relevant OWASP category, if known"),
title: z.string().describe("Short finding title"),
description: z.string().describe("What is vulnerable and why it matters"),
exploitScenario: z
.string()
.describe("A realistic abuse case or exploit path"),
remediation: z.string().describe("Concrete recommended fix or mitigation"),
confidence: z.enum(["high", "medium", "low"]),
});
const SecurityReviewResultSchema = z.object({
summary: z.string().describe("Brief overall security assessment"),
overallRisk: z.enum(["critical", "high", "medium", "low", "none"]),
blockMerge: z
.boolean()
.describe("Whether the changes should be blocked from merging"),
});
const findings: z.infer<typeof SecurityFindingSchema>[] = [];
let reviewResult: z.infer<typeof SecurityReviewResultSchema> | undefined;
const repoRoot = (() => {
try {
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
encoding: "utf-8",
}).trim();
} catch {
console.error("Error: must be run from within a git repository.");
process.exit(1);
}
})();
const systemPrompt = `You are a senior application security engineer performing a focused security review of a git diff.
Your goal is to identify exploitable or defense-in-depth security issues introduced or modified by the diff. The repository root is ${repoRoot}. Use the read_files tool with absolute paths, or search_codebase, when you need surrounding code to determine whether a finding is real.
Focus on:
- Authentication and authorization bypasses
- Injection flaws, including SQL/NoSQL/command/template/LDAP injection
- Cross-site scripting and unsafe HTML/script rendering
- Server-side request forgery and unsafe URL fetching
- Path traversal and unsafe file operations
- Hardcoded secrets, tokens, private keys, and credential leakage
- Cryptographic misuse, weak randomness, insecure hashing, or missing integrity checks
- Sensitive data exposure in logs, errors, telemetry, APIs, or client bundles
- Unsafe deserialization or dynamic code execution
- Insecure dependency, environment, CORS, cookie, header, or cloud configuration changes
- Missing validation, rate limits, audit logging, or security checks around sensitive operations
Only report actionable findings that are supported by the diff or file context. Do not report generic best practices unless they materially reduce a realistic risk. Prefer fewer high-quality findings over many speculative ones.
Severity guidance:
- critical: likely remote code execution, auth bypass, mass data exposure, secret compromise, or trivially exploitable injection on sensitive data paths
- high: practical privilege escalation, significant data exposure, SSRF with meaningful impact, or exploitable stored XSS
- medium: plausible vulnerability requiring constraints, limited data exposure, missing authorization on lower-risk operations, or reflected XSS with user interaction
- low: hardening issue with limited exploitability
- info: noteworthy observation with minimal direct risk
For each real issue, call add_security_finding with clear evidence, exploit scenario, remediation, confidence, and CWE/OWASP metadata when applicable.
When you are done reviewing, call submit_security_review with a brief summary, an overall risk rating, and whether the changes should be blocked from merging.`;
const addSecurityFindingTool = createTool({
name: "add_security_finding",
description:
"Add an actionable security finding on a specific file and line.",
inputSchema: SecurityFindingSchema,
async execute(input) {
findings.push(input);
return {
success: true,
message: `Security finding added (${findings.length} total)`,
findingCount: findings.length,
};
},
});
const submitSecurityReviewTool = createTool({
name: "submit_security_review",
description: "Submit the completed security review with a risk summary.",
inputSchema: SecurityReviewResultSchema,
lifecycle: { completesRun: true },
async execute(input) {
reviewResult = input;
return {
success: true,
summary: input.summary,
overallRisk: input.overallRisk,
blockMerge: input.blockMerge,
findingCount: findings.length,
};
},
});
const securityReviewTools: AgentTool[] = [
addSecurityFindingTool as AgentTool,
submitSecurityReviewTool as AgentTool,
];
const securityReviewToolPolicies = {
"*": { autoApprove: true },
read_files: { autoApprove: true },
search_codebase: { autoApprove: true },
add_security_finding: { autoApprove: true },
submit_security_review: { autoApprove: true },
run_commands: { enabled: false },
fetch_web_content: { enabled: false },
editor: { enabled: false },
apply_patch: { enabled: false },
skills: { enabled: false },
ask_question: { enabled: false },
} satisfies Record<string, ToolPolicy>;
let reasoningOpen = false;
function closeReasoning() {
if (reasoningOpen) {
process.stdout.write("\n");
reasoningOpen = false;
}
}
function renderToolStart(
event: Extract<AgentEvent, { type: "content_start" }>,
) {
const toolName = event.toolName ?? "unknown_tool";
if (toolName === "add_security_finding") {
const parsed = SecurityFindingSchema.safeParse(event.input);
if (!parsed.success) {
console.log(`\n[tool] ${toolName}`);
return;
}
const input = parsed.data;
const icon =
input.severity === "critical"
? "X"
: input.severity === "high"
? "!"
: input.severity === "medium"
? "~"
: "i";
console.log(
`\n [${icon}] ${input.severity.toUpperCase()} ${input.file}:${input.line} - ${input.title}`,
);
return;
}
console.log(`\n[tool] ${toolName}`);
}
function renderAgentEvent(event: AgentEvent) {
switch (event.type) {
case "content_start":
if (event.contentType === "text" && event.text) {
closeReasoning();
process.stdout.write(event.text);
return;
}
if (event.contentType === "reasoning") {
if (event.redacted === true && !event.reasoning) {
console.log("\n[thinking redacted]");
return;
}
if (event.reasoning) {
if (!reasoningOpen) {
process.stdout.write("\n[thinking]\n");
reasoningOpen = true;
}
process.stdout.write(event.reasoning);
}
return;
}
if (event.contentType === "tool") {
closeReasoning();
renderToolStart(event);
return;
}
break;
case "content_end":
if (event.contentType === "reasoning") {
closeReasoning();
return;
}
if (event.contentType === "tool" && event.error) {
closeReasoning();
console.log(
`\n[tool failed] ${event.toolName ?? "unknown_tool"}: ${event.error}`,
);
}
break;
}
}
// Get the diff to review
const { ref, extraPrompt } = parseArgs(process.argv.slice(2));
if (ref.startsWith("-")) {
console.error(`Invalid ref: ${ref}`);
process.exit(1);
}
let diff: string;
try {
diff = execFileSync("git", ["diff", ref], {
encoding: "utf-8",
maxBuffer: 5 * 1024 * 1024,
});
} catch {
console.error(`Failed to get diff for ref: ${ref}`);
console.error("Usage: bun dev [git-ref]");
console.error(" e.g. bun dev HEAD~3");
console.error(" e.g. bun dev main");
process.exit(1);
}
if (!diff.trim()) {
console.log("No diff found. Nothing to security review.");
process.exit(0);
}
console.log(
`Security reviewing diff against ${ref} (${diff.split("\n").length} lines)...\n`,
);
if (extraPrompt) {
console.log(`Additional review instructions: ${extraPrompt}\n`);
}
const cline = await ClineCore.create({
clientName: "security-review-bot",
backendMode: "local",
});
const unsubscribe = cline.subscribe((event) => {
switch (event.type) {
case "agent_event":
renderAgentEvent(event.payload.event);
break;
}
});
try {
const prompt = `Security review this git diff.${
extraPrompt ? `\n\nAdditional review instructions:\n${extraPrompt}` : ""
}\n\n\`\`\`diff\n${diff}\n\`\`\``;
const result = await cline.start({
source: "cli",
interactive: false,
prompt,
config: {
providerId: "cline",
modelId: "anthropic/claude-sonnet-4.6",
apiKey: process.env.CLINE_API_KEY,
cwd: repoRoot,
workspaceRoot: repoRoot,
mode: "act",
systemPrompt,
maxIterations: 25,
enableTools: true,
enableSpawnAgent: false,
enableAgentTeams: false,
disableMcpSettingsTools: true,
toolPolicies: securityReviewToolPolicies,
},
localRuntime: {
extraTools: securityReviewTools,
},
});
console.log("\n\n--- Security Review Complete ---\n");
if (reviewResult) {
console.log(`Summary: ${reviewResult.summary}`);
console.log(`Overall risk: ${reviewResult.overallRisk}`);
console.log(`Block merge: ${reviewResult.blockMerge ? "yes" : "no"}\n`);
}
if (findings.length === 0) {
console.log("No actionable security findings identified.");
} else {
const severityOrder = [
"critical",
"high",
"medium",
"low",
"info",
] as const;
for (const severity of severityOrder) {
const group = findings.filter((finding) => finding.severity === severity);
if (group.length === 0) {
continue;
}
console.log(`${severity.toUpperCase()}: ${group.length}`);
for (const finding of group) {
const metadata = [finding.cwe, finding.owasp]
.filter(Boolean)
.join(" | ");
console.log(
` ${finding.file}:${finding.line} - ${finding.title}${metadata ? ` (${metadata})` : ""}`,
);
console.log(` Category: ${finding.category}`);
console.log(` Confidence: ${finding.confidence}`);
console.log(` Risk: ${finding.description}`);
console.log(` Exploit: ${finding.exploitScenario}`);
console.log(` Fix: ${finding.remediation}`);
}
}
}
const usage = result.result?.usage;
console.log(
`\nSession: ${result.sessionId} | Status: ${result.result?.finishReason ?? "unknown"} | Iterations: ${result.result?.iterations ?? 0} | Tokens: ${usage?.outputTokens ?? 0} output`,
);
} finally {
unsubscribe();
await cline.dispose();
}
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.apps.json",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "dist",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true
},
"include": ["src"]
}
+13
View File
@@ -186,6 +186,17 @@
"typescript": "^5.9.3",
},
},
"apps/examples/security-review-bot": {
"name": "@cline/example-security-review-bot",
"version": "0.0.0",
"dependencies": {
"@cline/sdk": "workspace:*",
"zod": "^4.3.6",
},
"devDependencies": {
"typescript": "^5.9.3",
},
},
"apps/examples/vscode": {
"name": "@cline/vscode",
"version": "0.0.0",
@@ -611,6 +622,8 @@
"@cline/example-quickstart": ["@cline/example-quickstart@workspace:apps/examples/quickstart"],
"@cline/example-security-review-bot": ["@cline/example-security-review-bot@workspace:apps/examples/security-review-bot"],
"@cline/llms": ["@cline/llms@workspace:packages/llms"],
"@cline/menubar": ["@cline/menubar@workspace:apps/examples/menubar"],