@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.
- package/.github/workflows/publish.yml +32 -0
- package/LICENSE +5 -0
- package/README.md +90 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +37 -0
- package/dist/reporter.d.ts +2 -0
- package/dist/reporter.js +77 -0
- package/dist/scanner.d.ts +20 -0
- package/dist/scanner.js +234 -0
- package/package.json +33 -0
- package/src/cli.ts +46 -0
- package/src/reporter.ts +84 -0
- package/src/scanner.ts +276 -0
- package/tsconfig.json +14 -0
|
@@ -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
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
|
+

|
|
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
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(` `);
|
|
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
|
+
});
|
package/dist/reporter.js
ADDED
|
@@ -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>;
|
package/dist/scanner.js
ADDED
|
@@ -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(` `);
|
|
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
|
+
});
|
package/src/reporter.ts
ADDED
|
@@ -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
|
+
}
|