vibesec 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.
- vibesec/__init__.py +2 -0
- vibesec/cli.py +37 -0
- vibesec/fixgen.py +40 -0
- vibesec/reporter.py +102 -0
- vibesec/rules/__init__.py +23 -0
- vibesec/rules/auth_routes.py +67 -0
- vibesec/rules/cors.py +74 -0
- vibesec/rules/jwt.py +52 -0
- vibesec/rules/packages.py +83 -0
- vibesec/rules/rls.py +51 -0
- vibesec/rules/roles.py +58 -0
- vibesec/rules/secrets.py +59 -0
- vibesec/rules/sourcemaps.py +66 -0
- vibesec/rules/webhooks.py +77 -0
- vibesec/rules/xss.py +56 -0
- vibesec/scanner.py +25 -0
- vibesec/utils.py +31 -0
- vibesec-0.1.0.dist-info/METADATA +287 -0
- vibesec-0.1.0.dist-info/RECORD +23 -0
- vibesec-0.1.0.dist-info/WHEEL +5 -0
- vibesec-0.1.0.dist-info/entry_points.txt +2 -0
- vibesec-0.1.0.dist-info/licenses/LICENSE +21 -0
- vibesec-0.1.0.dist-info/top_level.txt +1 -0
vibesec/__init__.py
ADDED
vibesec/cli.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from vibesec.scanner import Scanner
|
|
3
|
+
from vibesec.reporter import Reporter
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
@click.version_option(version="0.1.0")
|
|
7
|
+
def cli():
|
|
8
|
+
"""VibeSec — Security scanner for AI-generated code."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
@cli.command()
|
|
12
|
+
@click.argument("path")
|
|
13
|
+
@click.option("--fix", is_flag=True, help="Generate fix suggestions using Groq AI")
|
|
14
|
+
@click.option("--output", type=click.Choice(["terminal", "json"]), default="terminal")
|
|
15
|
+
@click.option("--severity", type=click.Choice(["critical", "high", "medium", "low"]), default=None)
|
|
16
|
+
def scan(path, fix, output, severity):
|
|
17
|
+
"""Scan a directory or file for security vulnerabilities."""
|
|
18
|
+
|
|
19
|
+
reporter = Reporter()
|
|
20
|
+
reporter.print_banner()
|
|
21
|
+
|
|
22
|
+
scanner = Scanner(path)
|
|
23
|
+
findings = scanner.run()
|
|
24
|
+
|
|
25
|
+
if severity:
|
|
26
|
+
findings = [f for f in findings if f["severity"].lower() == severity]
|
|
27
|
+
|
|
28
|
+
if output == "json":
|
|
29
|
+
reporter.print_json(findings)
|
|
30
|
+
else:
|
|
31
|
+
reporter.print_report(findings, path, fix)
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
cli()
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
vibesec/fixgen.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from groq import Groq
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
load_dotenv()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_fix(finding: dict) -> str:
|
|
9
|
+
"""Generate an AI-powered fix suggestion for a security finding."""
|
|
10
|
+
|
|
11
|
+
api_key = os.environ.get("GROQ_API_KEY")
|
|
12
|
+
if not api_key:
|
|
13
|
+
return "Set GROQ_API_KEY environment variable to enable AI fix suggestions."
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
client = Groq(api_key=api_key)
|
|
17
|
+
|
|
18
|
+
prompt = f"""You are a security expert. A vulnerability was found in code.
|
|
19
|
+
|
|
20
|
+
Rule: {finding['rule']}
|
|
21
|
+
Severity: {finding['severity']}
|
|
22
|
+
File: {finding['file']}
|
|
23
|
+
Issue: {finding['message']}
|
|
24
|
+
Code: {finding.get('code_snippet', 'N/A')}
|
|
25
|
+
|
|
26
|
+
Give a specific, concise fix in 2-3 sentences maximum.
|
|
27
|
+
Show a corrected code example if possible.
|
|
28
|
+
Be direct — no preamble, no "I recommend", just the fix."""
|
|
29
|
+
|
|
30
|
+
response = client.chat.completions.create(
|
|
31
|
+
model="llama-3.1-8b-instant",
|
|
32
|
+
messages=[{"role": "user", "content": prompt}],
|
|
33
|
+
max_tokens=150,
|
|
34
|
+
temperature=0.1,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return response.choices[0].message.content.strip()
|
|
38
|
+
|
|
39
|
+
except Exception as e:
|
|
40
|
+
return f"Could not generate fix: {str(e)}"
|
vibesec/reporter.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from rich import box
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
SEVERITY_COLORS = {
|
|
11
|
+
"CRITICAL": "bold red",
|
|
12
|
+
"HIGH": "bold yellow",
|
|
13
|
+
"MEDIUM": "bold orange3",
|
|
14
|
+
"LOW": "bold green",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Reporter:
|
|
21
|
+
|
|
22
|
+
def print_banner(self):
|
|
23
|
+
console.print()
|
|
24
|
+
console.print(Panel(
|
|
25
|
+
"[bold cyan]VibeSec v0.1.0[/bold cyan] — [dim]AI-Generated Code Security Scanner[/dim]",
|
|
26
|
+
border_style="cyan",
|
|
27
|
+
expand=False
|
|
28
|
+
))
|
|
29
|
+
console.print()
|
|
30
|
+
|
|
31
|
+
def print_report(self, findings, path, fix=False):
|
|
32
|
+
if not findings:
|
|
33
|
+
console.print(Panel(
|
|
34
|
+
"[bold green]✓ No vulnerabilities found![/bold green]\n"
|
|
35
|
+
"[dim]VibeSec checked 10 vulnerability patterns.[/dim]",
|
|
36
|
+
border_style="green"
|
|
37
|
+
))
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Sort by severity
|
|
41
|
+
findings.sort(key=lambda x: SEVERITY_ORDER.get(x["severity"], 99))
|
|
42
|
+
|
|
43
|
+
# Summary
|
|
44
|
+
counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}
|
|
45
|
+
for f in findings:
|
|
46
|
+
counts[f["severity"]] = counts.get(f["severity"], 0) + 1
|
|
47
|
+
|
|
48
|
+
summary = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
|
|
49
|
+
summary.add_column(style="bold")
|
|
50
|
+
summary.add_column()
|
|
51
|
+
|
|
52
|
+
for severity, count in counts.items():
|
|
53
|
+
if count > 0:
|
|
54
|
+
color = SEVERITY_COLORS[severity]
|
|
55
|
+
summary.add_row(
|
|
56
|
+
f"[{color}]● {severity}[/{color}]",
|
|
57
|
+
f"[{color}]{count} finding{'s' if count > 1 else ''}[/{color}]"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
console.print(Panel(summary, title="[bold]FINDINGS SUMMARY[/bold]",
|
|
61
|
+
border_style="dim"))
|
|
62
|
+
|
|
63
|
+
# Generate AI fixes if requested
|
|
64
|
+
if fix:
|
|
65
|
+
from vibesec.fixgen import generate_fix
|
|
66
|
+
console.print("\n [cyan]Generating AI fix suggestions...[/cyan]\n")
|
|
67
|
+
for finding in findings:
|
|
68
|
+
finding["groq_fix"] = generate_fix(finding)
|
|
69
|
+
|
|
70
|
+
# Individual findings
|
|
71
|
+
for i, finding in enumerate(findings, 1):
|
|
72
|
+
severity = finding["severity"]
|
|
73
|
+
color = SEVERITY_COLORS[severity]
|
|
74
|
+
|
|
75
|
+
console.print()
|
|
76
|
+
console.print(f" [{color}]{severity}[/{color}] — "
|
|
77
|
+
f"[bold white]{finding['rule']}[/bold white]")
|
|
78
|
+
console.print(f" [dim]File: {finding['file']} "
|
|
79
|
+
f"Line: {finding.get('line', 'N/A')}[/dim]")
|
|
80
|
+
console.print(f" [red]Found:[/red] {finding['message']}")
|
|
81
|
+
console.print(f" [green]Fix:[/green] {finding['fix_hint']}")
|
|
82
|
+
|
|
83
|
+
if fix and finding.get("groq_fix"):
|
|
84
|
+
console.print(
|
|
85
|
+
f" [cyan]AI Fix:[/cyan] {finding['groq_fix']}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Footer
|
|
89
|
+
console.print()
|
|
90
|
+
console.print(
|
|
91
|
+
f" [dim]{len(findings)} finding{'s' if len(findings) > 1 else ''} "
|
|
92
|
+
f"in {path}[/dim]"
|
|
93
|
+
)
|
|
94
|
+
if not fix:
|
|
95
|
+
console.print(
|
|
96
|
+
" [dim]Run with [/dim][cyan]--fix[/cyan]"
|
|
97
|
+
"[dim] for AI-powered remediation suggestions[/dim]"
|
|
98
|
+
)
|
|
99
|
+
console.print()
|
|
100
|
+
|
|
101
|
+
def print_json(self, findings):
|
|
102
|
+
console.print(json.dumps(findings, indent=2))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from vibesec.rules.secrets import check_secrets
|
|
2
|
+
from vibesec.rules.rls import check_rls
|
|
3
|
+
from vibesec.rules.auth_routes import check_auth_routes
|
|
4
|
+
from vibesec.rules.packages import check_packages
|
|
5
|
+
from vibesec.rules.sourcemaps import check_sourcemaps
|
|
6
|
+
from vibesec.rules.jwt import check_jwt
|
|
7
|
+
from vibesec.rules.xss import check_xss
|
|
8
|
+
from vibesec.rules.roles import check_roles
|
|
9
|
+
from vibesec.rules.webhooks import check_webhooks
|
|
10
|
+
from vibesec.rules.cors import check_cors
|
|
11
|
+
|
|
12
|
+
ALL_RULES = [
|
|
13
|
+
check_secrets,
|
|
14
|
+
check_rls,
|
|
15
|
+
check_auth_routes,
|
|
16
|
+
check_packages,
|
|
17
|
+
check_sourcemaps,
|
|
18
|
+
check_jwt,
|
|
19
|
+
check_xss,
|
|
20
|
+
check_roles,
|
|
21
|
+
check_webhooks,
|
|
22
|
+
check_cors,
|
|
23
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
RULE_NAME = "Missing Route Authentication"
|
|
4
|
+
SEVERITY = "HIGH"
|
|
5
|
+
|
|
6
|
+
# Routes that look like admin or sensitive endpoints
|
|
7
|
+
SENSITIVE_ROUTE_PATTERNS = [
|
|
8
|
+
r'@app\.route\s*\(\s*["\'][^"\']*admin[^"\']*["\']',
|
|
9
|
+
r'@app\.route\s*\(\s*["\'][^"\']*delete[^"\']*["\']',
|
|
10
|
+
r'@app\.route\s*\(\s*["\'][^"\']*\/api\/user[^"\']*["\']',
|
|
11
|
+
r'router\.(post|put|delete|patch)\s*\(\s*["\'][^"\']*admin[^"\']*["\']',
|
|
12
|
+
r'app\.(post|put|delete|patch)\s*\(\s*["\'][^"\']*admin[^"\']*["\']',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# Auth decorators/middleware that make a route safe
|
|
16
|
+
AUTH_INDICATORS = [
|
|
17
|
+
"login_required",
|
|
18
|
+
"jwt_required",
|
|
19
|
+
"auth_required",
|
|
20
|
+
"verify_token",
|
|
21
|
+
"authenticate",
|
|
22
|
+
"authorization",
|
|
23
|
+
"requireAuth",
|
|
24
|
+
"withAuth",
|
|
25
|
+
"middleware",
|
|
26
|
+
"session",
|
|
27
|
+
"getServerSession",
|
|
28
|
+
"currentUser",
|
|
29
|
+
"verifyJWT",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_auth_routes(file_path, content):
|
|
34
|
+
findings = []
|
|
35
|
+
|
|
36
|
+
ext = file_path.split(".")[-1].lower()
|
|
37
|
+
if ext not in {"py", "js", "ts", "jsx", "tsx"}:
|
|
38
|
+
return findings
|
|
39
|
+
|
|
40
|
+
lines = content.splitlines()
|
|
41
|
+
|
|
42
|
+
for line_num, line in enumerate(lines, 1):
|
|
43
|
+
for pattern in SENSITIVE_ROUTE_PATTERNS:
|
|
44
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
45
|
+
# Check surrounding lines (5 above, 10 below) for auth indicators
|
|
46
|
+
start = max(0, line_num - 5)
|
|
47
|
+
end = min(len(lines), line_num + 10)
|
|
48
|
+
surrounding = "\n".join(lines[start:end]).lower()
|
|
49
|
+
|
|
50
|
+
has_auth = any(
|
|
51
|
+
indicator.lower() in surrounding
|
|
52
|
+
for indicator in AUTH_INDICATORS
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if not has_auth:
|
|
56
|
+
findings.append({
|
|
57
|
+
"rule": RULE_NAME,
|
|
58
|
+
"severity": SEVERITY,
|
|
59
|
+
"file": file_path,
|
|
60
|
+
"line": line_num,
|
|
61
|
+
"message": "Sensitive route defined without visible auth middleware",
|
|
62
|
+
"fix_hint": "Add authentication decorator or middleware before this route handler.",
|
|
63
|
+
"code_snippet": line.strip()[:80],
|
|
64
|
+
})
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
return findings
|
vibesec/rules/cors.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
RULE_NAME = "Permissive CORS Configuration"
|
|
4
|
+
SEVERITY = "MEDIUM"
|
|
5
|
+
|
|
6
|
+
PATTERNS = [
|
|
7
|
+
(r'origin\s*:\s*["\']?\*["\']?',
|
|
8
|
+
"CORS wildcard origin — any domain can make requests"),
|
|
9
|
+
(r'Access-Control-Allow-Origin.*\*',
|
|
10
|
+
"CORS wildcard in response header"),
|
|
11
|
+
(r'cors\(\s*\)',
|
|
12
|
+
"CORS enabled with no configuration — defaults to wildcard"),
|
|
13
|
+
(r'allowedOrigins\s*[:=]\s*\[?\s*["\']?\*',
|
|
14
|
+
"CORS allowed origins set to wildcard"),
|
|
15
|
+
(r'credentials.*true.*\*|\*.*credentials.*true',
|
|
16
|
+
"CORS wildcard with credentials — critical misconfiguration"),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
SAFE_INDICATORS = [
|
|
20
|
+
"process.env",
|
|
21
|
+
"ALLOWED_ORIGINS",
|
|
22
|
+
"whitelist",
|
|
23
|
+
"allowlist",
|
|
24
|
+
"origins.includes",
|
|
25
|
+
"origin ===",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
SKIP_FILES = {"README.md", "readme.md"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_cors(file_path, content):
|
|
32
|
+
findings = []
|
|
33
|
+
|
|
34
|
+
filename = file_path.split("/")[-1].split("\\")[-1]
|
|
35
|
+
if filename in SKIP_FILES:
|
|
36
|
+
return findings
|
|
37
|
+
|
|
38
|
+
ext = file_path.split(".")[-1].lower()
|
|
39
|
+
if ext not in {"py", "js", "ts", "jsx", "tsx"}:
|
|
40
|
+
return findings
|
|
41
|
+
|
|
42
|
+
if "cors" not in content.lower() and "origin" not in content.lower():
|
|
43
|
+
return findings
|
|
44
|
+
|
|
45
|
+
lines = content.splitlines()
|
|
46
|
+
for line_num, line in enumerate(lines, 1):
|
|
47
|
+
stripped = line.strip()
|
|
48
|
+
if stripped.startswith("//") or stripped.startswith("#"):
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
for pattern, description in PATTERNS:
|
|
52
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
53
|
+
start = max(0, line_num - 5)
|
|
54
|
+
end = min(len(lines), line_num + 5)
|
|
55
|
+
surrounding = "\n".join(lines[start:end])
|
|
56
|
+
|
|
57
|
+
is_safe = any(
|
|
58
|
+
indicator in surrounding
|
|
59
|
+
for indicator in SAFE_INDICATORS
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if not is_safe:
|
|
63
|
+
findings.append({
|
|
64
|
+
"rule": RULE_NAME,
|
|
65
|
+
"severity": SEVERITY,
|
|
66
|
+
"file": file_path,
|
|
67
|
+
"line": line_num,
|
|
68
|
+
"message": description,
|
|
69
|
+
"fix_hint": "Specify exact allowed origins. Never use wildcard CORS with credentials enabled.",
|
|
70
|
+
"code_snippet": line.strip()[:80],
|
|
71
|
+
})
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
return findings
|
vibesec/rules/jwt.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
RULE_NAME = "Unsafe JWT Handling"
|
|
4
|
+
SEVERITY = "HIGH"
|
|
5
|
+
|
|
6
|
+
PATTERNS = [
|
|
7
|
+
(r'jwt\.decode\s*\([^)]*algorithms\s*=\s*\[["\']none["\']\]',
|
|
8
|
+
"JWT accepts 'none' algorithm — critical auth bypass"),
|
|
9
|
+
(r'verify\s*=\s*False',
|
|
10
|
+
"JWT verification explicitly disabled"),
|
|
11
|
+
(r'localStorage\.setItem\s*\([^)]*token',
|
|
12
|
+
"JWT stored in localStorage — vulnerable to XSS theft"),
|
|
13
|
+
(r'sessionStorage\.setItem\s*\([^)]*token',
|
|
14
|
+
"JWT stored in sessionStorage — vulnerable to XSS theft"),
|
|
15
|
+
(r'algorithm.*none',
|
|
16
|
+
"JWT 'none' algorithm detected"),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
SKIP_FILES = {"README.md", "readme.md"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_jwt(file_path, content):
|
|
23
|
+
findings = []
|
|
24
|
+
|
|
25
|
+
filename = file_path.split("/")[-1].split("\\")[-1]
|
|
26
|
+
if filename in SKIP_FILES:
|
|
27
|
+
return findings
|
|
28
|
+
|
|
29
|
+
ext = file_path.split(".")[-1].lower()
|
|
30
|
+
if ext not in {"py", "js", "ts", "jsx", "tsx"}:
|
|
31
|
+
return findings
|
|
32
|
+
|
|
33
|
+
lines = content.splitlines()
|
|
34
|
+
for line_num, line in enumerate(lines, 1):
|
|
35
|
+
stripped = line.strip()
|
|
36
|
+
if stripped.startswith("#") or stripped.startswith("//"):
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
for pattern, description in PATTERNS:
|
|
40
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
41
|
+
findings.append({
|
|
42
|
+
"rule": RULE_NAME,
|
|
43
|
+
"severity": SEVERITY,
|
|
44
|
+
"file": file_path,
|
|
45
|
+
"line": line_num,
|
|
46
|
+
"message": description,
|
|
47
|
+
"fix_hint": "Always verify JWT signature. Use httpOnly cookies instead of localStorage.",
|
|
48
|
+
"code_snippet": line.strip()[:80],
|
|
49
|
+
})
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
return findings
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
RULE_NAME = "Hallucinated Package"
|
|
6
|
+
SEVERITY = "HIGH"
|
|
7
|
+
|
|
8
|
+
# Known hallucinated package names LLMs commonly generate
|
|
9
|
+
KNOWN_HALLUCINATED = {
|
|
10
|
+
"react-auth-handler",
|
|
11
|
+
"supabase-helpers",
|
|
12
|
+
"express-middleware-auth",
|
|
13
|
+
"nextjs-utils",
|
|
14
|
+
"react-secure-storage",
|
|
15
|
+
"express-auth-jwt",
|
|
16
|
+
"node-security-utils",
|
|
17
|
+
"react-api-handler",
|
|
18
|
+
"next-auth-helpers",
|
|
19
|
+
"prisma-utils",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check_npm_exists(package_name):
|
|
24
|
+
"""Check if package exists on npm registry."""
|
|
25
|
+
try:
|
|
26
|
+
url = f"https://registry.npmjs.org/{package_name}"
|
|
27
|
+
response = requests.get(url, timeout=3)
|
|
28
|
+
return response.status_code == 200
|
|
29
|
+
except Exception:
|
|
30
|
+
return True # If we can't check, assume it exists (avoid false positives)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_packages(file_path, content):
|
|
34
|
+
findings = []
|
|
35
|
+
|
|
36
|
+
filename = file_path.split("/")[-1].split("\\")[-1]
|
|
37
|
+
|
|
38
|
+
if filename != "package.json":
|
|
39
|
+
return findings
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
data = json.loads(content)
|
|
43
|
+
except json.JSONDecodeError:
|
|
44
|
+
return findings
|
|
45
|
+
|
|
46
|
+
all_deps = {}
|
|
47
|
+
all_deps.update(data.get("dependencies", {}))
|
|
48
|
+
all_deps.update(data.get("devDependencies", {}))
|
|
49
|
+
|
|
50
|
+
for package_name in all_deps:
|
|
51
|
+
# First check known hallucinated list (fast, no API call)
|
|
52
|
+
if package_name.lower() in KNOWN_HALLUCINATED:
|
|
53
|
+
findings.append({
|
|
54
|
+
"rule": RULE_NAME,
|
|
55
|
+
"severity": SEVERITY,
|
|
56
|
+
"file": file_path,
|
|
57
|
+
"line": "N/A",
|
|
58
|
+
"message": f"'{package_name}' is a known hallucinated package name",
|
|
59
|
+
"fix_hint": f"Remove '{package_name}' — this package does not exist. Find the correct package on npmjs.com.",
|
|
60
|
+
"code_snippet": package_name,
|
|
61
|
+
})
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
# Check for suspicious patterns
|
|
65
|
+
suspicious = (
|
|
66
|
+
re.search(r'(helper|util|handler|wrapper)s?$', package_name, re.I)
|
|
67
|
+
and not package_name.startswith("@")
|
|
68
|
+
and len(package_name) > 15
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if suspicious:
|
|
72
|
+
if not check_npm_exists(package_name):
|
|
73
|
+
findings.append({
|
|
74
|
+
"rule": RULE_NAME,
|
|
75
|
+
"severity": SEVERITY,
|
|
76
|
+
"file": file_path,
|
|
77
|
+
"line": "N/A",
|
|
78
|
+
"message": f"'{package_name}' does not exist on npm registry",
|
|
79
|
+
"fix_hint": "This package may be hallucinated by an AI tool. Verify on npmjs.com before using.",
|
|
80
|
+
"code_snippet": package_name,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return findings
|
vibesec/rules/rls.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
RULE_NAME = "Supabase RLS Disabled"
|
|
4
|
+
SEVERITY = "CRITICAL"
|
|
5
|
+
|
|
6
|
+
PATTERNS = [
|
|
7
|
+
(r'alter\s+table\s+\w+\s+disable\s+row\s+level\s+security',
|
|
8
|
+
"RLS explicitly disabled on table"),
|
|
9
|
+
(r'row\s+level\s+security.*disabled',
|
|
10
|
+
"Row level security disabled"),
|
|
11
|
+
(r'\.from\(["\'](\w+)["\']\)\s*\.select',
|
|
12
|
+
"Supabase query — verify RLS is enabled on this table"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
SKIP_FILES = {"README.md", "readme.md"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_rls(file_path, content):
|
|
19
|
+
findings = []
|
|
20
|
+
|
|
21
|
+
filename = file_path.split("/")[-1].split("\\")[-1]
|
|
22
|
+
if filename in SKIP_FILES:
|
|
23
|
+
return findings
|
|
24
|
+
|
|
25
|
+
ext = file_path.split(".")[-1].lower()
|
|
26
|
+
|
|
27
|
+
lines = content.splitlines()
|
|
28
|
+
for line_num, line in enumerate(lines, 1):
|
|
29
|
+
stripped = line.strip()
|
|
30
|
+
if stripped.startswith("--") or stripped.startswith("#"):
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
for pattern, description in PATTERNS:
|
|
34
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
35
|
+
# For .select queries, only flag in non-SQL files
|
|
36
|
+
# SQL files legitimately define RLS
|
|
37
|
+
if "select" in pattern and ext == "sql":
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
findings.append({
|
|
41
|
+
"rule": RULE_NAME,
|
|
42
|
+
"severity": SEVERITY,
|
|
43
|
+
"file": file_path,
|
|
44
|
+
"line": line_num,
|
|
45
|
+
"message": description,
|
|
46
|
+
"fix_hint": "Enable RLS: ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; then add policies.",
|
|
47
|
+
"code_snippet": line.strip()[:80],
|
|
48
|
+
})
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
return findings
|
vibesec/rules/roles.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
RULE_NAME = "Client-Side Role Trust"
|
|
4
|
+
SEVERITY = "HIGH"
|
|
5
|
+
|
|
6
|
+
PATTERNS = [
|
|
7
|
+
(r'localStorage\.getItem\s*\([^)]*role',
|
|
8
|
+
"Role read from localStorage — can be tampered by user"),
|
|
9
|
+
(r'localStorage\.getItem\s*\([^)]*admin',
|
|
10
|
+
"Admin flag read from localStorage — can be tampered by user"),
|
|
11
|
+
(r'localStorage\.getItem\s*\([^)]*permission',
|
|
12
|
+
"Permission read from localStorage — can be tampered by user"),
|
|
13
|
+
(r'if\s*\([^)]*localStorage[^)]*admin',
|
|
14
|
+
"Admin check using localStorage value — client-side trust"),
|
|
15
|
+
(r'params\.(role|admin|isAdmin|permission)\s*===',
|
|
16
|
+
"Role/permission check using URL params — can be manipulated"),
|
|
17
|
+
(r'searchParams\.(get|role|admin)',
|
|
18
|
+
"Role read from URL search params — easily manipulated"),
|
|
19
|
+
(r'isAdmin\s*=\s*.*localStorage',
|
|
20
|
+
"isAdmin derived from localStorage — insecure"),
|
|
21
|
+
(r'userRole\s*=\s*.*localStorage',
|
|
22
|
+
"userRole derived from localStorage — insecure"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
SKIP_FILES = {"README.md", "readme.md"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_roles(file_path, content):
|
|
29
|
+
findings = []
|
|
30
|
+
|
|
31
|
+
filename = file_path.split("/")[-1].split("\\")[-1]
|
|
32
|
+
if filename in SKIP_FILES:
|
|
33
|
+
return findings
|
|
34
|
+
|
|
35
|
+
ext = file_path.split(".")[-1].lower()
|
|
36
|
+
if ext not in {"js", "ts", "jsx", "tsx", "py"}:
|
|
37
|
+
return findings
|
|
38
|
+
|
|
39
|
+
lines = content.splitlines()
|
|
40
|
+
for line_num, line in enumerate(lines, 1):
|
|
41
|
+
stripped = line.strip()
|
|
42
|
+
if stripped.startswith("//") or stripped.startswith("#"):
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
for pattern, description in PATTERNS:
|
|
46
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
47
|
+
findings.append({
|
|
48
|
+
"rule": RULE_NAME,
|
|
49
|
+
"severity": SEVERITY,
|
|
50
|
+
"file": file_path,
|
|
51
|
+
"line": line_num,
|
|
52
|
+
"message": description,
|
|
53
|
+
"fix_hint": "Always verify roles server-side. Never trust client-provided role values.",
|
|
54
|
+
"code_snippet": line.strip()[:80],
|
|
55
|
+
})
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
return findings
|
vibesec/rules/secrets.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
RULE_NAME = "Hardcoded Secret"
|
|
4
|
+
SEVERITY = "CRITICAL"
|
|
5
|
+
|
|
6
|
+
# Patterns that indicate hardcoded secrets
|
|
7
|
+
PATTERNS = [
|
|
8
|
+
(r'api_key\s*=\s*["\'][a-zA-Z0-9_\-]{16,}["\']', "Hardcoded API key"),
|
|
9
|
+
(r'api_secret\s*=\s*["\'][a-zA-Z0-9_\-]{16,}["\']', "Hardcoded API secret"),
|
|
10
|
+
(r'password\s*=\s*["\'][^"\']{6,}["\']', "Hardcoded password"),
|
|
11
|
+
(r'secret_key\s*=\s*["\'][^"\']{8,}["\']', "Hardcoded secret key"),
|
|
12
|
+
(r'sk-[a-zA-Z0-9]{48}', "OpenAI API key"),
|
|
13
|
+
(r'ghp_[a-zA-Z0-9]{36}', "GitHub personal access token"),
|
|
14
|
+
(r'SUPABASE_SERVICE_KEY\s*=\s*["\'][^"\']+["\']', "Supabase service key exposed"),
|
|
15
|
+
(r'SUPABASE_SECRET\s*=\s*["\'][^"\']+["\']', "Supabase secret exposed"),
|
|
16
|
+
(r'stripe[_\s]secret\s*=\s*["\'][^"\']+["\']', "Stripe secret key"),
|
|
17
|
+
(r'sk_live_[a-zA-Z0-9]{24,}', "Stripe live secret key"),
|
|
18
|
+
(r'AUTH_SECRET\s*=\s*["\'][^"\']{8,}["\']', "Auth secret hardcoded"),
|
|
19
|
+
(r'DATABASE_URL\s*=\s*["\']postgresql://[^"\']+["\']', "Database URL with credentials"),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# Files to skip — these are expected to have these patterns
|
|
23
|
+
SKIP_FILES = {
|
|
24
|
+
".env.example", ".env.sample", ".env.template",
|
|
25
|
+
"README.md", "readme.md", ".gitignore"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check_secrets(file_path, content):
|
|
30
|
+
findings = []
|
|
31
|
+
|
|
32
|
+
# Skip example/template files
|
|
33
|
+
filename = file_path.split("/")[-1].split("\\")[-1]
|
|
34
|
+
if filename in SKIP_FILES:
|
|
35
|
+
return findings
|
|
36
|
+
|
|
37
|
+
# Skip .env files that are in .gitignore — but still flag if exposed
|
|
38
|
+
lines = content.splitlines()
|
|
39
|
+
|
|
40
|
+
for line_num, line in enumerate(lines, 1):
|
|
41
|
+
# Skip comments
|
|
42
|
+
stripped = line.strip()
|
|
43
|
+
if stripped.startswith("#") or stripped.startswith("//"):
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
for pattern, description in PATTERNS:
|
|
47
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
48
|
+
findings.append({
|
|
49
|
+
"rule": RULE_NAME,
|
|
50
|
+
"severity": SEVERITY,
|
|
51
|
+
"file": file_path,
|
|
52
|
+
"line": line_num,
|
|
53
|
+
"message": f"{description} detected in source code",
|
|
54
|
+
"fix_hint": "Move to environment variables. Never commit secrets to git.",
|
|
55
|
+
"code_snippet": line.strip()[:80],
|
|
56
|
+
})
|
|
57
|
+
break # One finding per line is enough
|
|
58
|
+
|
|
59
|
+
return findings
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
RULE_NAME = "Source Map Exposure"
|
|
5
|
+
SEVERITY = "HIGH"
|
|
6
|
+
|
|
7
|
+
PATTERNS = [
|
|
8
|
+
(r'["\']?sourceMap["\']?\s*[:=]\s*true', "Source maps enabled in build config"),
|
|
9
|
+
(r'GENERATE_SOURCEMAP\s*=\s*true', "Create React App source maps enabled"),
|
|
10
|
+
(r'devtool\s*:\s*["\']source-map["\']', "Webpack source-map devtool enabled"),
|
|
11
|
+
(r'\"source-map\"', "Source map configuration detected"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_sourcemaps(file_path, content):
|
|
16
|
+
findings = []
|
|
17
|
+
|
|
18
|
+
filename = file_path.split("/")[-1].split("\\")[-1]
|
|
19
|
+
ext = file_path.split(".")[-1].lower()
|
|
20
|
+
|
|
21
|
+
# Check .map files committed to repo
|
|
22
|
+
if filename.endswith(".map"):
|
|
23
|
+
# Check if it's in a build/dist directory
|
|
24
|
+
if any(d in file_path for d in ["dist/", "build/", ".next/", "out/"]):
|
|
25
|
+
findings.append({
|
|
26
|
+
"rule": RULE_NAME,
|
|
27
|
+
"severity": SEVERITY,
|
|
28
|
+
"file": file_path,
|
|
29
|
+
"line": "N/A",
|
|
30
|
+
"message": "Source map file committed to repository — exposes full source code",
|
|
31
|
+
"fix_hint": "Add *.map to .gitignore. Set sourceMap: false in production builds.",
|
|
32
|
+
"code_snippet": filename,
|
|
33
|
+
})
|
|
34
|
+
return findings
|
|
35
|
+
|
|
36
|
+
# Check build config files
|
|
37
|
+
config_files = {
|
|
38
|
+
"webpack.config.js", "webpack.config.ts",
|
|
39
|
+
"next.config.js", "next.config.ts",
|
|
40
|
+
"vite.config.js", "vite.config.ts",
|
|
41
|
+
".env", ".env.production", ".env.prod"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if filename not in config_files and ext not in {"json"}:
|
|
45
|
+
return findings
|
|
46
|
+
|
|
47
|
+
lines = content.splitlines()
|
|
48
|
+
for line_num, line in enumerate(lines, 1):
|
|
49
|
+
stripped = line.strip()
|
|
50
|
+
if stripped.startswith("//") or stripped.startswith("#"):
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
for pattern, description in PATTERNS:
|
|
54
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
55
|
+
findings.append({
|
|
56
|
+
"rule": RULE_NAME,
|
|
57
|
+
"severity": SEVERITY,
|
|
58
|
+
"file": file_path,
|
|
59
|
+
"line": line_num,
|
|
60
|
+
"message": description,
|
|
61
|
+
"fix_hint": "Set sourceMap: false in production. Use hidden-source-map if debugging is needed.",
|
|
62
|
+
"code_snippet": line.strip()[:80],
|
|
63
|
+
})
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
return findings
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
RULE_NAME = "Missing Webhook Verification"
|
|
4
|
+
SEVERITY = "MEDIUM"
|
|
5
|
+
|
|
6
|
+
PATTERNS = [
|
|
7
|
+
(r'stripe\.webhooks(?!.*constructEvent)',
|
|
8
|
+
"Stripe webhook used without constructEvent signature verification"),
|
|
9
|
+
(r'req\.body.*stripe(?!.*stripe-signature)',
|
|
10
|
+
"Stripe webhook body read without signature header check"),
|
|
11
|
+
(r'x-github-event(?!.*x-hub-signature)',
|
|
12
|
+
"GitHub webhook received without hub signature verification"),
|
|
13
|
+
(r'webhook.*payload(?!.*secret)',
|
|
14
|
+
"Webhook payload processed without secret verification"),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
# Positive indicators that webhook IS being verified
|
|
18
|
+
SAFE_INDICATORS = [
|
|
19
|
+
"constructEvent",
|
|
20
|
+
"stripe-signature",
|
|
21
|
+
"x-hub-signature",
|
|
22
|
+
"webhook_secret",
|
|
23
|
+
"WEBHOOK_SECRET",
|
|
24
|
+
"verifySignature",
|
|
25
|
+
"crypto.timingSafeEqual",
|
|
26
|
+
"hmac",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
SKIP_FILES = {"README.md", "readme.md"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_webhooks(file_path, content):
|
|
33
|
+
findings = []
|
|
34
|
+
|
|
35
|
+
filename = file_path.split("/")[-1].split("\\")[-1]
|
|
36
|
+
if filename in SKIP_FILES:
|
|
37
|
+
return findings
|
|
38
|
+
|
|
39
|
+
ext = file_path.split(".")[-1].lower()
|
|
40
|
+
if ext not in {"py", "js", "ts", "jsx", "tsx"}:
|
|
41
|
+
return findings
|
|
42
|
+
|
|
43
|
+
# Only scan files that mention webhooks
|
|
44
|
+
if "webhook" not in content.lower() and "stripe" not in content.lower():
|
|
45
|
+
return findings
|
|
46
|
+
|
|
47
|
+
lines = content.splitlines()
|
|
48
|
+
for line_num, line in enumerate(lines, 1):
|
|
49
|
+
stripped = line.strip()
|
|
50
|
+
if stripped.startswith("//") or stripped.startswith("#"):
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
for pattern, description in PATTERNS:
|
|
54
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
55
|
+
# Check surrounding context for safe indicators
|
|
56
|
+
start = max(0, line_num - 10)
|
|
57
|
+
end = min(len(lines), line_num + 10)
|
|
58
|
+
surrounding = "\n".join(lines[start:end])
|
|
59
|
+
|
|
60
|
+
is_safe = any(
|
|
61
|
+
indicator in surrounding
|
|
62
|
+
for indicator in SAFE_INDICATORS
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not is_safe:
|
|
66
|
+
findings.append({
|
|
67
|
+
"rule": RULE_NAME,
|
|
68
|
+
"severity": SEVERITY,
|
|
69
|
+
"file": file_path,
|
|
70
|
+
"line": line_num,
|
|
71
|
+
"message": description,
|
|
72
|
+
"fix_hint": "Verify webhook signatures using the provider's SDK. For Stripe use stripe.webhooks.constructEvent().",
|
|
73
|
+
"code_snippet": line.strip()[:80],
|
|
74
|
+
})
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
return findings
|
vibesec/rules/xss.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
RULE_NAME = "Unsafe HTML Injection (XSS)"
|
|
4
|
+
SEVERITY = "MEDIUM"
|
|
5
|
+
|
|
6
|
+
PATTERNS = [
|
|
7
|
+
(r'dangerouslySetInnerHTML\s*=\s*\{\s*\{',
|
|
8
|
+
"dangerouslySetInnerHTML used — potential XSS vulnerability"),
|
|
9
|
+
(r'dangerouslySetInnerHTML.*\$\{',
|
|
10
|
+
"dangerouslySetInnerHTML with template literal — XSS risk"),
|
|
11
|
+
(r'dangerouslySetInnerHTML.*props\.',
|
|
12
|
+
"dangerouslySetInnerHTML with props value — XSS risk"),
|
|
13
|
+
(r'dangerouslySetInnerHTML.*state\.',
|
|
14
|
+
"dangerouslySetInnerHTML with state value — XSS risk"),
|
|
15
|
+
(r'innerHTML\s*=\s*[^"\'`][^\n]*\+',
|
|
16
|
+
"innerHTML set with concatenated value — XSS risk"),
|
|
17
|
+
(r'document\.write\s*\(',
|
|
18
|
+
"document.write used — XSS risk"),
|
|
19
|
+
(r'eval\s*\(\s*[^"\'`]',
|
|
20
|
+
"eval() called with dynamic value — code injection risk"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
SKIP_FILES = {"README.md", "readme.md"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check_xss(file_path, content):
|
|
27
|
+
findings = []
|
|
28
|
+
|
|
29
|
+
filename = file_path.split("/")[-1].split("\\")[-1]
|
|
30
|
+
if filename in SKIP_FILES:
|
|
31
|
+
return findings
|
|
32
|
+
|
|
33
|
+
ext = file_path.split(".")[-1].lower()
|
|
34
|
+
if ext not in {"js", "ts", "jsx", "tsx"}:
|
|
35
|
+
return findings
|
|
36
|
+
|
|
37
|
+
lines = content.splitlines()
|
|
38
|
+
for line_num, line in enumerate(lines, 1):
|
|
39
|
+
stripped = line.strip()
|
|
40
|
+
if stripped.startswith("//"):
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
for pattern, description in PATTERNS:
|
|
44
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
45
|
+
findings.append({
|
|
46
|
+
"rule": RULE_NAME,
|
|
47
|
+
"severity": SEVERITY,
|
|
48
|
+
"file": file_path,
|
|
49
|
+
"line": line_num,
|
|
50
|
+
"message": description,
|
|
51
|
+
"fix_hint": "Sanitize HTML with DOMPurify before rendering. Avoid dangerouslySetInnerHTML with user input.",
|
|
52
|
+
"code_snippet": line.strip()[:80],
|
|
53
|
+
})
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
return findings
|
vibesec/scanner.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from vibesec.utils import walk_files, read_file
|
|
2
|
+
from vibesec.rules import ALL_RULES
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Scanner:
|
|
6
|
+
|
|
7
|
+
def __init__(self, path):
|
|
8
|
+
self.path = path
|
|
9
|
+
|
|
10
|
+
def run(self):
|
|
11
|
+
findings = []
|
|
12
|
+
files = list(walk_files(self.path))
|
|
13
|
+
|
|
14
|
+
for file_path in files:
|
|
15
|
+
content = read_file(file_path)
|
|
16
|
+
if not content:
|
|
17
|
+
continue
|
|
18
|
+
for rule in ALL_RULES:
|
|
19
|
+
try:
|
|
20
|
+
results = rule(file_path, content)
|
|
21
|
+
findings.extend(results)
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
return findings
|
vibesec/utils.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
SUPPORTED_EXTENSIONS = {
|
|
4
|
+
".py", ".js", ".ts", ".jsx", ".tsx",
|
|
5
|
+
".json", ".yaml", ".yml", ".env", ".sql"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
def walk_files(path):
|
|
9
|
+
"""Recursively yield all supported files in a directory."""
|
|
10
|
+
if os.path.isfile(path):
|
|
11
|
+
yield path
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
for root, dirs, files in os.walk(path):
|
|
15
|
+
# Skip common non-code directories
|
|
16
|
+
dirs[:] = [d for d in dirs if d not in {
|
|
17
|
+
"node_modules", ".git", "venv", "__pycache__",
|
|
18
|
+
".next", "dist", "build", ".venv"
|
|
19
|
+
}]
|
|
20
|
+
for file in files:
|
|
21
|
+
ext = os.path.splitext(file)[1].lower()
|
|
22
|
+
if ext in SUPPORTED_EXTENSIONS:
|
|
23
|
+
yield os.path.join(root, file)
|
|
24
|
+
|
|
25
|
+
def read_file(path):
|
|
26
|
+
"""Read file content safely."""
|
|
27
|
+
try:
|
|
28
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
29
|
+
return f.read()
|
|
30
|
+
except Exception:
|
|
31
|
+
return ""
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vibesec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Security scanner for AI-generated code
|
|
5
|
+
Author-email: Ayush Khati <ayushiskhati305@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Ayush Khati
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/AyushkhatiDev/vibesec
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/AyushkhatiDev/vibesec/issues
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Classifier: Topic :: Security
|
|
34
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
35
|
+
Requires-Python: >=3.8
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
License-File: LICENSE
|
|
38
|
+
Requires-Dist: click>=8.0
|
|
39
|
+
Requires-Dist: rich>=13.0
|
|
40
|
+
Requires-Dist: requests>=2.28
|
|
41
|
+
Requires-Dist: groq>=0.4.0
|
|
42
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# 🔒 VibeSec
|
|
46
|
+
|
|
47
|
+
**Security scanner for AI-generated code.**
|
|
48
|
+
|
|
49
|
+
[](https://badge.fury.io/py/vibesec)
|
|
50
|
+
[](https://opensource.org/licenses/MIT)
|
|
51
|
+
[](https://www.python.org/downloads/)
|
|
52
|
+
[](https://github.com/AyushkhatiDev/vibesec)
|
|
53
|
+
|
|
54
|
+
45% of AI-generated code ships with critical vulnerabilities. Cursor, Claude Code, Bolt, and Lovable generate insecure patterns that existing tools miss. VibeSec catches them before you deploy.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
$ vibesec scan ./my-cursor-app
|
|
58
|
+
|
|
59
|
+
VibeSec v0.1.0 — AI-Generated Code Security Scanner
|
|
60
|
+
|
|
61
|
+
● CRITICAL 7 findings
|
|
62
|
+
● HIGH 2 findings
|
|
63
|
+
|
|
64
|
+
CRITICAL — Hardcoded Secret
|
|
65
|
+
File: src/lib/supabase.ts Line: 12
|
|
66
|
+
Found: SUPABASE_SERVICE_KEY hardcoded in source code
|
|
67
|
+
Fix: Move to environment variables. Never commit secrets to git.
|
|
68
|
+
|
|
69
|
+
CRITICAL — Supabase RLS Disabled
|
|
70
|
+
File: supabase/migrations/001_init.sql Line: 34
|
|
71
|
+
Found: ALTER TABLE users DISABLE ROW LEVEL SECURITY
|
|
72
|
+
Fix: Enable RLS + add user isolation policies.
|
|
73
|
+
|
|
74
|
+
9 findings in ./my-cursor-app
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Why VibeSec
|
|
80
|
+
|
|
81
|
+
Existing tools like Semgrep, Snyk, and CodeQL are great — but they were built for human-written code. AI tools generate specific anti-patterns that these scanners miss:
|
|
82
|
+
|
|
83
|
+
| Pattern | Semgrep | Snyk | VibeSec |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| Hardcoded secrets | ✓ | ✓ | ✓ |
|
|
86
|
+
| Supabase RLS disabled | ✗ | ✗ | ✓ |
|
|
87
|
+
| Hallucinated npm packages | ✗ | ✗ | ✓ |
|
|
88
|
+
| Missing auth on scaffolded routes | Partial | ✗ | ✓ |
|
|
89
|
+
| Source map exposure in build config | ✗ | ✗ | ✓ |
|
|
90
|
+
| AI-specific JWT misuse | ✗ | ✗ | ✓ |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Install
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install vibesec
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Usage
|
|
103
|
+
|
|
104
|
+
**Scan a directory:**
|
|
105
|
+
```bash
|
|
106
|
+
vibesec scan ./my-project
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Scan and get AI-powered fix suggestions:**
|
|
110
|
+
```bash
|
|
111
|
+
vibesec scan ./my-project --fix
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Export results as JSON (for CI/CD):**
|
|
115
|
+
```bash
|
|
116
|
+
vibesec scan ./my-project --output json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Filter by severity:**
|
|
120
|
+
```bash
|
|
121
|
+
vibesec scan ./my-project --severity critical
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Ignore specific checks:**
|
|
125
|
+
```bash
|
|
126
|
+
vibesec scan ./my-project --ignore rls,cors
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## What VibeSec Checks
|
|
132
|
+
|
|
133
|
+
### 🔴 CRITICAL
|
|
134
|
+
|
|
135
|
+
**1. Hardcoded Secrets**
|
|
136
|
+
API keys, passwords, tokens, and database URLs hardcoded in source files. LLMs replicate tutorial patterns where secrets are hardcoded.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# VibeSec catches this
|
|
140
|
+
api_key = "sk-abc123..."
|
|
141
|
+
SUPABASE_SERVICE_KEY = "eyJhbGci..."
|
|
142
|
+
stripe_secret = "sk_live_..."
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**2. Supabase RLS Disabled**
|
|
146
|
+
Row Level Security disabled — any authenticated user can read or modify all data. LLMs skip RLS to make queries work quickly in scaffolding.
|
|
147
|
+
|
|
148
|
+
```sql
|
|
149
|
+
-- VibeSec catches this
|
|
150
|
+
ALTER TABLE users DISABLE ROW LEVEL SECURITY;
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 🟡 HIGH
|
|
154
|
+
|
|
155
|
+
**3. Missing Route Authentication**
|
|
156
|
+
Admin and sensitive API routes scaffolded without authentication middleware. LLMs build the happy path without thinking about access control.
|
|
157
|
+
|
|
158
|
+
**4. Hallucinated Packages**
|
|
159
|
+
npm packages that don't exist — a typosquatting attack surface. LLMs generate plausible-sounding package names that aren't real.
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
// VibeSec catches this
|
|
163
|
+
"react-auth-handler": "^1.0.0",
|
|
164
|
+
"supabase-helpers": "^2.1.0"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**5. Source Map Exposure**
|
|
168
|
+
Build config exposes full source code via `.map` files in production.
|
|
169
|
+
|
|
170
|
+
### 🟠 MEDIUM
|
|
171
|
+
|
|
172
|
+
**6. Unsafe JWT Handling** — JWT decoded without verification, or `none` algorithm accepted
|
|
173
|
+
|
|
174
|
+
**7. dangerouslySetInnerHTML** — Direct HTML injection without sanitization
|
|
175
|
+
|
|
176
|
+
**8. Client-Side Role Trust** — Admin checks done using `localStorage` values
|
|
177
|
+
|
|
178
|
+
**9. Missing Webhook Verification** — Stripe/GitHub webhooks without signature check
|
|
179
|
+
|
|
180
|
+
**10. Permissive CORS** — Wildcard CORS with credentials enabled
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## GitHub Actions Integration
|
|
185
|
+
|
|
186
|
+
Add VibeSec to your CI/CD pipeline:
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
# .github/workflows/vibesec.yml
|
|
190
|
+
name: VibeSec Security Scan
|
|
191
|
+
|
|
192
|
+
on: [push, pull_request]
|
|
193
|
+
|
|
194
|
+
jobs:
|
|
195
|
+
security:
|
|
196
|
+
runs-on: ubuntu-latest
|
|
197
|
+
steps:
|
|
198
|
+
- uses: actions/checkout@v3
|
|
199
|
+
- uses: actions/setup-python@v4
|
|
200
|
+
with:
|
|
201
|
+
python-version: '3.11'
|
|
202
|
+
- name: Install VibeSec
|
|
203
|
+
run: pip install vibesec
|
|
204
|
+
- name: Run Security Scan
|
|
205
|
+
run: vibesec scan . --output json --severity high
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Development
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
git clone https://github.com/AyushkhatiDev/vibesec
|
|
214
|
+
cd vibesec
|
|
215
|
+
python -m venv venv
|
|
216
|
+
source venv/bin/activate
|
|
217
|
+
pip install -e ".[dev]"
|
|
218
|
+
pytest tests/
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Contributing
|
|
224
|
+
|
|
225
|
+
VibeSec is open source and contributions are welcome.
|
|
226
|
+
|
|
227
|
+
**Adding a new rule:**
|
|
228
|
+
1. Create `vibesec/rules/your_rule.py`
|
|
229
|
+
2. Implement `check_your_rule(file_path, content) -> list[dict]`
|
|
230
|
+
3. Register it in `vibesec/rules/__init__.py`
|
|
231
|
+
4. Add test cases in `tests/corpus/`
|
|
232
|
+
5. Open a PR
|
|
233
|
+
|
|
234
|
+
Each finding must return:
|
|
235
|
+
```python
|
|
236
|
+
{
|
|
237
|
+
"rule": "Rule Name",
|
|
238
|
+
"severity": "CRITICAL|HIGH|MEDIUM|LOW",
|
|
239
|
+
"file": file_path,
|
|
240
|
+
"line": line_number,
|
|
241
|
+
"message": "What was found",
|
|
242
|
+
"fix_hint": "How to fix it",
|
|
243
|
+
"code_snippet": "offending line"
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for full guide.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Roadmap
|
|
252
|
+
|
|
253
|
+
- [x] Secrets detection
|
|
254
|
+
- [x] Supabase RLS checker
|
|
255
|
+
- [x] Missing auth on routes
|
|
256
|
+
- [x] Hallucinated package detector
|
|
257
|
+
- [x] Source map exposure
|
|
258
|
+
- [ ] JWT misuse rules
|
|
259
|
+
- [ ] dangerouslySetInnerHTML
|
|
260
|
+
- [ ] Client-side role trust
|
|
261
|
+
- [ ] Webhook verification
|
|
262
|
+
- [ ] Permissive CORS
|
|
263
|
+
- [ ] GitHub Action marketplace listing
|
|
264
|
+
- [ ] Web app (paste URL → get report)
|
|
265
|
+
- [ ] SARIF output for GitHub Security tab
|
|
266
|
+
- [ ] VS Code extension
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Built By
|
|
271
|
+
|
|
272
|
+
[Ayush Khati](https://github.com/AyushkhatiDev) — BCA student building real tools for real problems.
|
|
273
|
+
|
|
274
|
+
Found a bug? [Open an issue](https://github.com/AyushkhatiDev/vibesec/issues).
|
|
275
|
+
Want a rule added? [Start a discussion](https://github.com/AyushkhatiDev/vibesec/discussions).
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
MIT — free to use, modify, and distribute.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
<p align="center">
|
|
286
|
+
<sub>Built because 45% of vibe-coded apps ship with critical vulnerabilities. Someone had to fix that.</sub>
|
|
287
|
+
</p>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
vibesec/__init__.py,sha256=q31sV8RecGVcenSqZBLbtYG4iVMxR56N-gYs2to9Se8,49
|
|
2
|
+
vibesec/cli.py,sha256=i1B54upXZC0edXgSDYOFvl8rSk1b6eKSbQBGF-4qKGE,1050
|
|
3
|
+
vibesec/fixgen.py,sha256=CU0HFSqVLJvPrNTT1aum8oeTD6J2cG7_YCNV0ISTgsU,1153
|
|
4
|
+
vibesec/reporter.py,sha256=yfr1VQ3Bm58YhNyjFe0htCNZDqw_4VC5MEOe6Mamxuc,3435
|
|
5
|
+
vibesec/scanner.py,sha256=r8NGOBF1SFDUFH0Z5Puf8XvguGrvgr6-94TgVa2cvts,612
|
|
6
|
+
vibesec/utils.py,sha256=Cgpv0zbNeiGH7rQ-n9YOfySLoeTErB60QJ5ObRmihXE,905
|
|
7
|
+
vibesec/rules/__init__.py,sha256=F69yiHp72wnogMO8JNhYOwAguRqkeG45MrEklUe2SuM,633
|
|
8
|
+
vibesec/rules/auth_routes.py,sha256=qcu0yE6mnqkYt9CuPFyb79Zpnqcf3RlJqfeEBYp0HdI,2159
|
|
9
|
+
vibesec/rules/cors.py,sha256=nwsdt-38L9ZancdIpn1GrVBn_HYQbbWYDDrLzjuZB_0,2293
|
|
10
|
+
vibesec/rules/jwt.py,sha256=gJ_0GPIGHXYkIpsYBlEFTJ1yepR6SeTFkTWJcHE2duY,1663
|
|
11
|
+
vibesec/rules/packages.py,sha256=tGrJtlOKH79qdBbxkZalg3mzX89LtKTtFF1uajF8ayo,2594
|
|
12
|
+
vibesec/rules/rls.py,sha256=dSLefkEZPka2Xv-dpGDQbnffI42_XNgJoHlKn8cHxyU,1609
|
|
13
|
+
vibesec/rules/roles.py,sha256=0cSTUaupIvQAacaSNsh1wl9tpoVEc47NwbP0fJKqkCQ,2058
|
|
14
|
+
vibesec/rules/secrets.py,sha256=oMyQMjievSOek9UYEOovAEQAHfjur4YrdaRnkND3z6g,2286
|
|
15
|
+
vibesec/rules/sourcemaps.py,sha256=-7UsB_EcC-CEavj_v3pmGwMoRjgDRROr0hpRB_M97RQ,2316
|
|
16
|
+
vibesec/rules/webhooks.py,sha256=NXZy32ykHlX7R3i-Vs9eylBkQfXCZ9vmvjvd7dPGZhA,2501
|
|
17
|
+
vibesec/rules/xss.py,sha256=N6w_736r0YsTRoMvFadQ6YX9rNj4Eymnd8MU1Mg6nEo,1869
|
|
18
|
+
vibesec-0.1.0.dist-info/licenses/LICENSE,sha256=C85Ww3BMTD0gQt9J9N7MP_SbWl9EyLyp9yjc5eg0doQ,1068
|
|
19
|
+
vibesec-0.1.0.dist-info/METADATA,sha256=yfg6sphnIwhCkQ7ku6r2MPtQetLeE0HUcRlxFLIGSPs,8135
|
|
20
|
+
vibesec-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
21
|
+
vibesec-0.1.0.dist-info/entry_points.txt,sha256=MLazU6qKZaH2PHfOE1g_ybPvEKwDIBp51WGH4LMFmF0,45
|
|
22
|
+
vibesec-0.1.0.dist-info/top_level.txt,sha256=8Ivz0xZCNXO4NwrGs5xoUaYa_sQXqkq94Xg9yyvEU0g,8
|
|
23
|
+
vibesec-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ayush Khati
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vibesec
|