diff --git a/src/cli.py b/src/cli.py index 1bdebd6..658874b 100644 --- a/src/cli.py +++ b/src/cli.py @@ -1,30 +1,17 @@ -"""CLI interface for Shell Safe Validator.""" - -import json -import sys +"""CLI interface.""" +import json, sys from pathlib import Path from typing import List, Optional - import click - from .config import load_rules from .models import Severity from .validators import PatternValidator, SecurityValidator, BestPracticesValidator - @click.group() def main(): - """Shell Safe Validator - Validate shell scripts for safety issues.""" pass - -def validate_content( - content: str, - config_path: Optional[str] = None, - min_severity: str = "low", - output_format: str = "text", -) -> List[dict]: - """Validate shell content and return findings.""" +def validate_content(content, config_path=None, min_severity="low", output_format="text"): min_severity_level = Severity.from_string(min_severity) lines = content.split("\n") rules = load_rules(config_path) @@ -41,9 +28,7 @@ def validate_content( all_findings.append(finding.to_dict()) return all_findings - -def print_findings(findings: List[dict], output_format: str = "text") -> None: - """Print findings in the specified format.""" +def print_findings(findings, output_format="text"): if output_format == "json": click.echo(json.dumps(findings, indent=2)) elif output_format == "compact": @@ -63,72 +48,33 @@ def print_findings(findings: List[dict], output_format: str = "text") -> None: if finding["suggestion"]: click.echo(f" Suggestion: {finding['suggestion']}") - @main.command() @click.argument("file", type=click.Path(exists=True)) -@click.option("--config", "-c", help="Path to custom rules YAML file") -@click.option( - "--severity", - "-s", - default="low", - help="Minimum severity level (low, medium, high, critical)", -) -@click.option( - "--output-format", - "-o", - type=click.Choice(["text", "json", "compact"]), - default="text", - help="Output format", -) -def validate(file: str, config: Optional[str], severity: str, output_format: str): - """Validate a shell script file for safety issues.""" - try: - file_path = Path(file) - with open(file_path, "r") as f: - content = f.read() - click.echo(f"Validating {file}...\n") - findings = validate_content(content, config, severity, output_format) - if findings: - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0} - for finding in findings: - severity_counts[finding["severity"]] += 1 - print_findings(findings, output_format) - click.echo( - f"\nValidation complete: {len(findings)} issue(s) found " - f"({severity_counts['critical']} critical, " - f"{severity_counts['high']} high, " - f"{severity_counts['medium']} medium, " - f"{severity_counts['low']} low)" - ) - sys.exit(1) - else: - click.echo("No issues found. Script looks safe!") - sys.exit(0) - except FileNotFoundError: - click.echo(f"Error: File not found: {file}", err=True) +@click.option("--config", "-c") +@click.option("--severity", "-s", default="low") +@click.option("--output-format", "-o", type=click.Choice(["text", "json", "compact"]), default="text") +def validate(file, config, severity, output_format): + file_path = Path(file) + with open(file_path, "r") as f: + content = f.read() + click.echo(f"Validating {file}...\n") + findings = validate_content(content, config, severity, output_format) + if findings: + severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0} + for finding in findings: + severity_counts[finding["severity"]] += 1 + print_findings(findings, output_format) + click.echo(f"\nValidation complete: {len(findings)} issue(s) found ({severity_counts['critical']} critical, {severity_counts['high']} high, {severity_counts['medium']} medium, {severity_counts['low']} low)") sys.exit(1) - except PermissionError: - click.echo(f"Error: Permission denied: {file}", err=True) - sys.exit(1) - + else: + click.echo("No issues found. Script looks safe!") + sys.exit(0) @main.command() @click.argument("command", nargs=-1) -@click.option( - "--severity", - "-s", - default="low", - help="Minimum severity level (low, medium, high, critical)", -) -@click.option( - "--output-format", - "-o", - type=click.Choice(["text", "json", "compact"]), - default="text", - help="Output format", -) -def check(command: str, severity: str, output_format: str): - """Check a single shell command for safety issues.""" +@click.option("--severity", "-s", default="low") +@click.option("--output-format", "-o", type=click.Choice(["text", "json", "compact"]), default="text") +def check(command, severity, output_format): if not command: click.echo("Error: No command provided", err=True) sys.exit(2) @@ -142,40 +88,24 @@ def check(command: str, severity: str, output_format: str): click.echo("No issues found. Command looks safe!") sys.exit(0) - @main.command() @click.argument("command", nargs=-1) -@click.option("--dry-run", is_flag=True, help="Show what would be executed without running") -@click.option("--force", is_flag=True, help="Skip confirmation prompt") -def safe_exec(command: str, dry_run: bool, force: bool): - """Execute a command safely with validation and confirmation.""" +@click.option("--dry-run", is_flag=True) +@click.option("--force", is_flag=True) +def safe_exec(command, dry_run, force): if not command: click.echo("Error: No command provided", err=True) sys.exit(2) command_str = " ".join(command) findings = validate_content(command_str, None, "low", "text") - critical_high_findings = [ - f for f in findings if f["severity"] in ["critical", "high"] - ] + critical_high_findings = [f for f in findings if f["severity"] in ["critical", "high"]] if dry_run: click.echo(f"[DRY RUN] Would execute: {command_str}") - if findings: - click.echo(f" Issues found: {len(findings)}") - for f in findings[:5]: - click.echo(f" - [{f['severity'].upper()}] {f['message']}") sys.exit(0) if critical_high_findings: click.echo("ERROR: Command contains critical/high severity issues:", err=True) - for f in critical_high_findings: - click.echo(f" [{f['severity'].upper()}] {f['message']}", err=True) sys.exit(1) if not force: - if findings: - click.echo(f"Warning: Command has {len(findings)} issue(s)") - for f in findings[:3]: - click.echo(f" [{f['severity'].upper()}] {f['message']}") - if len(findings) > 3: - click.echo(f" ... and {len(findings) - 3} more") confirm = click.confirm(f"\nExecute: {command_str}") if not confirm: click.echo("Cancelled.") @@ -192,6 +122,5 @@ def safe_exec(command: str, dry_run: bool, force: bool): click.echo(f"Error executing command: {e}", err=True) sys.exit(1) - if __name__ == "__main__": main()