@authora/agent-audit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '22'
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - name: Install dependencies
24
+ run: npm install
25
+
26
+ - name: Build
27
+ run: npx tsc
28
+
29
+ - name: Publish
30
+ run: npm publish --provenance --access public
31
+ env:
32
+ NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Authora
4
+
5
+ Permission is hereby granted...
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # agent-audit
2
+
3
+ > Security scanner for AI agents. Find vulnerabilities in your agent setup in 30 seconds.
4
+
5
+ ```bash
6
+ npx agent-audit
7
+ ```
8
+
9
+ ## What it checks
10
+
11
+ | Category | What it finds |
12
+ |----------|--------------|
13
+ | **Credentials** | Shared API keys across agents, hardcoded secrets in code |
14
+ | **Identity** | Missing agent identity layer, no cryptographic verification |
15
+ | **MCP** | MCP servers without authentication, unprotected tool endpoints |
16
+ | **Permissions** | Overly broad agent permissions, admin/root access |
17
+ | **Delegation** | Missing delegation chains, agents inheriting full user permissions |
18
+ | **Audit** | No audit logging for agent actions |
19
+ | **Approvals** | No human-in-the-loop for sensitive operations |
20
+ | **Resilience** | Missing timeouts, no error handling on tool calls |
21
+
22
+ ## Output
23
+
24
+ ```
25
+ Agent Security Audit
26
+ by Authora -- https://authora.dev
27
+
28
+ Scanning current directory...
29
+
30
+ Scanned 47 files
31
+ Found 3 agent(s), 2 MCP server(s)
32
+
33
+ CRITICAL Shared API key may be used by 3 agent files (.env)
34
+ CRITICAL No agent identity layer detected
35
+ CRITICAL 2 MCP server(s) found but no agent identity
36
+ WARNING MCP server detected without visible auth configuration (mcp/server.ts)
37
+ WARNING No delegation chains -- agents may inherit unlimited permissions
38
+ WARNING No audit logging for agent actions detected
39
+ INFO No approval workflows for sensitive agent actions
40
+
41
+ Security Posture:
42
+ Identity layer: No
43
+ Delegation chains: No
44
+ Audit logging: No
45
+ Approval workflows: No
46
+
47
+ Agent Security Score: 1.5/10 [=== ] Grade: F
48
+ 3 critical, 3 warnings
49
+
50
+ Learn more: https://github.com/authora-dev/awesome-agent-security
51
+ Fix issues: https://authora.dev/get-started
52
+ ```
53
+
54
+ ## Options
55
+
56
+ ```bash
57
+ npx agent-audit [directory] # Scan a specific directory
58
+ npx agent-audit --json # Output as JSON
59
+ npx agent-audit --badge # Generate README badge markdown
60
+ ```
61
+
62
+ ## Badge
63
+
64
+ Add a security badge to your README:
65
+
66
+ ```markdown
67
+ ![Agent Security: A](https://img.shields.io/badge/Agent_Security-A-brightgreen)
68
+ ```
69
+
70
+ ## What scores mean
71
+
72
+ | Score | Grade | Meaning |
73
+ |-------|-------|---------|
74
+ | 9-10 | A+ | Excellent -- cryptographic identity, delegation, audit, approvals |
75
+ | 8 | A | Strong -- identity layer present, minor gaps |
76
+ | 7 | B+ | Good -- most practices in place |
77
+ | 6 | B | Decent -- some security measures, gaps remain |
78
+ | 5 | C | Needs work -- basic security only |
79
+ | 3-4 | D | Weak -- significant vulnerabilities |
80
+ | 0-2 | F | Critical -- no agent security measures |
81
+
82
+ ## Learn more
83
+
84
+ - [Awesome Agent Security](https://github.com/authora-dev/awesome-agent-security) -- curated resources
85
+ - [Authora](https://authora.dev) -- identity, coordination, and security for AI agents
86
+ - [Authora Docs](https://authora.dev/developers/quickstart) -- get started in 5 minutes
87
+
88
+ ## License
89
+
90
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { scanDirectory } from "./scanner.js";
3
+ import { formatReport } from "./reporter.js";
4
+ const args = process.argv.slice(2);
5
+ const targetDir = args[0] ?? ".";
6
+ const jsonOutput = args.includes("--json");
7
+ const badgeOutput = args.includes("--badge");
8
+ async function main() {
9
+ if (!jsonOutput) {
10
+ console.log("");
11
+ console.log(" \x1b[1m\x1b[36mAgent Security Audit\x1b[0m");
12
+ console.log(" \x1b[90mby Authora -- https://authora.dev\x1b[0m");
13
+ console.log("");
14
+ console.log(` Scanning ${targetDir === "." ? "current directory" : targetDir}...`);
15
+ console.log("");
16
+ }
17
+ const findings = await scanDirectory(targetDir);
18
+ if (jsonOutput) {
19
+ console.log(JSON.stringify(findings, null, 2));
20
+ return;
21
+ }
22
+ const report = formatReport(findings);
23
+ console.log(report);
24
+ if (badgeOutput) {
25
+ const score = findings.score;
26
+ const color = score >= 8 ? "brightgreen" : score >= 6 ? "yellow" : score >= 4 ? "orange" : "red";
27
+ const grade = score >= 9 ? "A+" : score >= 8 ? "A" : score >= 7 ? "B+" : score >= 6 ? "B" : score >= 5 ? "C" : score >= 3 ? "D" : "F";
28
+ console.log("");
29
+ console.log(` \x1b[1mREADME Badge:\x1b[0m`);
30
+ console.log(` ![Agent Security: ${grade}](https://img.shields.io/badge/Agent_Security-${grade}-${color})`);
31
+ }
32
+ process.exit(findings.score >= 6 ? 0 : 1);
33
+ }
34
+ main().catch((err) => {
35
+ console.error(" \x1b[31mError:\x1b[0m", err.message);
36
+ process.exit(2);
37
+ });
@@ -0,0 +1,2 @@
1
+ import type { ScanResult } from "./scanner.js";
2
+ export declare function formatReport(result: ScanResult): string;
@@ -0,0 +1,77 @@
1
+ const COLORS = {
2
+ red: "\x1b[31m",
3
+ yellow: "\x1b[33m",
4
+ green: "\x1b[32m",
5
+ cyan: "\x1b[36m",
6
+ gray: "\x1b[90m",
7
+ white: "\x1b[37m",
8
+ bold: "\x1b[1m",
9
+ reset: "\x1b[0m",
10
+ };
11
+ const SEVERITY_LABEL = {
12
+ critical: `${COLORS.red}${COLORS.bold}CRITICAL${COLORS.reset}`,
13
+ warning: `${COLORS.yellow}WARNING ${COLORS.reset}`,
14
+ info: `${COLORS.cyan}INFO ${COLORS.reset}`,
15
+ pass: `${COLORS.green}PASS ${COLORS.reset}`,
16
+ };
17
+ function scoreBar(score) {
18
+ const filled = Math.round(score * 2);
19
+ const empty = 20 - filled;
20
+ const color = score >= 8 ? COLORS.green : score >= 5 ? COLORS.yellow : COLORS.red;
21
+ return `${color}[${"=".repeat(filled)}${" ".repeat(empty)}]${COLORS.reset}`;
22
+ }
23
+ function gradeFromScore(score) {
24
+ if (score >= 9)
25
+ return `${COLORS.green}${COLORS.bold}A+${COLORS.reset}`;
26
+ if (score >= 8)
27
+ return `${COLORS.green}${COLORS.bold}A${COLORS.reset}`;
28
+ if (score >= 7)
29
+ return `${COLORS.green}B+${COLORS.reset}`;
30
+ if (score >= 6)
31
+ return `${COLORS.yellow}B${COLORS.reset}`;
32
+ if (score >= 5)
33
+ return `${COLORS.yellow}C${COLORS.reset}`;
34
+ if (score >= 3)
35
+ return `${COLORS.red}D${COLORS.reset}`;
36
+ return `${COLORS.red}${COLORS.bold}F${COLORS.reset}`;
37
+ }
38
+ export function formatReport(result) {
39
+ const lines = [];
40
+ // Summary
41
+ lines.push(` ${COLORS.gray}Scanned ${result.scannedFiles} files${COLORS.reset}`);
42
+ lines.push(` ${COLORS.gray}Found ${result.agents} agent(s), ${result.mcpServers} MCP server(s)${COLORS.reset}`);
43
+ lines.push("");
44
+ // Findings
45
+ if (result.findings.length === 0) {
46
+ lines.push(` ${COLORS.green}${COLORS.bold}No issues found!${COLORS.reset}`);
47
+ }
48
+ else {
49
+ for (const f of result.findings) {
50
+ const label = SEVERITY_LABEL[f.severity] ?? f.severity;
51
+ const file = f.file ? ` ${COLORS.gray}(${f.file})${COLORS.reset}` : "";
52
+ lines.push(` ${label} ${f.message}${file}`);
53
+ if (f.fix) {
54
+ lines.push(` ${COLORS.gray}Fix: ${f.fix}${COLORS.reset}`);
55
+ }
56
+ }
57
+ }
58
+ lines.push("");
59
+ // Security posture
60
+ lines.push(` ${COLORS.bold}Security Posture:${COLORS.reset}`);
61
+ lines.push(` Identity layer: ${result.hasIdentityLayer ? `${COLORS.green}Yes${COLORS.reset}` : `${COLORS.red}No${COLORS.reset}`}`);
62
+ lines.push(` Delegation chains: ${result.hasDelegation ? `${COLORS.green}Yes${COLORS.reset}` : `${COLORS.red}No${COLORS.reset}`}`);
63
+ lines.push(` Audit logging: ${result.hasAuditLog ? `${COLORS.green}Yes${COLORS.reset}` : `${COLORS.red}No${COLORS.reset}`}`);
64
+ lines.push(` Approval workflows:${result.hasApprovals ? `${COLORS.green}Yes${COLORS.reset}` : `${COLORS.yellow} No${COLORS.reset}`}`);
65
+ lines.push("");
66
+ // Score
67
+ const criticals = result.findings.filter((f) => f.severity === "critical").length;
68
+ const warnings = result.findings.filter((f) => f.severity === "warning").length;
69
+ lines.push(` ${COLORS.bold}Agent Security Score: ${result.score}/10${COLORS.reset} ${scoreBar(result.score)} Grade: ${gradeFromScore(result.score)}`);
70
+ lines.push(` ${COLORS.gray}${criticals} critical, ${warnings} warnings${COLORS.reset}`);
71
+ lines.push("");
72
+ // CTAs
73
+ lines.push(` ${COLORS.gray}Learn more:${COLORS.reset} https://github.com/authora-dev/awesome-agent-security`);
74
+ lines.push(` ${COLORS.gray}Fix issues:${COLORS.reset} https://authora.dev/get-started`);
75
+ lines.push("");
76
+ return lines.join("\n");
77
+ }
@@ -0,0 +1,20 @@
1
+ export interface Finding {
2
+ severity: "critical" | "warning" | "info" | "pass";
3
+ category: string;
4
+ message: string;
5
+ file?: string;
6
+ line?: number;
7
+ fix?: string;
8
+ }
9
+ export interface ScanResult {
10
+ findings: Finding[];
11
+ score: number;
12
+ agents: number;
13
+ mcpServers: number;
14
+ hasIdentityLayer: boolean;
15
+ hasDelegation: boolean;
16
+ hasAuditLog: boolean;
17
+ hasApprovals: boolean;
18
+ scannedFiles: number;
19
+ }
20
+ export declare function scanDirectory(dir: string): Promise<ScanResult>;
@@ -0,0 +1,234 @@
1
+ import { readFileSync, readdirSync, statSync } from "fs";
2
+ import { join, extname } from "path";
3
+ const CODE_EXTENSIONS = new Set([
4
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
5
+ ".py", ".go", ".rs", ".java", ".rb",
6
+ ".json", ".yaml", ".yml", ".toml", ".env",
7
+ ]);
8
+ const MAX_FILES = 500;
9
+ const MAX_FILE_SIZE = 100_000; // 100KB
10
+ function walkDir(dir, files = [], depth = 0) {
11
+ if (depth > 8 || files.length > MAX_FILES)
12
+ return files;
13
+ try {
14
+ const entries = readdirSync(dir);
15
+ for (const entry of entries) {
16
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" ||
17
+ entry === "build" || entry === "__pycache__" || entry === "venv" ||
18
+ entry === ".git" || entry === "vendor")
19
+ continue;
20
+ const fullPath = join(dir, entry);
21
+ try {
22
+ const stat = statSync(fullPath);
23
+ if (stat.isDirectory()) {
24
+ walkDir(fullPath, files, depth + 1);
25
+ }
26
+ else if (stat.isFile() && CODE_EXTENSIONS.has(extname(entry)) && stat.size < MAX_FILE_SIZE) {
27
+ files.push(fullPath);
28
+ }
29
+ }
30
+ catch {
31
+ // Skip inaccessible files
32
+ }
33
+ }
34
+ }
35
+ catch {
36
+ // Skip inaccessible directories
37
+ }
38
+ return files;
39
+ }
40
+ function readFile(path) {
41
+ try {
42
+ return readFileSync(path, "utf-8");
43
+ }
44
+ catch {
45
+ return "";
46
+ }
47
+ }
48
+ export async function scanDirectory(dir) {
49
+ const findings = [];
50
+ let agents = 0;
51
+ let mcpServers = 0;
52
+ let hasIdentityLayer = false;
53
+ let hasDelegation = false;
54
+ let hasAuditLog = false;
55
+ let hasApprovals = false;
56
+ const files = walkDir(dir);
57
+ for (const file of files) {
58
+ const content = readFile(file);
59
+ if (!content)
60
+ continue;
61
+ const lower = content.toLowerCase();
62
+ const relPath = file.replace(dir + "/", "");
63
+ // Detect agents
64
+ if (/\bagent\b/i.test(content) && (/\bcreate.*agent|agent.*config|new.*agent|agent.*class\b/i.test(content))) {
65
+ agents++;
66
+ }
67
+ // Detect MCP servers
68
+ if (/mcp.*server|mcpserver|model.*context.*protocol/i.test(lower)) {
69
+ mcpServers++;
70
+ }
71
+ // Check for identity layer
72
+ if (/authora|@authora\/sdk|agent.*identity|ed25519.*agent|agent.*keypair/i.test(lower)) {
73
+ hasIdentityLayer = true;
74
+ }
75
+ // Check for delegation
76
+ if (/delegation.*chain|delegate.*permission|token.*exchange|rfc.*8693/i.test(lower)) {
77
+ hasDelegation = true;
78
+ }
79
+ // Check for audit logging
80
+ if (/audit.*log|agent.*audit|action.*log.*agent/i.test(lower)) {
81
+ hasAuditLog = true;
82
+ }
83
+ // Check for approvals
84
+ if (/approval.*workflow|human.*in.*loop|require.*approval/i.test(lower)) {
85
+ hasApprovals = true;
86
+ }
87
+ // --- CRITICAL: Shared API keys ---
88
+ const apiKeyPattern = /(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|AZURE_API_KEY|API_KEY)\s*[=:]\s*["']?[A-Za-z0-9_-]{20,}/g;
89
+ const envFile = extname(file) === ".env";
90
+ if (envFile) {
91
+ const keys = content.match(apiKeyPattern);
92
+ if (keys && keys.length > 0) {
93
+ // Check if multiple agents might share this key
94
+ const agentFiles = files.filter((f) => {
95
+ const fc = readFile(f);
96
+ return /\bagent\b/i.test(fc) && /API_KEY|api_key|apiKey/i.test(fc);
97
+ });
98
+ if (agentFiles.length > 1) {
99
+ findings.push({
100
+ severity: "critical",
101
+ category: "credentials",
102
+ message: `Shared API key may be used by ${agentFiles.length} agent files`,
103
+ file: relPath,
104
+ fix: "Give each agent its own credentials with scoped permissions",
105
+ });
106
+ }
107
+ }
108
+ }
109
+ // --- CRITICAL: Hardcoded secrets in code ---
110
+ if (!envFile) {
111
+ const hardcodedSecrets = content.match(/(?:sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|xoxb-[0-9]+-[a-zA-Z0-9]+)/g);
112
+ if (hardcodedSecrets) {
113
+ findings.push({
114
+ severity: "critical",
115
+ category: "credentials",
116
+ message: `Hardcoded secret found in source code`,
117
+ file: relPath,
118
+ fix: "Move secrets to environment variables and use a secrets manager",
119
+ });
120
+ }
121
+ }
122
+ // --- WARNING: MCP server without auth ---
123
+ if (/mcp.*server|createServer/i.test(content) && !/auth|authentication|authorization|token|apiKey/i.test(content)) {
124
+ findings.push({
125
+ severity: "warning",
126
+ category: "mcp",
127
+ message: "MCP server detected without visible auth configuration",
128
+ file: relPath,
129
+ fix: "Add authentication to your MCP server (API key, JWT, or Ed25519 signature verification)",
130
+ });
131
+ }
132
+ // --- WARNING: Broad agent permissions ---
133
+ if (/\*.*permission|admin.*role|full.*access|sudo|root/i.test(content) && /agent/i.test(content)) {
134
+ findings.push({
135
+ severity: "warning",
136
+ category: "permissions",
137
+ message: "Agent may have overly broad permissions",
138
+ file: relPath,
139
+ fix: "Apply least-privilege: give agents only the permissions they need for their specific task",
140
+ });
141
+ }
142
+ // --- WARNING: No error handling on tool calls ---
143
+ if (/tool.*call|callTool|execute.*tool/i.test(content) && !/try|catch|error/i.test(content)) {
144
+ findings.push({
145
+ severity: "warning",
146
+ category: "resilience",
147
+ message: "Agent tool calls without error handling",
148
+ file: relPath,
149
+ fix: "Wrap tool calls in try/catch with proper error reporting and fallback behavior",
150
+ });
151
+ }
152
+ // --- INFO: Agent without timeout ---
153
+ if (/agent/i.test(content) && /async|await|fetch|request/i.test(content) && !/timeout|AbortSignal|signal/i.test(content)) {
154
+ findings.push({
155
+ severity: "info",
156
+ category: "resilience",
157
+ message: "Agent operations without timeout -- could run indefinitely",
158
+ file: relPath,
159
+ fix: "Add timeouts to agent operations to prevent runaway processes",
160
+ });
161
+ }
162
+ }
163
+ // --- Structural findings ---
164
+ if (!hasIdentityLayer) {
165
+ findings.push({
166
+ severity: "critical",
167
+ category: "identity",
168
+ message: "No agent identity layer detected",
169
+ fix: "Add cryptographic agent identities so each agent has a verifiable, unique identity. See: https://authora.dev/get-started",
170
+ });
171
+ }
172
+ if (!hasDelegation && agents > 1) {
173
+ findings.push({
174
+ severity: "warning",
175
+ category: "delegation",
176
+ message: "No delegation chains -- agents may inherit unlimited permissions",
177
+ fix: "Implement delegation chains (RFC 8693) so agents receive scoped, time-bound authority",
178
+ });
179
+ }
180
+ if (!hasAuditLog) {
181
+ findings.push({
182
+ severity: "warning",
183
+ category: "audit",
184
+ message: "No audit logging for agent actions detected",
185
+ fix: "Log every agent action with: who (agent ID), what (action), when (timestamp), authorized by (delegation chain)",
186
+ });
187
+ }
188
+ if (!hasApprovals && agents > 0) {
189
+ findings.push({
190
+ severity: "info",
191
+ category: "approvals",
192
+ message: "No approval workflows for sensitive agent actions",
193
+ fix: "Add human-in-the-loop approval for high-risk operations (production deploys, data access, secret rotation)",
194
+ });
195
+ }
196
+ if (mcpServers > 0 && !hasIdentityLayer) {
197
+ findings.push({
198
+ severity: "critical",
199
+ category: "mcp",
200
+ message: `${mcpServers} MCP server(s) found but no agent identity -- any client can call any tool`,
201
+ fix: "Add agent identity verification to your MCP servers. See: https://authora.dev/developers/mcp",
202
+ });
203
+ }
204
+ // Calculate score (0-10) based on CATEGORIES not raw count
205
+ // This prevents large codebases from being penalized for having many files
206
+ const criticalCategories = new Set(findings.filter((f) => f.severity === "critical").map((f) => f.category));
207
+ const warningCategories = new Set(findings.filter((f) => f.severity === "warning").map((f) => f.category));
208
+ const infoCategories = new Set(findings.filter((f) => f.severity === "info").map((f) => f.category));
209
+ let score = 10;
210
+ score -= criticalCategories.size * 2.5;
211
+ score -= warningCategories.size * 1.5;
212
+ score -= infoCategories.size * 0.5;
213
+ // Bonus for good practices (max +4)
214
+ if (hasIdentityLayer)
215
+ score += 1;
216
+ if (hasDelegation)
217
+ score += 1;
218
+ if (hasAuditLog)
219
+ score += 1;
220
+ if (hasApprovals)
221
+ score += 1;
222
+ score = Math.max(0, Math.min(10, Math.round(score * 10) / 10));
223
+ return {
224
+ findings,
225
+ score,
226
+ agents,
227
+ mcpServers,
228
+ hasIdentityLayer,
229
+ hasDelegation,
230
+ hasAuditLog,
231
+ hasApprovals,
232
+ scannedFiles: files.length,
233
+ };
234
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@authora/agent-audit",
3
+ "version": "0.1.0",
4
+ "description": "Security scanner for AI agents. Find vulnerabilities in your agent setup in 30 seconds.",
5
+ "bin": {
6
+ "agent-audit": "./dist/cli.js"
7
+ },
8
+ "type": "module",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/cli.js"
12
+ },
13
+ "keywords": [
14
+ "ai-agents",
15
+ "security",
16
+ "audit",
17
+ "mcp",
18
+ "agent-identity",
19
+ "llm-security",
20
+ "ai-security"
21
+ ],
22
+ "author": "Authora <admin@authora.dev> (https://authora.dev)",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/authora-dev/agent-audit"
27
+ },
28
+ "homepage": "https://authora.dev",
29
+ "devDependencies": {
30
+ "@types/node": "^25.5.0",
31
+ "typescript": "^5.9.3"
32
+ }
33
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { scanDirectory } from "./scanner.js";
4
+ import { formatReport } from "./reporter.js";
5
+
6
+ const args = process.argv.slice(2);
7
+ const targetDir = args[0] ?? ".";
8
+ const jsonOutput = args.includes("--json");
9
+ const badgeOutput = args.includes("--badge");
10
+
11
+ async function main() {
12
+ if (!jsonOutput) {
13
+ console.log("");
14
+ console.log(" \x1b[1m\x1b[36mAgent Security Audit\x1b[0m");
15
+ console.log(" \x1b[90mby Authora -- https://authora.dev\x1b[0m");
16
+ console.log("");
17
+ console.log(` Scanning ${targetDir === "." ? "current directory" : targetDir}...`);
18
+ console.log("");
19
+ }
20
+
21
+ const findings = await scanDirectory(targetDir);
22
+
23
+ if (jsonOutput) {
24
+ console.log(JSON.stringify(findings, null, 2));
25
+ return;
26
+ }
27
+
28
+ const report = formatReport(findings);
29
+ console.log(report);
30
+
31
+ if (badgeOutput) {
32
+ const score = findings.score;
33
+ const color = score >= 8 ? "brightgreen" : score >= 6 ? "yellow" : score >= 4 ? "orange" : "red";
34
+ const grade = score >= 9 ? "A+" : score >= 8 ? "A" : score >= 7 ? "B+" : score >= 6 ? "B" : score >= 5 ? "C" : score >= 3 ? "D" : "F";
35
+ console.log("");
36
+ console.log(` \x1b[1mREADME Badge:\x1b[0m`);
37
+ console.log(` ![Agent Security: ${grade}](https://img.shields.io/badge/Agent_Security-${grade}-${color})`);
38
+ }
39
+
40
+ process.exit(findings.score >= 6 ? 0 : 1);
41
+ }
42
+
43
+ main().catch((err) => {
44
+ console.error(" \x1b[31mError:\x1b[0m", (err as Error).message);
45
+ process.exit(2);
46
+ });
@@ -0,0 +1,84 @@
1
+ import type { ScanResult } from "./scanner.js";
2
+
3
+ const COLORS = {
4
+ red: "\x1b[31m",
5
+ yellow: "\x1b[33m",
6
+ green: "\x1b[32m",
7
+ cyan: "\x1b[36m",
8
+ gray: "\x1b[90m",
9
+ white: "\x1b[37m",
10
+ bold: "\x1b[1m",
11
+ reset: "\x1b[0m",
12
+ };
13
+
14
+ const SEVERITY_LABEL: Record<string, string> = {
15
+ critical: `${COLORS.red}${COLORS.bold}CRITICAL${COLORS.reset}`,
16
+ warning: `${COLORS.yellow}WARNING ${COLORS.reset}`,
17
+ info: `${COLORS.cyan}INFO ${COLORS.reset}`,
18
+ pass: `${COLORS.green}PASS ${COLORS.reset}`,
19
+ };
20
+
21
+ function scoreBar(score: number): string {
22
+ const filled = Math.round(score * 2);
23
+ const empty = 20 - filled;
24
+ const color = score >= 8 ? COLORS.green : score >= 5 ? COLORS.yellow : COLORS.red;
25
+ return `${color}[${"=".repeat(filled)}${" ".repeat(empty)}]${COLORS.reset}`;
26
+ }
27
+
28
+ function gradeFromScore(score: number): string {
29
+ if (score >= 9) return `${COLORS.green}${COLORS.bold}A+${COLORS.reset}`;
30
+ if (score >= 8) return `${COLORS.green}${COLORS.bold}A${COLORS.reset}`;
31
+ if (score >= 7) return `${COLORS.green}B+${COLORS.reset}`;
32
+ if (score >= 6) return `${COLORS.yellow}B${COLORS.reset}`;
33
+ if (score >= 5) return `${COLORS.yellow}C${COLORS.reset}`;
34
+ if (score >= 3) return `${COLORS.red}D${COLORS.reset}`;
35
+ return `${COLORS.red}${COLORS.bold}F${COLORS.reset}`;
36
+ }
37
+
38
+ export function formatReport(result: ScanResult): string {
39
+ const lines: string[] = [];
40
+
41
+ // Summary
42
+ lines.push(` ${COLORS.gray}Scanned ${result.scannedFiles} files${COLORS.reset}`);
43
+ lines.push(` ${COLORS.gray}Found ${result.agents} agent(s), ${result.mcpServers} MCP server(s)${COLORS.reset}`);
44
+ lines.push("");
45
+
46
+ // Findings
47
+ if (result.findings.length === 0) {
48
+ lines.push(` ${COLORS.green}${COLORS.bold}No issues found!${COLORS.reset}`);
49
+ } else {
50
+ for (const f of result.findings) {
51
+ const label = SEVERITY_LABEL[f.severity] ?? f.severity;
52
+ const file = f.file ? ` ${COLORS.gray}(${f.file})${COLORS.reset}` : "";
53
+ lines.push(` ${label} ${f.message}${file}`);
54
+ if (f.fix) {
55
+ lines.push(` ${COLORS.gray}Fix: ${f.fix}${COLORS.reset}`);
56
+ }
57
+ }
58
+ }
59
+
60
+ lines.push("");
61
+
62
+ // Security posture
63
+ lines.push(` ${COLORS.bold}Security Posture:${COLORS.reset}`);
64
+ lines.push(` Identity layer: ${result.hasIdentityLayer ? `${COLORS.green}Yes${COLORS.reset}` : `${COLORS.red}No${COLORS.reset}`}`);
65
+ lines.push(` Delegation chains: ${result.hasDelegation ? `${COLORS.green}Yes${COLORS.reset}` : `${COLORS.red}No${COLORS.reset}`}`);
66
+ lines.push(` Audit logging: ${result.hasAuditLog ? `${COLORS.green}Yes${COLORS.reset}` : `${COLORS.red}No${COLORS.reset}`}`);
67
+ lines.push(` Approval workflows:${result.hasApprovals ? `${COLORS.green}Yes${COLORS.reset}` : `${COLORS.yellow} No${COLORS.reset}`}`);
68
+ lines.push("");
69
+
70
+ // Score
71
+ const criticals = result.findings.filter((f) => f.severity === "critical").length;
72
+ const warnings = result.findings.filter((f) => f.severity === "warning").length;
73
+
74
+ lines.push(` ${COLORS.bold}Agent Security Score: ${result.score}/10${COLORS.reset} ${scoreBar(result.score)} Grade: ${gradeFromScore(result.score)}`);
75
+ lines.push(` ${COLORS.gray}${criticals} critical, ${warnings} warnings${COLORS.reset}`);
76
+ lines.push("");
77
+
78
+ // CTAs
79
+ lines.push(` ${COLORS.gray}Learn more:${COLORS.reset} https://github.com/authora-dev/awesome-agent-security`);
80
+ lines.push(` ${COLORS.gray}Fix issues:${COLORS.reset} https://authora.dev/get-started`);
81
+ lines.push("");
82
+
83
+ return lines.join("\n");
84
+ }
package/src/scanner.ts ADDED
@@ -0,0 +1,276 @@
1
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
2
+ import { join, extname } from "path";
3
+
4
+ export interface Finding {
5
+ severity: "critical" | "warning" | "info" | "pass";
6
+ category: string;
7
+ message: string;
8
+ file?: string;
9
+ line?: number;
10
+ fix?: string;
11
+ }
12
+
13
+ export interface ScanResult {
14
+ findings: Finding[];
15
+ score: number;
16
+ agents: number;
17
+ mcpServers: number;
18
+ hasIdentityLayer: boolean;
19
+ hasDelegation: boolean;
20
+ hasAuditLog: boolean;
21
+ hasApprovals: boolean;
22
+ scannedFiles: number;
23
+ }
24
+
25
+ const CODE_EXTENSIONS = new Set([
26
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
27
+ ".py", ".go", ".rs", ".java", ".rb",
28
+ ".json", ".yaml", ".yml", ".toml", ".env",
29
+ ]);
30
+
31
+ const MAX_FILES = 500;
32
+ const MAX_FILE_SIZE = 100_000; // 100KB
33
+
34
+ function walkDir(dir: string, files: string[] = [], depth = 0): string[] {
35
+ if (depth > 8 || files.length > MAX_FILES) return files;
36
+
37
+ try {
38
+ const entries = readdirSync(dir);
39
+ for (const entry of entries) {
40
+ if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" ||
41
+ entry === "build" || entry === "__pycache__" || entry === "venv" ||
42
+ entry === ".git" || entry === "vendor") continue;
43
+
44
+ const fullPath = join(dir, entry);
45
+ try {
46
+ const stat = statSync(fullPath);
47
+ if (stat.isDirectory()) {
48
+ walkDir(fullPath, files, depth + 1);
49
+ } else if (stat.isFile() && CODE_EXTENSIONS.has(extname(entry)) && stat.size < MAX_FILE_SIZE) {
50
+ files.push(fullPath);
51
+ }
52
+ } catch {
53
+ // Skip inaccessible files
54
+ }
55
+ }
56
+ } catch {
57
+ // Skip inaccessible directories
58
+ }
59
+
60
+ return files;
61
+ }
62
+
63
+ function readFile(path: string): string {
64
+ try {
65
+ return readFileSync(path, "utf-8");
66
+ } catch {
67
+ return "";
68
+ }
69
+ }
70
+
71
+ export async function scanDirectory(dir: string): Promise<ScanResult> {
72
+ const findings: Finding[] = [];
73
+ let agents = 0;
74
+ let mcpServers = 0;
75
+ let hasIdentityLayer = false;
76
+ let hasDelegation = false;
77
+ let hasAuditLog = false;
78
+ let hasApprovals = false;
79
+
80
+ const files = walkDir(dir);
81
+
82
+ for (const file of files) {
83
+ const content = readFile(file);
84
+ if (!content) continue;
85
+ const lower = content.toLowerCase();
86
+ const relPath = file.replace(dir + "/", "");
87
+
88
+ // Detect agents
89
+ if (/\bagent\b/i.test(content) && (/\bcreate.*agent|agent.*config|new.*agent|agent.*class\b/i.test(content))) {
90
+ agents++;
91
+ }
92
+
93
+ // Detect MCP servers
94
+ if (/mcp.*server|mcpserver|model.*context.*protocol/i.test(lower)) {
95
+ mcpServers++;
96
+ }
97
+
98
+ // Check for identity layer
99
+ if (/authora|@authora\/sdk|agent.*identity|ed25519.*agent|agent.*keypair/i.test(lower)) {
100
+ hasIdentityLayer = true;
101
+ }
102
+
103
+ // Check for delegation
104
+ if (/delegation.*chain|delegate.*permission|token.*exchange|rfc.*8693/i.test(lower)) {
105
+ hasDelegation = true;
106
+ }
107
+
108
+ // Check for audit logging
109
+ if (/audit.*log|agent.*audit|action.*log.*agent/i.test(lower)) {
110
+ hasAuditLog = true;
111
+ }
112
+
113
+ // Check for approvals
114
+ if (/approval.*workflow|human.*in.*loop|require.*approval/i.test(lower)) {
115
+ hasApprovals = true;
116
+ }
117
+
118
+ // --- CRITICAL: Shared API keys ---
119
+ const apiKeyPattern = /(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|AZURE_API_KEY|API_KEY)\s*[=:]\s*["']?[A-Za-z0-9_-]{20,}/g;
120
+ const envFile = extname(file) === ".env";
121
+ if (envFile) {
122
+ const keys = content.match(apiKeyPattern);
123
+ if (keys && keys.length > 0) {
124
+ // Check if multiple agents might share this key
125
+ const agentFiles = files.filter((f) => {
126
+ const fc = readFile(f);
127
+ return /\bagent\b/i.test(fc) && /API_KEY|api_key|apiKey/i.test(fc);
128
+ });
129
+ if (agentFiles.length > 1) {
130
+ findings.push({
131
+ severity: "critical",
132
+ category: "credentials",
133
+ message: `Shared API key may be used by ${agentFiles.length} agent files`,
134
+ file: relPath,
135
+ fix: "Give each agent its own credentials with scoped permissions",
136
+ });
137
+ }
138
+ }
139
+ }
140
+
141
+ // --- CRITICAL: Hardcoded secrets in code ---
142
+ if (!envFile) {
143
+ const hardcodedSecrets = content.match(/(?:sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|xoxb-[0-9]+-[a-zA-Z0-9]+)/g);
144
+ if (hardcodedSecrets) {
145
+ findings.push({
146
+ severity: "critical",
147
+ category: "credentials",
148
+ message: `Hardcoded secret found in source code`,
149
+ file: relPath,
150
+ fix: "Move secrets to environment variables and use a secrets manager",
151
+ });
152
+ }
153
+ }
154
+
155
+ // --- WARNING: MCP server without auth ---
156
+ if (/mcp.*server|createServer/i.test(content) && !/auth|authentication|authorization|token|apiKey/i.test(content)) {
157
+ findings.push({
158
+ severity: "warning",
159
+ category: "mcp",
160
+ message: "MCP server detected without visible auth configuration",
161
+ file: relPath,
162
+ fix: "Add authentication to your MCP server (API key, JWT, or Ed25519 signature verification)",
163
+ });
164
+ }
165
+
166
+ // --- WARNING: Broad agent permissions ---
167
+ if (/\*.*permission|admin.*role|full.*access|sudo|root/i.test(content) && /agent/i.test(content)) {
168
+ findings.push({
169
+ severity: "warning",
170
+ category: "permissions",
171
+ message: "Agent may have overly broad permissions",
172
+ file: relPath,
173
+ fix: "Apply least-privilege: give agents only the permissions they need for their specific task",
174
+ });
175
+ }
176
+
177
+ // --- WARNING: No error handling on tool calls ---
178
+ if (/tool.*call|callTool|execute.*tool/i.test(content) && !/try|catch|error/i.test(content)) {
179
+ findings.push({
180
+ severity: "warning",
181
+ category: "resilience",
182
+ message: "Agent tool calls without error handling",
183
+ file: relPath,
184
+ fix: "Wrap tool calls in try/catch with proper error reporting and fallback behavior",
185
+ });
186
+ }
187
+
188
+ // --- INFO: Agent without timeout ---
189
+ if (/agent/i.test(content) && /async|await|fetch|request/i.test(content) && !/timeout|AbortSignal|signal/i.test(content)) {
190
+ findings.push({
191
+ severity: "info",
192
+ category: "resilience",
193
+ message: "Agent operations without timeout -- could run indefinitely",
194
+ file: relPath,
195
+ fix: "Add timeouts to agent operations to prevent runaway processes",
196
+ });
197
+ }
198
+ }
199
+
200
+ // --- Structural findings ---
201
+ if (!hasIdentityLayer) {
202
+ findings.push({
203
+ severity: "critical",
204
+ category: "identity",
205
+ message: "No agent identity layer detected",
206
+ fix: "Add cryptographic agent identities so each agent has a verifiable, unique identity. See: https://authora.dev/get-started",
207
+ });
208
+ }
209
+
210
+ if (!hasDelegation && agents > 1) {
211
+ findings.push({
212
+ severity: "warning",
213
+ category: "delegation",
214
+ message: "No delegation chains -- agents may inherit unlimited permissions",
215
+ fix: "Implement delegation chains (RFC 8693) so agents receive scoped, time-bound authority",
216
+ });
217
+ }
218
+
219
+ if (!hasAuditLog) {
220
+ findings.push({
221
+ severity: "warning",
222
+ category: "audit",
223
+ message: "No audit logging for agent actions detected",
224
+ fix: "Log every agent action with: who (agent ID), what (action), when (timestamp), authorized by (delegation chain)",
225
+ });
226
+ }
227
+
228
+ if (!hasApprovals && agents > 0) {
229
+ findings.push({
230
+ severity: "info",
231
+ category: "approvals",
232
+ message: "No approval workflows for sensitive agent actions",
233
+ fix: "Add human-in-the-loop approval for high-risk operations (production deploys, data access, secret rotation)",
234
+ });
235
+ }
236
+
237
+ if (mcpServers > 0 && !hasIdentityLayer) {
238
+ findings.push({
239
+ severity: "critical",
240
+ category: "mcp",
241
+ message: `${mcpServers} MCP server(s) found but no agent identity -- any client can call any tool`,
242
+ fix: "Add agent identity verification to your MCP servers. See: https://authora.dev/developers/mcp",
243
+ });
244
+ }
245
+
246
+ // Calculate score (0-10) based on CATEGORIES not raw count
247
+ // This prevents large codebases from being penalized for having many files
248
+ const criticalCategories = new Set(findings.filter((f) => f.severity === "critical").map((f) => f.category));
249
+ const warningCategories = new Set(findings.filter((f) => f.severity === "warning").map((f) => f.category));
250
+ const infoCategories = new Set(findings.filter((f) => f.severity === "info").map((f) => f.category));
251
+
252
+ let score = 10;
253
+ score -= criticalCategories.size * 2.5;
254
+ score -= warningCategories.size * 1.5;
255
+ score -= infoCategories.size * 0.5;
256
+
257
+ // Bonus for good practices (max +4)
258
+ if (hasIdentityLayer) score += 1;
259
+ if (hasDelegation) score += 1;
260
+ if (hasAuditLog) score += 1;
261
+ if (hasApprovals) score += 1;
262
+
263
+ score = Math.max(0, Math.min(10, Math.round(score * 10) / 10));
264
+
265
+ return {
266
+ findings,
267
+ score,
268
+ agents,
269
+ mcpServers,
270
+ hasIdentityLayer,
271
+ hasDelegation,
272
+ hasAuditLog,
273
+ hasApprovals,
274
+ scannedFiles: files.length,
275
+ };
276
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }