Add validators module
This commit is contained in:
124
src/validators/best_practices.py
Normal file
124
src/validators/best_practices.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"""Best practices validation module."""
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from ..models import Rule, Finding
|
||||||
|
|
||||||
|
|
||||||
|
class BestPracticesValidator:
|
||||||
|
"""Validates shell scripts for best practices."""
|
||||||
|
|
||||||
|
def __init__(self, custom_rules: Optional[List[Rule]] = None):
|
||||||
|
"""Initialize the best practices validator with optional custom rules."""
|
||||||
|
self.rules = self.DEFAULT_PATTERNS.copy()
|
||||||
|
if custom_rules:
|
||||||
|
self.rules.extend(custom_rules)
|
||||||
|
|
||||||
|
DEFAULT_PATTERNS = [
|
||||||
|
Rule(
|
||||||
|
id="BEST001",
|
||||||
|
name="Missing set -e",
|
||||||
|
pattern=r"^[^#!]*set\s+-e",
|
||||||
|
severity="medium",
|
||||||
|
message="Script missing 'set -e' for automatic error handling",
|
||||||
|
suggestion="Add 'set -e' at the beginning of the script to exit on errors",
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="BEST002",
|
||||||
|
name="Missing set -u",
|
||||||
|
pattern=r"set\s+-[aeu]",
|
||||||
|
severity="medium",
|
||||||
|
message="Script missing 'set -u' to catch undefined variables",
|
||||||
|
suggestion="Add 'set -u' to catch usage of undefined variables",
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="BEST003",
|
||||||
|
name="Missing set -o pipefail",
|
||||||
|
pattern=r"set\s+-o\s+pipefail",
|
||||||
|
severity="medium",
|
||||||
|
message="Script missing 'set -o pipefail' for proper pipeline error handling",
|
||||||
|
suggestion="Add 'set -o pipefail' to capture errors in pipelines",
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="BEST004",
|
||||||
|
name="No error handling for command",
|
||||||
|
pattern=r"^(?!.*(\|\||\&\&|;|exit\s+\d+)).*$",
|
||||||
|
severity="medium",
|
||||||
|
message="Command without error handling",
|
||||||
|
suggestion="Add error handling with '|| exit 1' or '&&' operator",
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="BEST005",
|
||||||
|
name="Dangerous cp without -i",
|
||||||
|
pattern=r"cp\s+(?!.*-i)(?!.*--interactive)\s+\S+\s+\S+",
|
||||||
|
severity="medium",
|
||||||
|
message="cp without -i can overwrite files without warning",
|
||||||
|
suggestion="Use 'cp -i' for interactive mode or 'cp -n' for no-clobber",
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="BEST006",
|
||||||
|
name="Dangerous mv without -i",
|
||||||
|
pattern=r"mv\s+(?!.*-i)(?!.*--interactive)\s+\S+\s+\S+",
|
||||||
|
severity="medium",
|
||||||
|
message="mv without -i can overwrite files without warning",
|
||||||
|
suggestion="Use 'mv -i' for interactive mode or 'mv -n' for no-clobber",
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="STYLE001",
|
||||||
|
name="Use of deprecated backticks",
|
||||||
|
pattern=r"`[^`]+`",
|
||||||
|
severity="low",
|
||||||
|
message="Backticks for command substitution are deprecated",
|
||||||
|
suggestion="Use $() instead of backticks for command substitution",
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="STYLE002",
|
||||||
|
name="Long line detected",
|
||||||
|
pattern=r"^.{121,}$",
|
||||||
|
severity="low",
|
||||||
|
message="Line exceeds 120 characters",
|
||||||
|
suggestion="Consider breaking long lines for better readability",
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="STYLE003",
|
||||||
|
name="Use of cat with grep",
|
||||||
|
pattern=r"cat\s+\S+\s*\|\s*grep",
|
||||||
|
severity="low",
|
||||||
|
message="Useless use of cat",
|
||||||
|
suggestion="Use grep directly on the file instead of piping cat output",
|
||||||
|
),
|
||||||
|
Rule(
|
||||||
|
id="STYLE004",
|
||||||
|
name="Missing shebang",
|
||||||
|
pattern=r"^[^#!].*$",
|
||||||
|
severity="low",
|
||||||
|
message="Script missing shebang line",
|
||||||
|
suggestion="Add appropriate shebang like #!/bin/bash or #!/usr/bin/env bash",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate(self, content: str, line_number: Optional[int] = None) -> List[Finding]:
|
||||||
|
"""Validate content for best practices."""
|
||||||
|
findings = []
|
||||||
|
for rule in self.rules:
|
||||||
|
if not rule.enabled:
|
||||||
|
continue
|
||||||
|
matches = rule._compiled_pattern.finditer(content) if rule._compiled_pattern else []
|
||||||
|
for match in matches:
|
||||||
|
finding = Finding.from_match(rule, match, line_number, content)
|
||||||
|
findings.append(finding)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
def validate_lines(self, lines: List[str]) -> List[Finding]:
|
||||||
|
"""Validate multiple lines, tracking line numbers."""
|
||||||
|
findings = []
|
||||||
|
for line_number, line in enumerate(lines, start=1):
|
||||||
|
line_findings = self.validate(line, line_number)
|
||||||
|
for finding in line_findings:
|
||||||
|
finding.context = line
|
||||||
|
findings.extend(line_findings)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
def check(self, command: str) -> List[Finding]:
|
||||||
|
"""Check a single command for best practices issues."""
|
||||||
|
return self.validate(command)
|
||||||
Reference in New Issue
Block a user