diff --git a/src/codeguard/cli/__init__.py b/src/codeguard/cli/__init__.py new file mode 100644 index 0000000..128ea02 --- /dev/null +++ b/src/codeguard/cli/__init__.py @@ -0,0 +1,202 @@ +"""CLI module for CodeGuard.""" + +import sys +from typing import Optional + +import click + +from codeguard.core.scanner import CodeScanner +from codeguard.git.hooks import HookManager +from codeguard.utils.config import ConfigLoader +from codeguard.utils.output import OutputFormatter + + +@click.group() +@click.option( + "--ollama-url", + default="http://localhost:11434", + help="Ollama server URL", + envvar="CODEGUARD_OLLAMA_URL", +) +@click.option( + "--model", + default="codellama", + help="Ollama model to use", + envvar="CODEGUARD_MODEL", +) +@click.option( + "--timeout", + default=120, + help="Request timeout in seconds", + envvar="CODEGUARD_TIMEOUT", + type=int, +) +@click.option( + "--config", + default="codeguard.yaml", + help="Path to config file", + envvar="CODEGUARD_CONFIG", +) +@click.pass_context +def main( + ctx: click.Context, + ollama_url: str, + model: str, + timeout: int, + config: str, +) -> None: + """CodeGuard: Local LLM-based code security analysis.""" + ctx.ensure_object(dict) + ctx.obj["ollama_url"] = ollama_url + ctx.obj["model"] = model + ctx.obj["timeout"] = timeout + ctx.obj["config"] = config + + +@main.command() +@click.option( + "--path", + default=".", + help="Path to scan", + type=click.Path(exists=True, file_okay=False, dir_okay=True), +) +@click.option( + "--output", + type=click.Choice(["text", "json"]), + default="text", + help="Output format", +) +@click.option( + "--fail-level", + type=click.Choice(["critical", "high", "medium", "low", "none"]), + default="none", + help="Exit with error if findings at or above this level", +) +@click.option( + "--include", + multiple=True, + help="File patterns to include", +) +@click.option( + "--exclude", + multiple=True, + help="File patterns to exclude", +) +@click.pass_context +def scan( + ctx: click.Context, + path: str, + output: str, + fail_level: str, + include: tuple, + exclude: tuple, +) -> None: + """Scan code for security vulnerabilities.""" + config = ConfigLoader.load(ctx.obj["config"]) + scanner = CodeScanner( + ollama_url=ctx.obj["ollama_url"], + model=ctx.obj["model"], + timeout=ctx.obj["timeout"], + config=config, + ) + findings = scanner.scan(path, include=list(include), exclude=list(exclude)) + OutputFormatter.print(findings, output_format=output) + if fail_level != "none": + severity_levels = ["low", "medium", "high", "critical"] + fail_index = severity_levels.index(fail_level) + for finding in findings: + if finding.severity.value in severity_levels[: fail_index + 1]: + sys.exit(1) + + +@main.command() +@click.option( + "--path", + default=".", + help="Path to git repository", + type=click.Path(exists=True, file_okay=False, dir_okay=True), +) +@click.option( + "--force", + is_flag=True, + help="Force reinstall existing hook", +) +def install_hook(path: str, force: bool) -> None: + """Install git pre-commit hook.""" + manager = HookManager(path) + manager.install(force=force) + click.echo("Pre-commit hook installed successfully") + + +@main.command() +@click.argument( + "paths", + nargs=-1, + type=click.Path(exists=True), +) +@click.option( + "--output", + type=click.Choice(["text", "json"]), + default="text", + help="Output format", +) +@click.pass_context +def check( + ctx: click.Context, + paths: tuple[str, ...], + output: str, +) -> None: + """Check specific files for security issues.""" + if not paths: + click.echo("No files specified", err=True) + sys.exit(1) + config = ConfigLoader.load(ctx.obj["config"]) + scanner = CodeScanner( + ollama_url=ctx.obj["ollama_url"], + model=ctx.obj["model"], + timeout=ctx.obj["timeout"], + config=config, + ) + findings = scanner.check_files(list(paths)) + OutputFormatter.print(findings, output_format=output) + + +@main.command() +def version() -> None: + """Show version.""" + from codeguard import __version__ + click.echo(f"CodeGuard-CLI v{__version__}") + + +@main.command() +@click.option( + "--ollama-url", + default=None, + help="Ollama server URL", +) +@click.option( + "--model", + default=None, + help="Ollama model to use", +) +@click.option( + "--timeout", + default=None, + help="Request timeout in seconds", + type=int, +) +def status(ollama_url: Optional[str], model: Optional[str], timeout: Optional[str]) -> None: + """Check CodeGuard and Ollama status.""" + from codeguard.llm.client import OllamaClient + + url = ollama_url or "http://localhost:11434" + client = OllamaClient(url) + + click.echo("Checking Ollama connection...") + if client.health_check(): + click.secho("Ollama is running", fg="green") + else: + click.secho("Ollama is not accessible", fg="red") + return + + click.echo(f"Available models: {', '.join(client.list_models())}")