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