src/pqc_lint/cli.py
2.7 KB · 101 lines · python Raw
1 """Command-line interface for pqc-lint."""
2
3 from __future__ import annotations
4
5 import sys
6
7 import click
8
9 from pqc_lint import __version__
10 from pqc_lint.findings import Severity
11 from pqc_lint.reporters import REPORTERS
12 from pqc_lint.rules import RULES
13 from pqc_lint.scanner import DEFAULT_EXCLUDES, Scanner
14
15
16 @click.group()
17 @click.version_option(version=__version__, prog_name="pqc-lint")
18 def main() -> None:
19 """PQC Lint - find classical cryptography and suggest PQC replacements."""
20
21
22 @main.command()
23 @click.argument("path", type=click.Path(exists=True), default=".")
24 @click.option(
25 "--format", "output_format",
26 type=click.Choice(["text", "json", "sarif", "github"]),
27 default="text",
28 help="Output format.",
29 )
30 @click.option(
31 "--output", "-o", "output_file",
32 type=click.Path(),
33 default=None,
34 help="Write output to file instead of stdout.",
35 )
36 @click.option(
37 "--fail-on",
38 type=click.Choice(["critical", "high", "medium", "low", "info"]),
39 default="high",
40 help="Exit with non-zero status if any finding is >= this severity.",
41 )
42 @click.option(
43 "--languages", "-l",
44 default="",
45 help="Comma-separated languages to scan (python,javascript,go,rust,java,c). Empty=all.",
46 )
47 @click.option(
48 "--exclude",
49 default="",
50 help="Comma-separated glob patterns to exclude (in addition to defaults).",
51 )
52 def scan(
53 path: str,
54 output_format: str,
55 output_file: str | None,
56 fail_on: str,
57 languages: str,
58 exclude: str,
59 ) -> None:
60 """Scan PATH for classical cryptography usage."""
61 lang_tuple = tuple(
62 s.strip().lower() for s in languages.split(",") if s.strip()
63 )
64 extra_excludes = tuple(s.strip() for s in exclude.split(",") if s.strip())
65 excludes = DEFAULT_EXCLUDES + extra_excludes
66
67 scanner = Scanner(languages=lang_tuple, excludes=excludes)
68 report = scanner.scan_path(path)
69
70 reporter_cls = REPORTERS[output_format]
71 output = reporter_cls().render(report)
72
73 if output_file:
74 with open(output_file, "w", encoding="utf-8") as f:
75 f.write(output)
76 click.echo(f"Wrote {output_format} report to {output_file}")
77 else:
78 click.echo(output, nl=False)
79
80 # exit code based on fail-on
81 threshold = Severity.from_str(fail_on)
82 if report.has_failing(threshold):
83 sys.exit(1)
84
85
86 @main.command()
87 def rules() -> None:
88 """List all rules."""
89 click.echo(f"pqc-lint rules ({len(RULES)})\n")
90 for r in RULES:
91 click.echo(f" {r.id} [{r.severity.value.upper():8}] {r.title}")
92 click.echo(f" primitive: {r.classical_primitive}")
93 click.echo(f" languages: {', '.join(r.languages)}")
94 if r.cwe:
95 click.echo(f" cwe: {r.cwe}")
96 click.echo()
97
98
99 if __name__ == "__main__":
100 main()
101