Add CLI module
This commit is contained in:
129
src/cli.py
129
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()
|
||||
|
||||
Reference in New Issue
Block a user