vibescan-cli 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.
Files changed (39) hide show
  1. vibescan/__init__.py +3 -0
  2. vibescan/__main__.py +5 -0
  3. vibescan/aggregator/__init__.py +0 -0
  4. vibescan/cli.py +85 -0
  5. vibescan/collector/__init__.py +4 -0
  6. vibescan/collector/context.py +19 -0
  7. vibescan/collector/file_collector.py +90 -0
  8. vibescan/collector/gitignore_parser.py +24 -0
  9. vibescan/models/__init__.py +4 -0
  10. vibescan/models/issue.py +45 -0
  11. vibescan/models/scan_result.py +27 -0
  12. vibescan/reporters/__init__.py +0 -0
  13. vibescan/reporters/console.py +89 -0
  14. vibescan/rules/__init__.py +4 -0
  15. vibescan/rules/base.py +14 -0
  16. vibescan/rules/dangerous_patterns.py +296 -0
  17. vibescan/rules/git_hygiene.py +175 -0
  18. vibescan/rules/registry.py +44 -0
  19. vibescan/rules/secret/__init__.py +0 -0
  20. vibescan/rules/secret/cicd_pipeline.py +67 -0
  21. vibescan/rules/secret/cloud_credentials.py +69 -0
  22. vibescan/rules/secret/config_hardcode.py +65 -0
  23. vibescan/rules/secret/data_files.py +84 -0
  24. vibescan/rules/secret/doc_secrets.py +69 -0
  25. vibescan/rules/secret/docker_infra.py +75 -0
  26. vibescan/rules/secret/editor_remnants.py +91 -0
  27. vibescan/rules/secret/env_exposure.py +57 -0
  28. vibescan/rules/secret/frontend_env.py +75 -0
  29. vibescan/rules/secret/hardcoded_patterns.py +108 -0
  30. vibescan/rules/secret/ide_settings.py +82 -0
  31. vibescan/rules/secret/mobile_files.py +57 -0
  32. vibescan/rules/secret/private_keys.py +67 -0
  33. vibescan/rules/secret/system_configs.py +71 -0
  34. vibescan/rules/structure.py +168 -0
  35. vibescan/templates/.gitkeep +0 -0
  36. vibescan_cli-0.1.0.dist-info/METADATA +220 -0
  37. vibescan_cli-0.1.0.dist-info/RECORD +39 -0
  38. vibescan_cli-0.1.0.dist-info/WHEEL +4 -0
  39. vibescan_cli-0.1.0.dist-info/entry_points.txt +2 -0
