aiscan-security 1.0.0__tar.gz

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.
Files changed (37) hide show
  1. aiscan_security-1.0.0/PKG-INFO +36 -0
  2. aiscan_security-1.0.0/README.md +8 -0
  3. aiscan_security-1.0.0/aiscan/__init__.py +0 -0
  4. aiscan_security-1.0.0/aiscan/api/__init__.py +0 -0
  5. aiscan_security-1.0.0/aiscan/api/routes.py +122 -0
  6. aiscan_security-1.0.0/aiscan/cli.py +342 -0
  7. aiscan_security-1.0.0/aiscan/config.py +20 -0
  8. aiscan_security-1.0.0/aiscan/models.py +47 -0
  9. aiscan_security-1.0.0/aiscan/reporters/__init__.py +0 -0
  10. aiscan_security-1.0.0/aiscan/reporters/console_reporter.py +47 -0
  11. aiscan_security-1.0.0/aiscan/reporters/json_reporter.py +34 -0
  12. aiscan_security-1.0.0/aiscan/rules/__init__.py +0 -0
  13. aiscan_security-1.0.0/aiscan/rules/registry.py +196 -0
  14. aiscan_security-1.0.0/aiscan/scanners/__init__.py +0 -0
  15. aiscan_security-1.0.0/aiscan/scanners/agent_scanner.py +6 -0
  16. aiscan_security-1.0.0/aiscan/scanners/api_scanner.py +6 -0
  17. aiscan_security-1.0.0/aiscan/scanners/base_scanner.py +71 -0
  18. aiscan_security-1.0.0/aiscan/scanners/integration_scanner.py +6 -0
  19. aiscan_security-1.0.0/aiscan/scanners/mcp_scanner.py +6 -0
  20. aiscan_security-1.0.0/aiscan/scanners/prompt_scanner.py +6 -0
  21. aiscan_security-1.0.0/aiscan/sdk/__init__.py +13 -0
  22. aiscan_security-1.0.0/aiscan/sdk/decorators.py +171 -0
  23. aiscan_security-1.0.0/aiscan/sdk/http_client.py +124 -0
  24. aiscan_security-1.0.0/aiscan/sdk/middleware.py +202 -0
  25. aiscan_security-1.0.0/aiscan/sdk/scanner.py +235 -0
  26. aiscan_security-1.0.0/aiscan/utils/__init__.py +0 -0
  27. aiscan_security-1.0.0/aiscan/utils/aggregator.py +15 -0
  28. aiscan_security-1.0.0/aiscan/utils/ai_checker.py +162 -0
  29. aiscan_security-1.0.0/aiscan_security.egg-info/PKG-INFO +36 -0
  30. aiscan_security-1.0.0/aiscan_security.egg-info/SOURCES.txt +35 -0
  31. aiscan_security-1.0.0/aiscan_security.egg-info/dependency_links.txt +1 -0
  32. aiscan_security-1.0.0/aiscan_security.egg-info/entry_points.txt +2 -0
  33. aiscan_security-1.0.0/aiscan_security.egg-info/requires.txt +23 -0
  34. aiscan_security-1.0.0/aiscan_security.egg-info/top_level.txt +1 -0
  35. aiscan_security-1.0.0/pyproject.toml +42 -0
  36. aiscan_security-1.0.0/setup.cfg +4 -0
  37. aiscan_security-1.0.0/setup.py +30 -0
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiscan-security
3
+ Version: 1.0.0
4
+ Summary: AI Security Scanner — scan prompts, agents, APIs, MCP tools and integrations
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: typer>=0.12.0
8
+ Requires-Dist: rich>=13.7.0
9
+ Requires-Dist: pyyaml>=6.0.1
10
+ Requires-Dist: pydantic>=2.7.0
11
+ Requires-Dist: pydantic-settings>=2.2.0
12
+ Requires-Dist: anthropic>=0.28.0
13
+ Requires-Dist: openai>=1.30.0
14
+ Requires-Dist: python-dotenv>=1.0.0
15
+ Requires-Dist: structlog>=24.1.0
16
+ Requires-Dist: twine>=6.2.0
17
+ Provides-Extra: api
18
+ Requires-Dist: fastapi>=0.111.0; extra == "api"
19
+ Requires-Dist: uvicorn[standard]>=0.29.0; extra == "api"
20
+ Provides-Extra: vertex
21
+ Requires-Dist: google-cloud-aiplatform>=1.60.0; extra == "vertex"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
24
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
25
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
26
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
27
+ Dynamic: requires-python
28
+
29
+ # AI Security Scanner
30
+
31
+ Scan prompts, agents, APIs, MCP tools and integrations for AI security risks.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install aiscan
@@ -0,0 +1,8 @@
1
+ # AI Security Scanner
2
+
3
+ Scan prompts, agents, APIs, MCP tools and integrations for AI security risks.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install aiscan
File without changes
File without changes
@@ -0,0 +1,122 @@
1
+ """
2
+ REST API — all 5 scan type endpoints.
3
+ POST /scan/prompt
4
+ POST /scan/agent
5
+ POST /scan/api
6
+ POST /scan/mcp
7
+ POST /scan/integration
8
+ POST /scan/all — runs all 5 scanners, returns combined results
9
+ GET /health
10
+ """
11
+ import tempfile, os
12
+ from fastapi import FastAPI
13
+ from pydantic import BaseModel
14
+ from aiscan.models import ScanType, Severity
15
+ from aiscan.scanners.prompt_scanner import scan_prompt
16
+ from aiscan.scanners.agent_scanner import scan_agent
17
+ from aiscan.scanners.api_scanner import scan_api
18
+ from aiscan.scanners.mcp_scanner import scan_mcp
19
+ from aiscan.scanners.integration_scanner import scan_integration
20
+ from aiscan.utils.aggregator import aggregate
21
+
22
+ app = FastAPI(
23
+ title="AI Security Scanner",
24
+ description="Scan prompts, agents, APIs, MCP tools and integrations for security vulnerabilities",
25
+ version="0.0.1",
26
+ )
27
+
28
+ class ScanRequest(BaseModel):
29
+ content: str
30
+ filename: str = "input.txt"
31
+
32
+ class FindingOut(BaseModel):
33
+ rule_id: str
34
+ severity: str
35
+ threat: str
36
+ title: str
37
+ detail: str
38
+ file: str
39
+ line: int
40
+ fix: str
41
+
42
+ class ScanResponse(BaseModel):
43
+ scan_type: str
44
+ passed: bool
45
+ critical: int
46
+ high: int
47
+ medium: int
48
+ info: int
49
+ total: int
50
+ findings: list[FindingOut]
51
+
52
+
53
+ def _run_scan(req: ScanRequest, scan_fn, scan_type: ScanType, suffix: str = ".txt") -> ScanResponse:
54
+ with tempfile.NamedTemporaryFile(mode="w", suffix=suffix, delete=False, encoding="utf-8") as f:
55
+ f.write(req.content)
56
+ tmp = f.name
57
+ try:
58
+ result = scan_fn(tmp)
59
+ result.findings = aggregate(result.findings)
60
+ return _shape(result, scan_type)
61
+ finally:
62
+ os.unlink(tmp)
63
+
64
+ def _shape(result, scan_type: ScanType) -> ScanResponse:
65
+ findings = [{
66
+ "rule_id": f.rule_id, "severity": f.severity.value, "threat": f.threat_type.value,
67
+ "title": f.title, "detail": f.detail, "file": f.file, "line": f.line, "fix": f.fix,
68
+ } for f in result.findings]
69
+ return ScanResponse(
70
+ scan_type=scan_type.value, passed=result.passed,
71
+ critical=result.critical_count, high=result.high_count,
72
+ medium=sum(1 for f in result.findings if f.severity == Severity.MEDIUM),
73
+ info=sum(1 for f in result.findings if f.severity == Severity.INFO),
74
+ total=len(result.findings), findings=findings,
75
+ )
76
+
77
+ @app.get("/health")
78
+ async def health():
79
+ return {"status": "ok", "service": "ai-security-scanner", "version": "0.0.1",
80
+ "scan_types": ["prompt", "agent", "api", "mcp", "integration"]}
81
+
82
+ @app.post("/scan/prompt", response_model=ScanResponse)
83
+ async def scan_prompt_ep(req: ScanRequest):
84
+ return _run_scan(req, scan_prompt, ScanType.PROMPT, ".txt")
85
+
86
+ @app.post("/scan/agent", response_model=ScanResponse)
87
+ async def scan_agent_ep(req: ScanRequest):
88
+ return _run_scan(req, scan_agent, ScanType.AGENT, ".yaml")
89
+
90
+ @app.post("/scan/api", response_model=ScanResponse)
91
+ async def scan_api_ep(req: ScanRequest):
92
+ suffix = "." + req.filename.rsplit(".", 1)[-1] if "." in req.filename else ".yaml"
93
+ return _run_scan(req, scan_api, ScanType.API, suffix)
94
+
95
+ @app.post("/scan/mcp", response_model=ScanResponse)
96
+ async def scan_mcp_ep(req: ScanRequest):
97
+ return _run_scan(req, scan_mcp, ScanType.MCP, ".json")
98
+
99
+ @app.post("/scan/integration", response_model=ScanResponse)
100
+ async def scan_integration_ep(req: ScanRequest):
101
+ return _run_scan(req, scan_integration, ScanType.INTEGRATION, ".yaml")
102
+
103
+ @app.post("/scan/all")
104
+ async def scan_all(req: ScanRequest):
105
+ """Run all 5 scanners against the same content. Useful for mixed config files."""
106
+ results = []
107
+ for fn, st, sfx in [
108
+ (scan_prompt, ScanType.PROMPT, ".txt"),
109
+ (scan_agent, ScanType.AGENT, ".yaml"),
110
+ (scan_api, ScanType.API, ".yaml"),
111
+ (scan_mcp, ScanType.MCP, ".json"),
112
+ (scan_integration, ScanType.INTEGRATION, ".yaml"),
113
+ ]:
114
+ results.append(_run_scan(req, fn, st, sfx).model_dump())
115
+ total_findings = sum(r["total"] for r in results)
116
+ total_critical = sum(r["critical"] for r in results)
117
+ return {
118
+ "passed": total_critical == 0 and sum(r["high"] for r in results) == 0,
119
+ "total_findings": total_findings,
120
+ "total_critical": total_critical,
121
+ "results": results,
122
+ }
@@ -0,0 +1,342 @@
1
+ """
2
+ aiscan — AI Security Scanner CLI
3
+
4
+ A generic, installable command-line tool to scan AI system components
5
+ for security vulnerabilities.
6
+
7
+ Usage after install:
8
+ aiscan scan prompt.txt
9
+ aiscan scan --type api openapi.yaml
10
+ aiscan scan --type all config.yaml --output json
11
+ aiscan init
12
+ aiscan serve
13
+ """
14
+ import sys
15
+ import importlib
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ import typer
20
+ from rich.console import Console
21
+ from rich.panel import Panel
22
+
23
+ app = typer.Typer(
24
+ name="aiscan",
25
+ help="AI Security Scanner — scan prompts, agents, APIs, MCP tools and integrations.",
26
+ add_completion=True,
27
+ no_args_is_help=True,
28
+ )
29
+ console = Console()
30
+
31
+ # ── scan type → (module, function, default suffix) ────────────────────────────
32
+ SCAN_REGISTRY = {
33
+ "prompt": ("aiscan.scanners.prompt_scanner", "scan_prompt", ".txt"),
34
+ "agent": ("aiscan.scanners.agent_scanner", "scan_agent", ".yaml"),
35
+ "api": ("aiscan.scanners.api_scanner", "scan_api", ".yaml"),
36
+ "mcp": ("aiscan.scanners.mcp_scanner", "scan_mcp", ".json"),
37
+ "integration": ("aiscan.scanners.integration_scanner", "scan_integration", ".yaml"),
38
+ }
39
+
40
+ # Auto-detect scan type from file extension
41
+ AUTO_TYPE = {
42
+ # Prompt files
43
+ ".txt": "prompt", ".md": "prompt", ".j2": "prompt", ".jinja": "prompt",
44
+ # Code files — scanned with full rule set
45
+ ".py": "prompt", ".js": "prompt", ".ts": "prompt", ".jsx": "prompt",
46
+ ".tsx": "prompt", ".go": "prompt", ".java": "prompt", ".rb": "prompt",
47
+ ".php": "prompt", ".cs": "prompt", ".cpp": "prompt", ".c": "prompt",
48
+ ".rs": "prompt", ".swift": "prompt", ".kt": "prompt",
49
+ # API / config files
50
+ ".yaml": "api", ".yml": "api", ".json": "mcp",
51
+ # Integration / environment files
52
+ ".env": "integration", ".toml": "integration",
53
+ ".ini": "integration", ".cfg": "integration", ".conf": "integration",
54
+ ".properties": "integration",
55
+ }
56
+
57
+ # All file extensions aiscan will pick up when scanning a directory
58
+ SCANNABLE_EXTENSIONS = set(AUTO_TYPE.keys())
59
+
60
+
61
+ def _resolve_type(file: Path, declared: str) -> list[str]:
62
+ """Return list of scan types to run for a file."""
63
+ if declared == "all":
64
+ return list(SCAN_REGISTRY.keys())
65
+ if declared == "auto":
66
+ return [AUTO_TYPE.get(file.suffix.lower(), "prompt")]
67
+ return [declared]
68
+
69
+
70
+ def _run_one(file: Path, scan_type: str):
71
+ """Run a single scanner and return ScanResult."""
72
+ mod_name, fn_name, _ = SCAN_REGISTRY[scan_type]
73
+ mod = importlib.import_module(mod_name)
74
+ return getattr(mod, fn_name)(str(file))
75
+
76
+
77
+ # ── scan command ─────────────────────────────────────────────────────────────
78
+ @app.command()
79
+ def scan(
80
+ files: list[Path] = typer.Argument(
81
+ ..., help="One or more files to scan. Globs are expanded by the shell.",
82
+ exists=True, readable=True,
83
+ ),
84
+ type: str = typer.Option(
85
+ "auto",
86
+ "--type", "-t",
87
+ help="Scan type: prompt | agent | api | mcp | integration | all | auto",
88
+ ),
89
+ output: str = typer.Option(
90
+ "console",
91
+ "--output", "-o",
92
+ help="Output format: console | json | sarif",
93
+ ),
94
+ out: Optional[Path] = typer.Option(
95
+ None,
96
+ "--out", "-O",
97
+ help="Write output to file instead of stdout",
98
+ ),
99
+ fail_on: str = typer.Option(
100
+ "high",
101
+ "--fail-on", "-f",
102
+ help="Exit code 1 if any finding reaches this severity: critical | high | medium | none",
103
+ ),
104
+ no_ai: bool = typer.Option(
105
+ False,
106
+ "--no-ai",
107
+ help="Skip AI deep scan — run static rules only (faster, no API key needed)",
108
+ ),
109
+ quiet: bool = typer.Option(
110
+ False,
111
+ "--quiet", "-q",
112
+ help="Suppress all output except errors. Useful in CI.",
113
+ ),
114
+ ):
115
+ """
116
+ Scan one or more files for AI security vulnerabilities.
117
+
118
+ Examples:
119
+
120
+ aiscan scan system_prompt.txt
121
+
122
+ aiscan scan --type api openapi.yaml
123
+
124
+ aiscan scan --type all config.yaml --output json
125
+
126
+ aiscan scan --type prompt prompts/ --output sarif --out results.sarif
127
+
128
+ aiscan scan --no-ai --fail-on critical agent.yaml
129
+ """
130
+ from aiscan.utils.aggregator import aggregate
131
+ from aiscan.reporters.console_reporter import print_results
132
+ from aiscan.reporters.json_reporter import to_json, to_sarif
133
+
134
+ if no_ai:
135
+ import os
136
+ os.environ["AI_PROVIDER"] = ""
137
+
138
+ results = []
139
+ for file in files:
140
+ if file.is_dir():
141
+ # Expand directory — scan all recognised file types inside
142
+ all_files = [
143
+ f for f in file.rglob("*")
144
+ if f.is_file() and f.suffix.lower() in SCANNABLE_EXTENSIONS
145
+ ]
146
+ if not all_files and not quiet:
147
+ console.print(f"[yellow]No scannable files found in {file}[/yellow]")
148
+ for f in all_files:
149
+ for st in _resolve_type(f, type):
150
+ r = _run_one(f, st)
151
+ r.findings = aggregate(r.findings)
152
+ results.append(r)
153
+ else:
154
+ for st in _resolve_type(file, type):
155
+ r = _run_one(file, st)
156
+ r.findings = aggregate(r.findings)
157
+ results.append(r)
158
+
159
+ if not results:
160
+ if not quiet:
161
+ console.print("[yellow]No files scanned.[/yellow]")
162
+ raise typer.Exit(0)
163
+
164
+ # Output
165
+ if output == "json":
166
+ rendered = to_json(results)
167
+ if out:
168
+ out.write_text(rendered)
169
+ if not quiet:
170
+ console.print(f"[green]JSON written to {out}[/green]")
171
+ else:
172
+ print(rendered)
173
+
174
+ elif output == "sarif":
175
+ rendered = to_sarif(results)
176
+ if out:
177
+ out.write_text(rendered)
178
+ if not quiet:
179
+ console.print(f"[green]SARIF written to {out}[/green]")
180
+ else:
181
+ print(rendered)
182
+
183
+ else:
184
+ if not quiet:
185
+ print_results(results)
186
+
187
+ # CI exit code
188
+ FAIL_RANK = {"critical": 4, "high": 3, "medium": 2, "none": 0}
189
+ from aiscan.models import Severity
190
+ SEV_RANK = {Severity.CRITICAL: 4, Severity.HIGH: 3, Severity.MEDIUM: 2, Severity.INFO: 1}
191
+ threshold = FAIL_RANK.get(fail_on, 3)
192
+ max_sev = max((SEV_RANK.get(f.severity, 0) for r in results for f in r.findings), default=0)
193
+ if max_sev >= threshold:
194
+ raise typer.Exit(code=1)
195
+
196
+
197
+ # ── serve command ─────────────────────────────────────────────────────────────
198
+ @app.command()
199
+ def serve(
200
+ host: str = typer.Option("0.0.0.0", "--host", help="Host to bind"),
201
+ port: int = typer.Option(8000, "--port", help="Port to listen on"),
202
+ reload: bool = typer.Option(False, "--reload", help="Auto-reload on code changes (dev mode)"),
203
+ ):
204
+ """
205
+ Start the AI Security Scanner REST API server.
206
+
207
+ Endpoints:
208
+ GET /health
209
+ POST /scan/prompt
210
+ POST /scan/agent
211
+ POST /scan/api
212
+ POST /scan/mcp
213
+ POST /scan/integration
214
+ POST /scan/all
215
+ GET /docs (Swagger UI)
216
+
217
+ Example:
218
+ aiscan serve --port 8000 --reload
219
+ """
220
+ try:
221
+ import uvicorn
222
+ except ImportError:
223
+ console.print("[red]Install the api extra to use serve: pip install aiscan[api][/red]")
224
+ raise typer.Exit(1)
225
+
226
+ from aiscan.api import routes # noqa: F401 — triggers app creation
227
+ console.print(Panel(
228
+ f"[bold]AI Security Scanner API[/bold]\n"
229
+ f"http://{host}:{port}\n"
230
+ f"Swagger UI: http://{host}:{port}/docs",
231
+ title="aiscan serve",
232
+ ))
233
+ uvicorn.run("aiscan.api.routes:app", host=host, port=port, reload=reload)
234
+
235
+
236
+ # ── rules command ─────────────────────────────────────────────────────────────
237
+ @app.command()
238
+ def rules(
239
+ type: Optional[str] = typer.Argument(None, help="Filter by scan type: prompt | agent | api | mcp | integration"),
240
+ severity: Optional[str] = typer.Option(None, "--severity", "-s", help="Filter by severity: critical | high | medium | info"),
241
+ ):
242
+ """
243
+ List all security rules in the registry.
244
+
245
+ Examples:
246
+ aiscan rules
247
+ aiscan rules prompt
248
+ aiscan rules api --severity critical
249
+ """
250
+ from rich.table import Table
251
+ from rich import box
252
+ from aiscan.rules.registry import RULES
253
+ from aiscan.models import ScanType, Severity
254
+
255
+ SEV_STYLE = {"critical": "bold red", "high": "bold yellow", "medium": "yellow", "info": "dim"}
256
+
257
+ table = Table(box=box.SIMPLE, show_header=True, header_style="bold")
258
+ table.add_column("ID", width=9)
259
+ table.add_column("Target", width=12)
260
+ table.add_column("Threat", width=20)
261
+ table.add_column("Severity", width=10)
262
+ table.add_column("Title", width=48)
263
+
264
+ filtered = RULES
265
+ if type:
266
+ try:
267
+ st = ScanType(type)
268
+ filtered = [r for r in filtered if st in r["scan_types"]]
269
+ except ValueError:
270
+ console.print(f"[red]Unknown type '{type}'. Use: {', '.join(SCAN_REGISTRY)}[/red]")
271
+ raise typer.Exit(1)
272
+
273
+ if severity:
274
+ filtered = [r for r in filtered if r["severity"].value == severity]
275
+
276
+ for r in filtered:
277
+ sev = r["severity"].value
278
+ targets = ", ".join(s.value for s in r["scan_types"])
279
+ table.add_row(
280
+ r["id"], targets, r["threat"].value, sev, r["title"],
281
+ style=SEV_STYLE.get(sev, ""),
282
+ )
283
+
284
+ console.print(table)
285
+ console.print(f"\n[dim]{len(filtered)} rule(s) shown[/dim]")
286
+
287
+
288
+ # ── init command ──────────────────────────────────────────────────────────────
289
+ @app.command()
290
+ def init(
291
+ provider: str = typer.Option("claude", "--provider", "-p", help="AI provider: claude | openai | vertex"),
292
+ force: bool = typer.Option(False, "--force", help="Overwrite existing .env file"),
293
+ ):
294
+ """
295
+ Initialise a new .env config file in the current directory.
296
+
297
+ Example:
298
+ aiscan init
299
+ aiscan init --provider openai
300
+ """
301
+ env_path = Path(".env")
302
+ if env_path.exists() and not force:
303
+ console.print("[yellow].env already exists. Use --force to overwrite.[/yellow]")
304
+ raise typer.Exit(0)
305
+
306
+ content = f"""# aiscan configuration
307
+ # Generated by: aiscan init --provider {provider}
308
+
309
+ # Active provider: claude | openai | vertex
310
+ AI_PROVIDER={provider}
311
+
312
+ # Anthropic (Claude)
313
+ ANTHROPIC_API_KEY=sk-ant-...
314
+ SCANNER_MODEL=claude-sonnet-4-20250514
315
+
316
+ # OpenAI (GPT-4o)
317
+ OPENAI_API_KEY=sk-...
318
+ OPENAI_MODEL=gpt-4o
319
+
320
+ # Vertex AI (Gemini)
321
+ VERTEX_PROJECT=your-gcp-project-id
322
+ VERTEX_LOCATION=us-central1
323
+ VERTEX_MODEL=gemini-2.5-flash-001
324
+
325
+ # Limits
326
+ MAX_TOKENS=2000
327
+ LOG_LEVEL=INFO
328
+ """
329
+ env_path.write_text(content)
330
+ console.print(f"[green]Created .env — set your {provider.upper()}_API_KEY and run:[/green]")
331
+ console.print(f" aiscan scan --type prompt your_prompt.txt")
332
+
333
+
334
+ # ── version command ───────────────────────────────────────────────────────────
335
+ @app.command()
336
+ def version():
337
+ """Show aiscan version."""
338
+ console.print("aiscan 1.0.0")
339
+
340
+
341
+ if __name__ == "__main__":
342
+ app()
@@ -0,0 +1,20 @@
1
+ from pydantic_settings import BaseSettings
2
+ from typing import Literal
3
+
4
+ class Settings(BaseSettings):
5
+ log_level: str = "INFO"
6
+ ai_provider: Literal["claude", "openai", "vertex", ""] = "claude"
7
+ anthropic_api_key: str = ""
8
+ scanner_model: str = "claude-sonnet-4-20250514"
9
+ openai_api_key: str = ""
10
+ openai_model: str = "gpt-4o"
11
+ vertex_project: str = ""
12
+ vertex_location: str = "us-central1"
13
+ vertex_model: str = "gemini-2.5-flash-001"
14
+ max_tokens: int = 2000
15
+
16
+ class Config:
17
+ env_file = ".env"
18
+ extra = "ignore"
19
+
20
+ settings = Settings()
@@ -0,0 +1,47 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+
4
+ class Severity(str, Enum):
5
+ CRITICAL = "critical"
6
+ HIGH = "high"
7
+ MEDIUM = "medium"
8
+ INFO = "info"
9
+
10
+ class ScanType(str, Enum):
11
+ PROMPT = "prompt"
12
+ AGENT = "agent"
13
+ API = "api"
14
+ MCP = "mcp"
15
+ INTEGRATION = "integration"
16
+
17
+ class ThreatType(str, Enum):
18
+ PROMPT_INJECTION = "prompt_injection"
19
+ DATA_LEAKAGE = "data_leakage"
20
+ TOOL_ABUSE = "tool_abuse"
21
+ SECURITY_RISK = "security_risk"
22
+
23
+ @dataclass
24
+ class Finding:
25
+ scan_type: ScanType
26
+ threat_type: ThreatType
27
+ severity: Severity
28
+ rule_id: str
29
+ title: str
30
+ detail: str
31
+ file: str
32
+ line: int
33
+ fix: str
34
+
35
+ @dataclass
36
+ class ScanResult:
37
+ target: str
38
+ scan_type: ScanType
39
+ findings: list[Finding] = field(default_factory=list)
40
+ error: str = ""
41
+
42
+ @property
43
+ def critical_count(self): return sum(1 for f in self.findings if f.severity == Severity.CRITICAL)
44
+ @property
45
+ def high_count(self): return sum(1 for f in self.findings if f.severity == Severity.HIGH)
46
+ @property
47
+ def passed(self): return self.critical_count == 0 and self.high_count == 0
File without changes
@@ -0,0 +1,47 @@
1
+ """Rich console reporter for all 5 scan types."""
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ from rich.panel import Panel
5
+ from rich import box
6
+ from aiscan.models import ScanResult, Severity
7
+
8
+ console = Console()
9
+
10
+ ICON = {Severity.CRITICAL:"🔴", Severity.HIGH:"🟠", Severity.MEDIUM:"🟡", Severity.INFO:"🔵"}
11
+ STYLE = {Severity.CRITICAL:"bold red", Severity.HIGH:"bold yellow", Severity.MEDIUM:"yellow", Severity.INFO:"dim"}
12
+
13
+ def print_results(results: list[ScanResult]) -> int:
14
+ total = 0
15
+ for r in results:
16
+ if r.error:
17
+ console.print(f"[red]ERROR {r.target}: {r.error}[/red]")
18
+ continue
19
+ if not r.findings:
20
+ console.print(f"[green]✅ {r.target} ({r.scan_type.value}) — no issues[/green]")
21
+ continue
22
+ t = Table(box=box.SIMPLE, show_header=True, header_style="bold")
23
+ t.add_column("Sev", width=10)
24
+ t.add_column("Rule", width=8)
25
+ t.add_column("Threat", width=20)
26
+ t.add_column("Title", width=44)
27
+ t.add_column("Line", width=5, justify="right")
28
+ for f in r.findings:
29
+ t.add_row(
30
+ f"{ICON.get(f.severity,'')} {f.severity.value}",
31
+ f.rule_id, f.threat_type.value, f.title,
32
+ str(f.line) if f.line else "—",
33
+ style=STYLE.get(f.severity,""),
34
+ )
35
+ console.print(Panel(t, title=f"[bold]{r.target}[/bold] [{r.scan_type.value}]",
36
+ subtitle=f"{len(r.findings)} finding(s)"))
37
+ for f in r.findings:
38
+ if f.severity in (Severity.CRITICAL, Severity.HIGH):
39
+ console.print(f" [bold]{f.title}[/bold]")
40
+ console.print(f" [dim]{f.detail}[/dim]")
41
+ console.print(f" [green]Fix:[/green] {f.fix}\n")
42
+ total += len(r.findings)
43
+ if total == 0:
44
+ console.print("\n[bold green]✅ All scans passed.[/bold green]")
45
+ else:
46
+ console.print(f"\n[bold]Total findings: {total}[/bold]")
47
+ return total
@@ -0,0 +1,34 @@
1
+ """JSON and SARIF reporters."""
2
+ import json
3
+ from aiscan.models import ScanResult
4
+
5
+ def to_json(results: list[ScanResult], pretty: bool = True) -> str:
6
+ data = [{
7
+ "target": r.target, "scan_type": r.scan_type.value,
8
+ "passed": r.passed, "critical": r.critical_count, "high": r.high_count,
9
+ "findings": [{
10
+ "rule_id": f.rule_id, "severity": f.severity.value,
11
+ "threat": f.threat_type.value, "title": f.title,
12
+ "detail": f.detail, "file": f.file, "line": f.line, "fix": f.fix,
13
+ } for f in r.findings],
14
+ } for r in results]
15
+ return json.dumps(data, indent=2 if pretty else None)
16
+
17
+ def to_sarif(results: list[ScanResult]) -> str:
18
+ rules, run_results, seen = [], [], set()
19
+ for r in results:
20
+ for f in r.findings:
21
+ if f.rule_id not in seen:
22
+ seen.add(f.rule_id)
23
+ rules.append({"id": f.rule_id, "name": f.threat_type.value,
24
+ "shortDescription": {"text": f.title}, "help": {"text": f.fix},
25
+ "properties": {"security-severity": {"critical":"9.0","high":"7.5","medium":"5.0","info":"2.0"}.get(f.severity.value,"5.0")}})
26
+ run_results.append({"ruleId": f.rule_id,
27
+ "level": {"critical":"error","high":"error","medium":"warning","info":"note"}.get(f.severity.value,"warning"),
28
+ "message": {"text": f"{f.title} — {f.detail}"},
29
+ "locations": [{"physicalLocation": {"artifactLocation": {"uri": f.file},
30
+ "region": {"startLine": max(f.line, 1)}}}]})
31
+ sarif = {"version":"2.1.0",
32
+ "$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
33
+ "runs":[{"tool":{"driver":{"name":"AI Security Scanner","version":"0.0.1","rules":rules}},"results":run_results}]}
34
+ return json.dumps(sarif, indent=2)
File without changes