Add config_auditor module files: cli, discovery, parsers
This commit is contained in:
221
config_auditor/cli.py
Normal file
221
config_auditor/cli.py
Normal file
@@ -0,0 +1,221 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import click
|
||||
|
||||
from config_auditor.discovery import ConfigDiscovery
|
||||
from config_auditor.parsers import ParserFactory
|
||||
from config_auditor.rules import RuleRegistry, Issue
|
||||
from config_auditor.report import ReportGenerator
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option("--path", "-p", default=".", help="Path to scan")
|
||||
@click.option("--format", "-f", type=click.Choice(["json", "yaml", "text"]), default="text", help="Output format")
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
||||
@click.pass_context
|
||||
def cli(ctx: click.Context, path: str, format: str, verbose: bool):
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["path"] = Path(path)
|
||||
ctx.obj["format"] = format
|
||||
ctx.obj["verbose"] = verbose
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def scan(ctx: click.Context):
|
||||
"""Scan a directory for configuration files."""
|
||||
path = ctx.obj["path"]
|
||||
verbose = ctx.obj.get("verbose", False)
|
||||
|
||||
if not path.exists():
|
||||
click.echo(f"Error: Path does not exist: {path}", err=True)
|
||||
sys.exit(2)
|
||||
|
||||
if not path.is_dir():
|
||||
click.echo(f"Error: Path is not a directory: {path}", err=True)
|
||||
sys.exit(2)
|
||||
|
||||
discovery = ConfigDiscovery(max_depth=3)
|
||||
config_files = discovery.discover(path)
|
||||
|
||||
if verbose:
|
||||
for cf in config_files:
|
||||
click.echo(f"Found: {cf.path} ({cf.format})")
|
||||
|
||||
click.echo(f"Found {len(config_files)} configuration files")
|
||||
|
||||
if ctx.obj["format"] == "json":
|
||||
import json
|
||||
result = [{"path": str(cf.path), "format": cf.format} for cf in config_files]
|
||||
click.echo(json.dumps(result, indent=2))
|
||||
elif ctx.obj["format"] == "yaml":
|
||||
import yaml
|
||||
result = [{"path": str(cf.path), "format": cf.format} for cf in config_files]
|
||||
click.echo(yaml.dump(result))
|
||||
else:
|
||||
for cf in config_files:
|
||||
click.echo(f" {cf.path} ({cf.format})")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--format", "-f", type=click.Choice(["json", "yaml", "text"]), default="text", help="Output format")
|
||||
@click.pass_context
|
||||
def audit(ctx: click.Context, format: str):
|
||||
"""Audit configuration files for issues."""
|
||||
path = ctx.obj["path"]
|
||||
format_output = format
|
||||
verbose = ctx.obj.get("verbose", False)
|
||||
|
||||
if not path.exists():
|
||||
click.echo(f"Error: Path does not exist: {path}", err=True)
|
||||
sys.exit(2)
|
||||
|
||||
discovery = ConfigDiscovery(max_depth=3)
|
||||
config_files = discovery.discover(path)
|
||||
|
||||
if not config_files:
|
||||
click.echo("No configuration files found", err=True)
|
||||
sys.exit(3)
|
||||
|
||||
parser_factory = ParserFactory()
|
||||
rule_registry = RuleRegistry()
|
||||
issues: list[Issue] = []
|
||||
|
||||
for cf in config_files:
|
||||
try:
|
||||
content = cf.path.read_text()
|
||||
data = parser_factory.parse(cf.format, content)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
file_issues = rule_registry.evaluate(cf.format, data, cf.path)
|
||||
issues.extend(file_issues)
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
click.echo(f"Warning: Failed to parse {cf.path}: {e}")
|
||||
|
||||
report_gen = ReportGenerator()
|
||||
if format_output == "json":
|
||||
output = report_gen.to_json(issues)
|
||||
click.echo(output)
|
||||
elif format_output == "yaml":
|
||||
output = report_gen.to_yaml(issues)
|
||||
click.echo(output)
|
||||
else:
|
||||
report_gen.to_text(issues, click.echo)
|
||||
|
||||
critical_count = sum(1 for i in issues if i.severity == "critical")
|
||||
if critical_count > 0:
|
||||
sys.exit(4)
|
||||
elif issues:
|
||||
sys.exit(4)
|
||||
return 0
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--dry-run", is_flag=True, default=False, help="Preview changes without applying")
|
||||
@click.option("--force", is_flag=True, default=False, help="Skip confirmation")
|
||||
@click.pass_context
|
||||
def fix(ctx: click.Context, dry_run: bool, force: bool):
|
||||
"""Automatically fix detected issues."""
|
||||
path = ctx.obj["path"]
|
||||
verbose = ctx.obj.get("verbose", False)
|
||||
|
||||
if not path.exists():
|
||||
click.echo(f"Error: Path does not exist: {path}", err=True)
|
||||
sys.exit(2)
|
||||
|
||||
discovery = ConfigDiscovery(max_depth=3)
|
||||
config_files = discovery.discover(path)
|
||||
|
||||
if not config_files:
|
||||
click.echo("No configuration files found", err=True)
|
||||
sys.exit(3)
|
||||
|
||||
from config_auditor.fixes import Fixer
|
||||
fixer = Fixer(dry_run=dry_run, force=force)
|
||||
|
||||
fixed_count = 0
|
||||
for cf in config_files:
|
||||
try:
|
||||
content = cf.path.read_text()
|
||||
fixes_applied = fixer.fix_config(cf.path, cf.format, content)
|
||||
if fixes_applied:
|
||||
fixed_count += fixes_applied
|
||||
if verbose:
|
||||
click.echo(f"Applied {fixes_applied} fixes to {cf.path}")
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
click.echo(f"Warning: Failed to fix {cf.path}: {e}")
|
||||
|
||||
if fixed_count > 0:
|
||||
click.echo(f"Applied {fixed_count} fixes")
|
||||
return 0
|
||||
else:
|
||||
click.echo("No fixes applied")
|
||||
return 0
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--template", "-t", type=click.Choice(["node", "python", "typescript"]), help="Template type")
|
||||
@click.pass_context
|
||||
def generate(ctx: click.Context, template: Optional[str]):
|
||||
"""Generate optimal configurations."""
|
||||
path = ctx.obj["path"]
|
||||
|
||||
if not path.exists():
|
||||
click.echo(f"Error: Path does not exist: {path}", err=True)
|
||||
sys.exit(2)
|
||||
|
||||
from config_auditor.generate import ConfigGenerator
|
||||
|
||||
generator = ConfigGenerator()
|
||||
|
||||
if template:
|
||||
config = generator.generate_from_template(template, path)
|
||||
output = ctx.obj["format"]
|
||||
if output == "json":
|
||||
import json
|
||||
click.echo(json.dumps(config, indent=2))
|
||||
elif output == "yaml":
|
||||
import yaml
|
||||
click.echo(yaml.dump(config))
|
||||
else:
|
||||
import json
|
||||
click.echo(json.dumps(config, indent=2))
|
||||
else:
|
||||
project_type = generator.detect_project_type(path)
|
||||
if project_type:
|
||||
config = generator.generate_from_template(project_type, path)
|
||||
import json
|
||||
click.echo(json.dumps(config, indent=2))
|
||||
else:
|
||||
click.echo("Could not detect project type. Use --template to specify.", err=True)
|
||||
sys.exit(2)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def config(ctx: click Context):
|
||||
"""Show current configuration."""
|
||||
import yaml
|
||||
|
||||
config_path = Path("config.yaml")
|
||||
if config_path.exists():
|
||||
content = config_path.read_text()
|
||||
click.echo(content)
|
||||
else:
|
||||
click.echo("No config.yaml found in current directory")
|
||||
|
||||
|
||||
def main():
|
||||
cli(obj={})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user