vibescan/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """VibeScan - Vibe-coded project security scanner."""
2
+
3
+ __version__ = "0.1.0"
vibescan/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running with `python -m vibescan`."""
2
+
3
+ from vibescan.cli import app
4
+
5
+ app()
File without changes
vibescan/cli.py ADDED
@@ -0,0 +1,85 @@
1
+ """CLI entry point using typer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from vibescan import __version__
10
+
11
+ app = typer.Typer(
12
+ name="vibescan",
13
+ help="Scan your project for leaked secrets and security issues.",
14
+ add_completion=False,
15
+ )
16
+
17
+
18
+ def _version_callback(value: bool) -> None:
19
+ if value:
20
+ typer.echo(f"vibescan {__version__}")
21
+ raise typer.Exit()
22
+
23
+
24
+ @app.command()
25
+ def scan(
26
+ path: Path = typer.Argument(
27
+ ".",
28
+ help="Project directory to scan.",
29
+ exists=True,
30
+ file_okay=False,
31
+ resolve_path=True,
32
+ ),
33
+ min_severity: str = typer.Option(
34
+ "info",
35
+ "--min-severity", "-s",
36
+ help="Minimum severity to report: critical, high, medium, low, info.",
37
+ ),
38
+ version: bool = typer.Option(
39
+ False,
40
+ "--version", "-v",
41
+ help="Show version and exit.",
42
+ callback=_version_callback,
43
+ is_eager=True,
44
+ ),
45
+ ) -> None:
46
+ """Scan a project directory for security issues."""
47
+ from vibescan.collector import collect
48
+ from vibescan.models import ScanResult, Severity
49
+ from vibescan.reporters.console import print_report
50
+ from vibescan.rules import get_all_rules
51
+
52
+ # Validate min_severity
53
+ try:
54
+ threshold = Severity(min_severity.lower())
55
+ except ValueError:
56
+ typer.echo(f"Error: Invalid severity '{min_severity}'. "
57
+ f"Choose from: critical, high, medium, low, info.")
58
+ raise typer.Exit(code=2)
59
+
60
+ # Collect project context
61
+ ctx = collect(path)
62
+
63
+ # Run rules
64
+ all_issues = []
65
+ for rule in get_all_rules():
66
+ all_issues.extend(rule.run(ctx))
67
+
68
+ # Filter by severity
69
+ filtered = [i for i in all_issues if i.severity >= threshold]
70
+
71
+ # Sort: critical first
72
+ filtered.sort(key=lambda i: -i.severity.rank)
73
+
74
+ # Build result
75
+ result = ScanResult(
76
+ issues=filtered,
77
+ project_root=str(ctx.project_root),
78
+ files_scanned=len(ctx.text_files),
79
+ files_skipped=len(ctx.skipped_files),
80
+ )
81
+
82
+ # Report
83
+ print_report(result)
84
+
85
+ raise typer.Exit(code=result.exit_code)
@@ -0,0 +1,4 @@
1
+ from vibescan.collector.context import ProjectContext
2
+ from vibescan.collector.file_collector import collect
3
+
4
+ __all__ = ["ProjectContext", "collect"]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass
8
+ class TextFile:
9
+ path: str
10
+ content: str
11
+
12
+
13
+ @dataclass
14
+ class ProjectContext:
15
+ project_root: Path
16
+ text_files: list[TextFile] = field(default_factory=list)
17
+ all_files: list[str] = field(default_factory=list)
18
+ gitignore_patterns: list[str] = field(default_factory=list)
19
+ skipped_files: list[str] = field(default_factory=list)
@@ -0,0 +1,90 @@
1
+ """File Collector - collects text files, all file paths, and .gitignore patterns."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from vibescan.collector.context import ProjectContext, TextFile
8
+ from vibescan.collector.gitignore_parser import parse_gitignore_files
9
+
10
+ EXCLUDED_DIRS: set[str] = {
11
+ "node_modules", ".git", ".venv", "venv",
12
+ "build", "dist", "coverage", "__pycache__",
13
+ ".next", ".nuxt", ".output",
14
+ "vendor", "target", ".gradle",
15
+ ".tox", "eggs", ".mypy_cache", ".pytest_cache",
16
+ }
17
+
18
+ TEXT_EXTENSIONS: set[str] = {
19
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs",
20
+ ".java", ".go", ".rs", ".rb", ".php", ".c", ".cpp", ".h",
21
+ ".cs", ".swift", ".kt", ".kts", ".scala",
22
+ ".html", ".htm", ".css", ".scss", ".less",
23
+ ".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf",
24
+ ".xml", ".csv", ".sql", ".sh", ".bash", ".zsh",
25
+ ".md", ".txt", ".rst", ".env", ".properties",
26
+ ".gradle", ".sbt", ".pom",
27
+ ".tf", ".hcl", ".vue", ".svelte",
28
+ }
29
+
30
+ TEXT_FILENAMES: set[str] = {
31
+ "Dockerfile", "Makefile", "Jenkinsfile", "Procfile",
32
+ "Caddyfile", "Gemfile", "Rakefile", "Fastfile",
33
+ ".gitignore", ".dockerignore", ".editorconfig",
34
+ ".htaccess", ".npmrc", ".pypirc", ".netrc",
35
+ ".pgpass", ".my.cnf", ".boto",
36
+ }
37
+
38
+ MAX_FILE_SIZE: int = 5 * 1024 * 1024 # 5MB
39
+
40
+
41
+ def _is_text_file(path: Path) -> bool:
42
+ return path.suffix.lower() in TEXT_EXTENSIONS or path.name in TEXT_FILENAMES
43
+
44
+
45
+ def _should_skip_dir(name: str) -> bool:
46
+ return name in EXCLUDED_DIRS or name.endswith(".egg-info")
47
+
48
+
49
+ def collect(root: Path) -> ProjectContext:
50
+ root = root.resolve()
51
+ ctx = ProjectContext(project_root=root)
52
+
53
+ for item in _walk(root):
54
+ rel = str(item.relative_to(root))
55
+ ctx.all_files.append(rel)
56
+
57
+ if not _is_text_file(item):
58
+ continue
59
+
60
+ if item.stat().st_size > MAX_FILE_SIZE:
61
+ ctx.skipped_files.append(rel)
62
+ continue
63
+
64
+ try:
65
+ content = item.read_text(encoding="utf-8")
66
+ except (UnicodeDecodeError, PermissionError, OSError):
67
+ ctx.skipped_files.append(rel)
68
+ continue
69
+
70
+ ctx.text_files.append(TextFile(path=rel, content=content))
71
+
72
+ ctx.gitignore_patterns = parse_gitignore_files(root)
73
+ return ctx
74
+
75
+
76
+ def _walk(root: Path):
77
+ """Recursively yield files, skipping excluded dirs and symlinks."""
78
+ try:
79
+ entries = sorted(root.iterdir())
80
+ except PermissionError:
81
+ return
82
+
83
+ for entry in entries:
84
+ if entry.is_symlink():
85
+ continue
86
+ if entry.is_dir():
87
+ if not _should_skip_dir(entry.name):
88
+ yield from _walk(entry)
89
+ elif entry.is_file():
90
+ yield entry
@@ -0,0 +1,24 @@
1
+ """Parse .gitignore files and extract patterns."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def parse_gitignore_files(root: Path) -> list[str]:
9
+ patterns: list[str] = []
10
+
11
+ for gitignore in root.rglob(".gitignore"):
12
+ if gitignore.is_symlink():
13
+ continue
14
+ try:
15
+ text = gitignore.read_text(encoding="utf-8")
16
+ except (UnicodeDecodeError, PermissionError, OSError):
17
+ continue
18
+
19
+ for line in text.splitlines():
20
+ stripped = line.strip()
21
+ if stripped and not stripped.startswith("#"):
22
+ patterns.append(stripped)
23
+
24
+ return patterns
@@ -0,0 +1,4 @@
1
+ from vibescan.models.issue import Issue, Severity
2
+ from vibescan.models.scan_result import ScanResult
3
+
4
+ __all__ = ["Issue", "Severity", "ScanResult"]
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+
7
+ class Severity(str, Enum):
8
+ CRITICAL = "critical"
9
+ HIGH = "high"
10
+ MEDIUM = "medium"
11
+ LOW = "low"
12
+ INFO = "info"
13
+
14
+ @property
15
+ def rank(self) -> int:
16
+ return {
17
+ Severity.CRITICAL: 4,
18
+ Severity.HIGH: 3,
19
+ Severity.MEDIUM: 2,
20
+ Severity.LOW: 1,
21
+ Severity.INFO: 0,
22
+ }[self]
23
+
24
+ def __ge__(self, other: Severity) -> bool:
25
+ return self.rank >= other.rank
26
+
27
+ def __gt__(self, other: Severity) -> bool:
28
+ return self.rank > other.rank
29
+
30
+ def __le__(self, other: Severity) -> bool:
31
+ return self.rank <= other.rank
32
+
33
+ def __lt__(self, other: Severity) -> bool:
34
+ return self.rank < other.rank
35
+
36
+
37
+ @dataclass
38
+ class Issue:
39
+ rule_id: str
40
+ severity: Severity
41
+ file: str
42
+ line: int | None
43
+ message: str
44
+ why: str
45
+ fix: str
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from vibescan.models.issue import Issue, Severity
6
+
7
+
8
+ @dataclass
9
+ class ScanResult:
10
+ issues: list[Issue] = field(default_factory=list)
11
+ project_root: str = ""
12
+ files_scanned: int = 0
13
+ files_skipped: int = 0
14
+
15
+ @property
16
+ def summary(self) -> dict[str, int]:
17
+ counts: dict[str, int] = {}
18
+ for sev in Severity:
19
+ counts[sev.value] = sum(1 for i in self.issues if i.severity == sev)
20
+ return counts
21
+
22
+ @property
23
+ def exit_code(self) -> int:
24
+ for issue in self.issues:
25
+ if issue.severity in (Severity.CRITICAL, Severity.HIGH):
26
+ return 1
27
+ return 0
File without changes
@@ -0,0 +1,89 @@
1
+ """Console Reporter - rich-based colored terminal output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+ from rich.text import Text
9
+
10
+ from vibescan.models.issue import Severity
11
+ from vibescan.models.scan_result import ScanResult
12
+
13
+ SEVERITY_COLORS: dict[Severity, str] = {
14
+ Severity.CRITICAL: "bold red",
15
+ Severity.HIGH: "red",
16
+ Severity.MEDIUM: "yellow",
17
+ Severity.LOW: "cyan",
18
+ Severity.INFO: "dim",
19
+ }
20
+
21
+ SEVERITY_ICONS: dict[Severity, str] = {
22
+ Severity.CRITICAL: "[!]",
23
+ Severity.HIGH: "[H]",
24
+ Severity.MEDIUM: "[M]",
25
+ Severity.LOW: "[L]",
26
+ Severity.INFO: "[i]",
27
+ }
28
+
29
+
30
+ def print_report(result: ScanResult, console: Console | None = None) -> None:
31
+ console = console or Console()
32
+
33
+ # Header
34
+ console.print()
35
+ console.print(
36
+ Panel(
37
+ f"[bold]VibeScan[/bold] scanned [cyan]{result.files_scanned}[/cyan] files "
38
+ f"in [cyan]{result.project_root}[/cyan]",
39
+ title="Scan Complete",
40
+ border_style="blue",
41
+ )
42
+ )
43
+
44
+ if not result.issues:
45
+ console.print("\n[bold green]No issues found. Your project looks clean![/bold green]\n")
46
+ return
47
+
48
+ # Summary table
49
+ summary = result.summary
50
+ summary_table = Table(title="Summary", show_header=False, box=None, padding=(0, 2))
51
+ summary_table.add_column("Severity", style="bold")
52
+ summary_table.add_column("Count", justify="right")
53
+ for sev in Severity:
54
+ count = summary[sev.value]
55
+ if count > 0:
56
+ style = SEVERITY_COLORS[sev]
57
+ summary_table.add_row(
58
+ Text(sev.value.upper(), style=style),
59
+ Text(str(count), style=style),
60
+ )
61
+ console.print(summary_table)
62
+ console.print()
63
+
64
+ # Issues grouped by file
65
+ issues_by_file: dict[str, list] = {}
66
+ for issue in result.issues:
67
+ issues_by_file.setdefault(issue.file, []).append(issue)
68
+
69
+ for file_path, issues in sorted(issues_by_file.items()):
70
+ console.print(f"[bold underline]{file_path}[/bold underline]")
71
+ for issue in issues:
72
+ sev = issue.severity
73
+ icon = SEVERITY_ICONS[sev]
74
+ color = SEVERITY_COLORS[sev]
75
+ loc = f":{issue.line}" if issue.line else ""
76
+
77
+ console.print(f" [{color}]{icon}[/{color}] {issue.message}")
78
+ if issue.line:
79
+ console.print(f" Line {issue.line}")
80
+ console.print(f" [dim]Why:[/dim] {issue.why}")
81
+ console.print(f" [dim]Fix:[/dim] {issue.fix}")
82
+ console.print()
83
+
84
+ # Exit code hint
85
+ if result.exit_code != 0:
86
+ console.print("[bold red]Exit code 1: CRITICAL or HIGH issues found.[/bold red]")
87
+ else:
88
+ console.print("[bold green]Exit code 0: No critical issues.[/bold green]")
89
+ console.print()
@@ -0,0 +1,4 @@
1
+ from vibescan.rules.base import BaseRule
2
+ from vibescan.rules.registry import get_all_rules
3
+
4
+ __all__ = ["BaseRule", "get_all_rules"]
vibescan/rules/base.py ADDED
@@ -0,0 +1,14 @@
1
+ """Base class for all rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from vibescan.collector.context import ProjectContext
8
+ from vibescan.models.issue import Issue
9
+
10
+
11
+ class BaseRule(ABC):
12
+ @abstractmethod
13
+ def run(self, ctx: ProjectContext) -> list[Issue]:
14
+ ...