agent-audit 0.1.0__py3-none-any.whl
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.
- agent_audit/__init__.py +3 -0
- agent_audit/__main__.py +13 -0
- agent_audit/cli/__init__.py +1 -0
- agent_audit/cli/commands/__init__.py +1 -0
- agent_audit/cli/commands/init.py +44 -0
- agent_audit/cli/commands/inspect.py +236 -0
- agent_audit/cli/commands/scan.py +329 -0
- agent_audit/cli/formatters/__init__.py +1 -0
- agent_audit/cli/formatters/json.py +138 -0
- agent_audit/cli/formatters/sarif.py +155 -0
- agent_audit/cli/formatters/terminal.py +221 -0
- agent_audit/cli/main.py +34 -0
- agent_audit/config/__init__.py +1 -0
- agent_audit/config/ignore.py +477 -0
- agent_audit/core_utils/__init__.py +1 -0
- agent_audit/models/__init__.py +18 -0
- agent_audit/models/finding.py +159 -0
- agent_audit/models/risk.py +77 -0
- agent_audit/models/tool.py +182 -0
- agent_audit/rules/__init__.py +6 -0
- agent_audit/rules/engine.py +503 -0
- agent_audit/rules/loader.py +160 -0
- agent_audit/scanners/__init__.py +5 -0
- agent_audit/scanners/base.py +32 -0
- agent_audit/scanners/config_scanner.py +390 -0
- agent_audit/scanners/mcp_config_scanner.py +321 -0
- agent_audit/scanners/mcp_inspector.py +421 -0
- agent_audit/scanners/python_scanner.py +544 -0
- agent_audit/scanners/secret_scanner.py +521 -0
- agent_audit/utils/__init__.py +21 -0
- agent_audit/utils/compat.py +98 -0
- agent_audit/utils/mcp_client.py +343 -0
- agent_audit/version.py +3 -0
- agent_audit-0.1.0.dist-info/METADATA +219 -0
- agent_audit-0.1.0.dist-info/RECORD +37 -0
- agent_audit-0.1.0.dist-info/WHEEL +4 -0
- agent_audit-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""MCP protocol client with STDIO and SSE transports."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Windows subprocess creation flags
|
|
14
|
+
_IS_WINDOWS = sys.platform == "win32"
|
|
15
|
+
_CREATE_NO_WINDOW = 0x08000000 if _IS_WINDOWS else 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TransportType(Enum):
|
|
19
|
+
"""MCP transport types."""
|
|
20
|
+
STDIO = "stdio"
|
|
21
|
+
SSE = "sse"
|
|
22
|
+
STREAMABLE_HTTP = "streamable_http"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BaseMCPTransport(ABC):
|
|
26
|
+
"""Base class for MCP transport implementations."""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def connect(self):
|
|
30
|
+
"""Establish connection to MCP server."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
async def send(self, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
35
|
+
"""Send a JSON-RPC request and wait for response."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def notify(self, method: str, params: Dict[str, Any]):
|
|
40
|
+
"""Send a JSON-RPC notification (no response expected)."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
async def close(self):
|
|
45
|
+
"""Close the connection."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StdioTransport(BaseMCPTransport):
|
|
50
|
+
"""
|
|
51
|
+
STDIO transport for local MCP servers.
|
|
52
|
+
|
|
53
|
+
Communicates with MCP server via stdin/stdout using JSON-RPC.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, command: str, args: Optional[list] = None, env: Optional[dict] = None):
|
|
57
|
+
"""
|
|
58
|
+
Initialize STDIO transport.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
command: Command to execute (e.g., "python", "node")
|
|
62
|
+
args: Command arguments (e.g., ["server.py"])
|
|
63
|
+
env: Environment variables
|
|
64
|
+
"""
|
|
65
|
+
self.command = command
|
|
66
|
+
self.args = args or []
|
|
67
|
+
self.env = env
|
|
68
|
+
self.process: Optional[asyncio.subprocess.Process] = None
|
|
69
|
+
self._request_id = 0
|
|
70
|
+
self._pending_requests: Dict[int, asyncio.Future] = {}
|
|
71
|
+
self._reader_task: Optional[asyncio.Task] = None
|
|
72
|
+
|
|
73
|
+
async def connect(self):
|
|
74
|
+
"""Start the MCP server process."""
|
|
75
|
+
import os
|
|
76
|
+
|
|
77
|
+
# Prepare environment
|
|
78
|
+
process_env = os.environ.copy()
|
|
79
|
+
if self.env:
|
|
80
|
+
process_env.update(self.env)
|
|
81
|
+
|
|
82
|
+
# Build kwargs for subprocess creation
|
|
83
|
+
kwargs: Dict[str, Any] = {
|
|
84
|
+
"stdin": asyncio.subprocess.PIPE,
|
|
85
|
+
"stdout": asyncio.subprocess.PIPE,
|
|
86
|
+
"stderr": asyncio.subprocess.PIPE,
|
|
87
|
+
"env": process_env,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# On Windows, prevent console window from appearing
|
|
91
|
+
if _IS_WINDOWS:
|
|
92
|
+
kwargs["creationflags"] = _CREATE_NO_WINDOW
|
|
93
|
+
|
|
94
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
95
|
+
self.command, *self.args,
|
|
96
|
+
**kwargs
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Start background reader
|
|
100
|
+
self._reader_task = asyncio.create_task(self._read_responses())
|
|
101
|
+
|
|
102
|
+
async def _read_responses(self):
|
|
103
|
+
"""Background task to read responses from server."""
|
|
104
|
+
try:
|
|
105
|
+
while self.process and self.process.stdout:
|
|
106
|
+
line = await self.process.stdout.readline()
|
|
107
|
+
if not line:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
response = json.loads(line.decode())
|
|
112
|
+
request_id = response.get('id')
|
|
113
|
+
|
|
114
|
+
if request_id is not None and request_id in self._pending_requests:
|
|
115
|
+
future = self._pending_requests.pop(request_id)
|
|
116
|
+
if not future.done():
|
|
117
|
+
future.set_result(response)
|
|
118
|
+
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
logger.warning(f"Invalid JSON from server: {line}")
|
|
121
|
+
|
|
122
|
+
except asyncio.CancelledError:
|
|
123
|
+
pass
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.error(f"Error reading from server: {e}")
|
|
126
|
+
|
|
127
|
+
async def send(self, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
128
|
+
"""Send a JSON-RPC request and wait for response."""
|
|
129
|
+
if not self.process or not self.process.stdin:
|
|
130
|
+
raise RuntimeError("Not connected")
|
|
131
|
+
|
|
132
|
+
self._request_id += 1
|
|
133
|
+
request_id = self._request_id
|
|
134
|
+
|
|
135
|
+
request = {
|
|
136
|
+
"jsonrpc": "2.0",
|
|
137
|
+
"id": request_id,
|
|
138
|
+
"method": method,
|
|
139
|
+
"params": params
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Create future for response
|
|
143
|
+
future: asyncio.Future = asyncio.get_event_loop().create_future()
|
|
144
|
+
self._pending_requests[request_id] = future
|
|
145
|
+
|
|
146
|
+
# Send request
|
|
147
|
+
request_bytes = json.dumps(request).encode() + b"\n"
|
|
148
|
+
self.process.stdin.write(request_bytes)
|
|
149
|
+
await self.process.stdin.drain()
|
|
150
|
+
|
|
151
|
+
# Wait for response with timeout
|
|
152
|
+
try:
|
|
153
|
+
response = await asyncio.wait_for(future, timeout=30)
|
|
154
|
+
except asyncio.TimeoutError:
|
|
155
|
+
self._pending_requests.pop(request_id, None)
|
|
156
|
+
raise TimeoutError(f"Request {method} timed out")
|
|
157
|
+
|
|
158
|
+
if "error" in response:
|
|
159
|
+
error = response["error"]
|
|
160
|
+
raise RuntimeError(f"MCP Error: {error.get('message', error)}")
|
|
161
|
+
|
|
162
|
+
return response.get("result", {})
|
|
163
|
+
|
|
164
|
+
async def notify(self, method: str, params: Dict[str, Any]):
|
|
165
|
+
"""Send a JSON-RPC notification."""
|
|
166
|
+
if not self.process or not self.process.stdin:
|
|
167
|
+
raise RuntimeError("Not connected")
|
|
168
|
+
|
|
169
|
+
notification = {
|
|
170
|
+
"jsonrpc": "2.0",
|
|
171
|
+
"method": method,
|
|
172
|
+
"params": params
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
notification_bytes = json.dumps(notification).encode() + b"\n"
|
|
176
|
+
self.process.stdin.write(notification_bytes)
|
|
177
|
+
await self.process.stdin.drain()
|
|
178
|
+
|
|
179
|
+
async def close(self):
|
|
180
|
+
"""Terminate the server process."""
|
|
181
|
+
if self._reader_task:
|
|
182
|
+
self._reader_task.cancel()
|
|
183
|
+
try:
|
|
184
|
+
await self._reader_task
|
|
185
|
+
except asyncio.CancelledError:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
if self.process:
|
|
189
|
+
self.process.terminate()
|
|
190
|
+
try:
|
|
191
|
+
await asyncio.wait_for(self.process.wait(), timeout=5)
|
|
192
|
+
except asyncio.TimeoutError:
|
|
193
|
+
self.process.kill()
|
|
194
|
+
await self.process.wait()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class SSETransport(BaseMCPTransport):
|
|
198
|
+
"""
|
|
199
|
+
SSE (Server-Sent Events) transport for remote MCP servers.
|
|
200
|
+
|
|
201
|
+
Uses HTTP POST for requests and SSE for responses.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self, url: str):
|
|
205
|
+
"""
|
|
206
|
+
Initialize SSE transport.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
url: SSE endpoint URL
|
|
210
|
+
"""
|
|
211
|
+
self.url = url
|
|
212
|
+
self.session = None
|
|
213
|
+
self._request_id = 0
|
|
214
|
+
self._endpoint: Optional[str] = None
|
|
215
|
+
|
|
216
|
+
async def connect(self):
|
|
217
|
+
"""Connect to the SSE endpoint and get the messages URL."""
|
|
218
|
+
import aiohttp
|
|
219
|
+
|
|
220
|
+
self.session = aiohttp.ClientSession()
|
|
221
|
+
|
|
222
|
+
# Connect to SSE endpoint to get messages URL
|
|
223
|
+
try:
|
|
224
|
+
async with self.session.get(self.url) as response:
|
|
225
|
+
if response.status != 200:
|
|
226
|
+
raise RuntimeError(f"SSE connection failed: {response.status}")
|
|
227
|
+
|
|
228
|
+
# Read SSE events to find endpoint
|
|
229
|
+
async for line in response.content:
|
|
230
|
+
decoded = line.decode().strip()
|
|
231
|
+
|
|
232
|
+
if decoded.startswith("event: endpoint"):
|
|
233
|
+
next_line = await response.content.readline()
|
|
234
|
+
data = next_line.decode().strip()
|
|
235
|
+
if data.startswith("data: "):
|
|
236
|
+
self._endpoint = data[6:]
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
await self.close()
|
|
241
|
+
raise RuntimeError(f"Failed to connect to SSE: {e}")
|
|
242
|
+
|
|
243
|
+
if not self._endpoint:
|
|
244
|
+
raise RuntimeError("Failed to get messages endpoint from SSE")
|
|
245
|
+
|
|
246
|
+
async def send(self, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
247
|
+
"""Send a JSON-RPC request via HTTP POST."""
|
|
248
|
+
if not self.session or not self._endpoint:
|
|
249
|
+
raise RuntimeError("Not connected")
|
|
250
|
+
|
|
251
|
+
self._request_id += 1
|
|
252
|
+
request = {
|
|
253
|
+
"jsonrpc": "2.0",
|
|
254
|
+
"id": self._request_id,
|
|
255
|
+
"method": method,
|
|
256
|
+
"params": params
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async with self.session.post(self._endpoint, json=request) as response:
|
|
260
|
+
if response.status != 200:
|
|
261
|
+
raise RuntimeError(f"Request failed: {response.status}")
|
|
262
|
+
|
|
263
|
+
result = await response.json()
|
|
264
|
+
|
|
265
|
+
if "error" in result:
|
|
266
|
+
error = result["error"]
|
|
267
|
+
raise RuntimeError(f"MCP Error: {error.get('message', error)}")
|
|
268
|
+
|
|
269
|
+
return result.get("result", {})
|
|
270
|
+
|
|
271
|
+
async def notify(self, method: str, params: Dict[str, Any]):
|
|
272
|
+
"""Send a JSON-RPC notification via HTTP POST."""
|
|
273
|
+
if not self.session or not self._endpoint:
|
|
274
|
+
raise RuntimeError("Not connected")
|
|
275
|
+
|
|
276
|
+
notification = {
|
|
277
|
+
"jsonrpc": "2.0",
|
|
278
|
+
"method": method,
|
|
279
|
+
"params": params
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async with self.session.post(self._endpoint, json=notification):
|
|
283
|
+
pass # Notifications don't expect a response
|
|
284
|
+
|
|
285
|
+
async def close(self):
|
|
286
|
+
"""Close the HTTP session."""
|
|
287
|
+
if self.session:
|
|
288
|
+
await self.session.close()
|
|
289
|
+
self.session = None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def create_client(target: str, transport_type: TransportType) -> BaseMCPTransport:
|
|
293
|
+
"""
|
|
294
|
+
Factory function to create an appropriate MCP transport.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
target: Target specification
|
|
298
|
+
- For STDIO: "python server.py" or command with args
|
|
299
|
+
- For SSE: "https://example.com/sse"
|
|
300
|
+
transport_type: Type of transport to use
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Connected MCP transport instance
|
|
304
|
+
"""
|
|
305
|
+
if transport_type == TransportType.STDIO:
|
|
306
|
+
# Parse command and arguments
|
|
307
|
+
parts = target.split()
|
|
308
|
+
if not parts:
|
|
309
|
+
raise ValueError("Empty command for STDIO transport")
|
|
310
|
+
|
|
311
|
+
command = parts[0]
|
|
312
|
+
args = parts[1:]
|
|
313
|
+
|
|
314
|
+
transport = StdioTransport(command, args)
|
|
315
|
+
await transport.connect()
|
|
316
|
+
return transport
|
|
317
|
+
|
|
318
|
+
elif transport_type == TransportType.SSE:
|
|
319
|
+
transport = SSETransport(target)
|
|
320
|
+
await transport.connect()
|
|
321
|
+
return transport
|
|
322
|
+
|
|
323
|
+
else:
|
|
324
|
+
raise ValueError(f"Unsupported transport type: {transport_type}")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def infer_transport_type(target: str) -> TransportType:
|
|
328
|
+
"""
|
|
329
|
+
Infer transport type from target string.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
target: Target specification
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Inferred TransportType
|
|
336
|
+
"""
|
|
337
|
+
if target.startswith(("http://", "https://")):
|
|
338
|
+
return TransportType.SSE
|
|
339
|
+
elif target.startswith("stdio"):
|
|
340
|
+
return TransportType.STDIO
|
|
341
|
+
else:
|
|
342
|
+
# Assume it's a command for STDIO
|
|
343
|
+
return TransportType.STDIO
|
agent_audit/version.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: agent-audit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Security scanner for AI agents and MCP configurations - Based on OWASP Agentic Top 10
|
|
5
|
+
Home-page: https://github.com/HeadyZhang/agent-audit
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ai,agent,security,mcp,audit,owasp,vulnerability,scanner
|
|
8
|
+
Author: Agent Security Team
|
|
9
|
+
Author-email: security@example.com
|
|
10
|
+
Requires-Python: >=3.9,<4.0
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Security
|
|
24
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
25
|
+
Classifier: Topic :: Software Development :: Testing
|
|
26
|
+
Requires-Dist: aiofiles (>=23.0,<24.0)
|
|
27
|
+
Requires-Dist: aiohttp (>=3.9,<4.0)
|
|
28
|
+
Requires-Dist: click (>=8.1.0,<9.0.0)
|
|
29
|
+
Requires-Dist: pydantic (>=2.0,<3.0)
|
|
30
|
+
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
31
|
+
Requires-Dist: rich (>=13.0.0,<14.0.0)
|
|
32
|
+
Project-URL: Bug Tracker, https://github.com/HeadyZhang/agent-audit/issues
|
|
33
|
+
Project-URL: Changelog, https://github.com/HeadyZhang/agent-audit/releases
|
|
34
|
+
Project-URL: Documentation, https://github.com/HeadyZhang/agent-audit#readme
|
|
35
|
+
Project-URL: Repository, https://github.com/HeadyZhang/agent-audit
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Agent Audit
|
|
39
|
+
|
|
40
|
+
[](https://badge.fury.io/py/agent-audit)
|
|
41
|
+
[](https://pypi.org/project/agent-audit/)
|
|
42
|
+
[](https://opensource.org/licenses/MIT)
|
|
43
|
+
[](https://github.com/HeadyZhang/agent-audit/actions/workflows/ci.yml)
|
|
44
|
+
[](https://codecov.io/gh/HeadyZhang/agent-audit)
|
|
45
|
+
|
|
46
|
+
> 🛡️ Security scanner for AI agents and MCP configurations. Detects vulnerabilities based on the **OWASP Agentic Top 10**.
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<img src="docs/demo.gif" alt="Agent Audit Demo" width="800">
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
## ✨ Features
|
|
53
|
+
|
|
54
|
+
- **🔍 Python AST Scanning** - Detects dangerous patterns like `shell=True`, `eval()`, and tainted input flows
|
|
55
|
+
- **⚙️ MCP Configuration Scanning** - Validates MCP server configurations for security issues
|
|
56
|
+
- **🔐 Secret Detection** - Finds hardcoded credentials (AWS keys, API tokens, private keys)
|
|
57
|
+
- **🌐 Runtime MCP Inspection** - Probes MCP servers without executing tools ("Agent Nmap")
|
|
58
|
+
- **📊 Multiple Output Formats** - Terminal, JSON, SARIF (for GitHub Code Scanning), Markdown
|
|
59
|
+
|
|
60
|
+
## 🚀 Quick Start
|
|
61
|
+
|
|
62
|
+
### Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install agent-audit
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Basic Usage
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Scan current directory
|
|
72
|
+
agent-audit scan .
|
|
73
|
+
|
|
74
|
+
# Scan with JSON output
|
|
75
|
+
agent-audit scan ./my-agent --format json
|
|
76
|
+
|
|
77
|
+
# Scan with SARIF output for GitHub Code Scanning
|
|
78
|
+
agent-audit scan . --format sarif --output results.sarif
|
|
79
|
+
|
|
80
|
+
# Fail CI on critical findings only
|
|
81
|
+
agent-audit scan . --fail-on critical
|
|
82
|
+
|
|
83
|
+
# Inspect an MCP server at runtime
|
|
84
|
+
agent-audit inspect stdio -- npx -y @modelcontextprotocol/server-filesystem /tmp
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 🔗 GitHub Action
|
|
88
|
+
|
|
89
|
+
Add Agent Audit to your CI/CD pipeline with just a few lines:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
name: Security Scan
|
|
93
|
+
on: [push, pull_request]
|
|
94
|
+
|
|
95
|
+
jobs:
|
|
96
|
+
agent-audit:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
steps:
|
|
99
|
+
- uses: actions/checkout@v4
|
|
100
|
+
|
|
101
|
+
- name: Run Agent Audit
|
|
102
|
+
uses: HeadyZhang/agent-audit@v1
|
|
103
|
+
with:
|
|
104
|
+
path: '.'
|
|
105
|
+
fail-on: 'high'
|
|
106
|
+
upload-sarif: 'true'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Action Inputs
|
|
110
|
+
|
|
111
|
+
| Input | Description | Default |
|
|
112
|
+
|-------|-------------|---------|
|
|
113
|
+
| `path` | Path to scan | `.` |
|
|
114
|
+
| `format` | Output format: `terminal`, `json`, `sarif`, `markdown` | `sarif` |
|
|
115
|
+
| `severity` | Minimum severity to report: `info`, `low`, `medium`, `high`, `critical` | `low` |
|
|
116
|
+
| `fail-on` | Exit with error if findings at this severity | `high` |
|
|
117
|
+
| `baseline` | Path to baseline file for incremental scanning | - |
|
|
118
|
+
| `upload-sarif` | Upload SARIF to GitHub Security tab | `true` |
|
|
119
|
+
|
|
120
|
+
## 🎯 Detected Issues
|
|
121
|
+
|
|
122
|
+
| Rule ID | Title | Severity |
|
|
123
|
+
|---------|-------|----------|
|
|
124
|
+
| AGENT-001 | Command Injection via Unsanitized Input | 🔴 Critical |
|
|
125
|
+
| AGENT-002 | Excessive Agent Permissions | 🟡 Medium |
|
|
126
|
+
| AGENT-003 | Potential Data Exfiltration Chain | 🟠 High |
|
|
127
|
+
| AGENT-004 | Hardcoded Credentials | 🔴 Critical |
|
|
128
|
+
| AGENT-005 | Unverified MCP Server | 🟠 High |
|
|
129
|
+
|
|
130
|
+
## ⚙️ Configuration
|
|
131
|
+
|
|
132
|
+
Create a `.agent-audit.yaml` file to customize scanning:
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
# Allowed network hosts (reduces AGENT-003 confidence)
|
|
136
|
+
allowed_hosts:
|
|
137
|
+
- "*.internal.company.com"
|
|
138
|
+
- "api.openai.com"
|
|
139
|
+
|
|
140
|
+
# Ignore rules
|
|
141
|
+
ignore:
|
|
142
|
+
- rule_id: AGENT-003
|
|
143
|
+
paths:
|
|
144
|
+
- "auth/**"
|
|
145
|
+
reason: "Auth module legitimately communicates externally"
|
|
146
|
+
|
|
147
|
+
# Scan settings
|
|
148
|
+
scan:
|
|
149
|
+
exclude:
|
|
150
|
+
- "tests/**"
|
|
151
|
+
- "venv/**"
|
|
152
|
+
min_severity: low
|
|
153
|
+
fail_on: high
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 📈 Baseline Scanning
|
|
157
|
+
|
|
158
|
+
Track new findings incrementally:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Save current findings as baseline
|
|
162
|
+
agent-audit scan . --save-baseline baseline.json
|
|
163
|
+
|
|
164
|
+
# Only report new findings
|
|
165
|
+
agent-audit scan . --baseline baseline.json
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## 📖 CLI Reference
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
Usage: agent-audit [OPTIONS] COMMAND [ARGS]...
|
|
172
|
+
|
|
173
|
+
Commands:
|
|
174
|
+
scan Scan agent code and configurations
|
|
175
|
+
inspect Inspect an MCP server at runtime
|
|
176
|
+
init Initialize configuration file
|
|
177
|
+
|
|
178
|
+
Options:
|
|
179
|
+
--version Show version
|
|
180
|
+
-v Enable verbose output
|
|
181
|
+
-q Only show errors
|
|
182
|
+
--help Show this message
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 🛠️ Development
|
|
186
|
+
|
|
187
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Clone the repository
|
|
191
|
+
git clone https://github.com/HeadyZhang/agent-audit
|
|
192
|
+
cd agent-security-suite
|
|
193
|
+
|
|
194
|
+
# Install dependencies
|
|
195
|
+
cd packages/core && poetry install
|
|
196
|
+
cd ../audit && poetry install
|
|
197
|
+
|
|
198
|
+
# Run tests
|
|
199
|
+
poetry run pytest tests/ -v
|
|
200
|
+
|
|
201
|
+
# Run the scanner
|
|
202
|
+
poetry run agent-audit scan .
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## 📄 License
|
|
206
|
+
|
|
207
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
208
|
+
|
|
209
|
+
## 🙏 Acknowledgments
|
|
210
|
+
|
|
211
|
+
- Based on the [OWASP Agentic Security Top 10](https://owasp.org/www-project-agentic-security/)
|
|
212
|
+
- Inspired by the need for better AI agent security tooling
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
<p align="center">
|
|
217
|
+
Made with ❤️ for the AI agent security community
|
|
218
|
+
</p>
|
|
219
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
agent_audit/__init__.py,sha256=D8k1t9MieJMyq0in1RsJAOgZfGW1F5r-djHTspnXEG0,98
|
|
2
|
+
agent_audit/__main__.py,sha256=juDPvuN3eWRpjRm94fxhPUyM8VBMmg8xOS59iEYdAMM,340
|
|
3
|
+
agent_audit/cli/__init__.py,sha256=rsgbDj0L-wbwEfkY4bWRbr2QPUlDrtMp3z9k7qEtXAo,34
|
|
4
|
+
agent_audit/cli/commands/__init__.py,sha256=YKhyx4t_78Ti9_utLsgSo_iFNqXBCxCPyD0MZtGZwRo,36
|
|
5
|
+
agent_audit/cli/commands/init.py,sha256=agP2lvZoNwcVSrNJGatAb7boQZL5ma3Xcq5K3y0Scsw,1274
|
|
6
|
+
agent_audit/cli/commands/inspect.py,sha256=HFxPrLCGRP1Chtccip_9fr4l49Mvx3rpIbaOHPrwlC8,7697
|
|
7
|
+
agent_audit/cli/commands/scan.py,sha256=5JvH8HsGtDnNsJRwWkR5A1IH10zFg-tuGfbzHAeAd8w,11642
|
|
8
|
+
agent_audit/cli/formatters/__init__.py,sha256=BspmN4xcVYvmSY5fClccAIqwMUqQP7MO9auVwJJbWIM,41
|
|
9
|
+
agent_audit/cli/formatters/json.py,sha256=Wy-cKPmH62oXeHyM4rjQySgB7X4GwJX_7XEcWHK1Pcg,3997
|
|
10
|
+
agent_audit/cli/formatters/sarif.py,sha256=5j4dfuEBIy5HkusWLT0Ha_vW6sJsxcNcTfm4jq-CPH8,5361
|
|
11
|
+
agent_audit/cli/formatters/terminal.py,sha256=naNy-jAqWlrlO_SsL9iSZPsN-4jY1EsHcepVFIE2Fpg,6917
|
|
12
|
+
agent_audit/cli/main.py,sha256=JHKm0VXsNsSWwNLFOurwaUR54PuKVLod1Mat6ypqwss,971
|
|
13
|
+
agent_audit/config/__init__.py,sha256=BHW5R9IU5aMumvd5JLDaeuUu3ZH_kU7qODTKs3tjdak,48
|
|
14
|
+
agent_audit/config/ignore.py,sha256=6tLxp89zR5FOst1Ci6GvVGZhghNm_Q6UyXkFYmAZu1Q,15019
|
|
15
|
+
agent_audit/core_utils/__init__.py,sha256=XtUKNre_-5Ryf9u3brruaWu8kmhWklWYn1brRQg0vJg,49
|
|
16
|
+
agent_audit/models/__init__.py,sha256=0KBDUfUlXOfQPNkx8VDL89DxCjjbQgw2vO-v5xTf2Xs,475
|
|
17
|
+
agent_audit/models/finding.py,sha256=2M8H4gNlZGUMEUTj4svvNGJ3kGu-OagOQS49YH0K5ug,5512
|
|
18
|
+
agent_audit/models/risk.py,sha256=mDpvi5WpvcppTktPgTMw2mSgnfgusn7fU8HXIjMBvN8,2216
|
|
19
|
+
agent_audit/models/tool.py,sha256=hVQW4Q9ZvNDEDNtFMUFvvxwcXTiMw0ZFLb_IQLNSihE,5790
|
|
20
|
+
agent_audit/rules/__init__.py,sha256=L-sssDeN5jVP8_mi6zF4JMGxq4jtvJOQZAt3jKnDnhs,192
|
|
21
|
+
agent_audit/rules/engine.py,sha256=9S6TMfW-0v8XpzGW4vVscTJUo8OUf2K5p7V5qUyoyPE,18179
|
|
22
|
+
agent_audit/rules/loader.py,sha256=Fdwl_0L89s97gP-6TD27S6V-jD_A5TkQMsmr3xtC-is,5157
|
|
23
|
+
agent_audit/scanners/__init__.py,sha256=My758N4VyF-g2ARYPxWLIlyvZBAMm7KFQx3w6FDrZR4,136
|
|
24
|
+
agent_audit/scanners/base.py,sha256=pdSu0lxS5gJtvI7o7c3MFcytzOXezytID0KDUlCyxks,687
|
|
25
|
+
agent_audit/scanners/config_scanner.py,sha256=hkpq-5-Qbm7gCTjdUrcXrQGklK7D89gTmpPaMcMTl-Y,13136
|
|
26
|
+
agent_audit/scanners/mcp_config_scanner.py,sha256=LAtW9F7HFgDIb1W9U4dSFz27YOB3ls_ifF8_XG-lDbU,10666
|
|
27
|
+
agent_audit/scanners/mcp_inspector.py,sha256=8tfeLm_9t5c4KPkaEoRSQwB-94HN3DXbm_86Mbc88Js,15316
|
|
28
|
+
agent_audit/scanners/python_scanner.py,sha256=BO6sfp1lq6WARACqQzueDFbvnSCBHSP0i5SnW7tIhso,20691
|
|
29
|
+
agent_audit/scanners/secret_scanner.py,sha256=moNaesAqc3PFfWEvfu8V8OmO0bhZsGPj8dz_Ha_Cqvc,18686
|
|
30
|
+
agent_audit/utils/__init__.py,sha256=3leLd2NVV013ymtXxJjUg-THvZwUmfYiCBTwJaSsDqg,402
|
|
31
|
+
agent_audit/utils/compat.py,sha256=-TC2CCq1gBislYbVxdqKq7XE7c325GoPbbDF9BovOA8,3040
|
|
32
|
+
agent_audit/utils/mcp_client.py,sha256=TjW-aUcwnOgnSuAvk352HxiHO0sfd8Vb3Y2ySqA3-9M,10631
|
|
33
|
+
agent_audit/version.py,sha256=YeuRVUF33dlS0mzQu2Huk9qG-_by3kSZEAwoBB2VjCA,66
|
|
34
|
+
agent_audit-0.1.0.dist-info/METADATA,sha256=XjaOj5QxB2MeA-zv1DYSagyLliwmsI0dDHAcUM0mkrM,6588
|
|
35
|
+
agent_audit-0.1.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
36
|
+
agent_audit-0.1.0.dist-info/entry_points.txt,sha256=Fc0jPCn3KKoZttTJATiXY2QljohsIdTv-J29TRNU_Gc,56
|
|
37
|
+
agent_audit-0.1.0.dist-info/RECORD,,
|