Skip to content

Commit 9b37dd6

Browse files
feat: Improve CLI output and warning messages
* feat: Add progress indicator and improve CLI usability This commit introduces several improvements to the user experience of the privlog CLI. - A progress indicator is now displayed by default during the scanning process to provide better feedback and prevent the appearance of a hanging process. - The --verbose flag now controls the verbosity of the underlying semgrep scanner, providing more detailed output for debugging. - The check subcommand has been merged into the main privlog command to simplify usage and align with the documentation. - A --version flag has been added to display the current version of the tool. - A CHANGELOG.md has been created to document changes for future releases. * chore: Bump version to 0.2.2 * feat: Improve AST-based warnings for sensitive data The AST checker now provides more informative warnings by including the name of the sensitive identifier that was detected. Previously, the warning was a generic message: "Sensitive identifier passed to log. Hash/pseudonymize or omit." Now, it will include the variable name, for example: 'Sensitive identifier "user_email" passed to log. Hash, pseudonymize, or omit before logging.' This makes it easier for developers to quickly identify and remediate the issue. * fix: Add python version classifiers to pyproject.toml Adds classifiers for Python 3.9 through 3.12 to resolve the 'python missing' badge on PyPI and in the README. * docs: Update examples to reflect new output style The examples in the README and the GitHub Pages index.html have been updated to show the new, more informative warning message that includes the name of the sensitive identifier.
1 parent 4897a64 commit 9b37dd6

7 files changed

Lines changed: 107 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.2.2] - 2026-03-11
9+
10+
### Added
11+
- Progress indicator during the AST scan when using the `--verbose` flag, showing which file is being scanned.
12+
- `--version` flag to display the current version of the tool.
13+
- Verbose output for scanning stages, making it clear when `semgrep` and `AST` scans are running.
14+
15+
### Changed
16+
- Improved AST check warnings to include the name of the sensitive identifier found, making it easier to locate and fix issues. For example, the warning for `PL2101` will now be `Sensitive identifier "user_email" passed to log...`.
17+
- The progress indicator and scanning stage messages are now shown by default to provide better feedback during scans. The `--verbose` flag now only controls the verbosity of the underlying `semgrep` tool.
18+
- The `check` subcommand has been merged into the main `privlog` command. This simplifies the command-line usage from `privlog check` to `privlog` and aligns the tool's behavior with the `README.md` documentation.

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def reauthenticate_user(user_email):
2929
Running `privlog .` will produce the following error:
3030

3131
```
32-
app/auth.py:5:5 [ERROR] PL2101 Sensitive identifier passed to log. Hash/pseudonymize or omit.
32+
app/auth.py:5:5 [ERROR] PL2101 Sensitive identifier "user_email" passed to log. Hash, pseudonymize, or omit before logging.
3333
```
3434

3535
## Features
@@ -99,6 +99,11 @@ privlog -w .
9999

100100
This will display all findings, color-coded by severity, but will still only fail the build if `ERROR`s are present.
101101

102+
### Other Flags
103+
104+
- `--verbose` / `-v`: Enables verbose output from the underlying `semgrep` scanner. This is useful for debugging rules and understanding which files `semgrep` is scanning or skipping. By default, `privlog` always shows a high-level progress indicator; this flag provides much more detail about the `semgrep` scanning phase.
105+
- `--version`: Display the installed version of `privlog`.
106+
102107
### Configuring Custom Wrappers
103108

104109
You can teach `privlog` to recognize your own custom logging functions. In your project's `pyproject.toml` file, add a `[tool.privlog.custom_wrappers]` section.
@@ -120,7 +125,7 @@ log_event = { details = "WARNING" }
120125

121126
## Status
122127

123-
Privlog is currently in early development (v0.2.1).
128+
Privlog is currently in early development (v0.2.2).
124129
Feedback and contributions are welcome.
125130

126131
---

docs/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ <h2>What it checks</h2>
157157

158158
<section class="card">
159159
<h2>Example</h2>
160-
<pre><code>app.py:12:9 [ERROR] PL2101 Sensitive identifier passed to log. Hash/pseudonymize or omit.</code></pre>
160+
<pre><code>app.py:12:9 [ERROR] PL2101 Sensitive identifier "user_email" passed to log. Hash, pseudonymize, or omit before logging.</code></pre>
161161
</section>
162162

163163
<section class="card">

privlog/ast_checks.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ast
44
from dataclasses import dataclass
55
from pathlib import Path
6+
import typer
67

78
# A forward declaration is needed for the type hint in this file
89
class PrivlogConfig: ...
@@ -92,10 +93,10 @@ def _is_safe_wrapper(expr: ast.AST) -> bool:
9293
)
9394

9495

95-
def _get_expr_sensitivity(expr: ast.AST) -> str | None:
96+
def _get_expr_sensitivity(expr: ast.AST) -> tuple[str, str] | None:
9697
"""
9798
Checks an expression for sensitive names.
98-
Returns severity ('ERROR', 'WARNING') or None if not sensitive.
99+
Returns a tuple of (severity, sensitive_name) or None if not sensitive.
99100
"""
100101
# Allow slicing, which is a form of truncation
101102
if isinstance(expr, ast.Subscript):
@@ -105,17 +106,19 @@ def _get_expr_sensitivity(expr: ast.AST) -> str | None:
105106
if _is_safe_wrapper(expr):
106107
return None
107108

108-
# Allow known-safe name variables
109109
names = _names_in_expr(expr)
110+
# Allow known-safe name variables
110111
if any(n in SAFE_NAMES for n in names):
111112
return None
112113

113114
# Flag if any sensitive name appears
114-
if any(n.lower() in HIGH_CONFIDENCE_SENSITIVE_NAMES for n in names):
115-
return "ERROR"
116-
117-
if any(n.lower() in WARNING_SENSITIVE_NAMES for n in names):
118-
return "WARNING"
115+
for name in names:
116+
if name.lower() in HIGH_CONFIDENCE_SENSITIVE_NAMES:
117+
return "ERROR", name
118+
119+
for name in names:
120+
if name.lower() in WARNING_SENSITIVE_NAMES:
121+
return "WARNING", name
119122

120123
return None
121124

@@ -177,11 +180,13 @@ def visit_Call(self, node: ast.Call) -> None:
177180
args_to_check.extend(node.args[1:])
178181

179182
for arg in args_to_check:
180-
severity = _get_expr_sensitivity(arg)
181-
if severity:
183+
sensitivity = _get_expr_sensitivity(arg)
184+
if sensitivity:
185+
severity, name = sensitivity
182186
code = "PL2301" if is_print else "PL2101"
183187
call_type = "print()" if is_print else "log"
184-
self._add_finding(node, code, f"Sensitive identifier passed to {call_type}. Hash/pseudonymize or omit.", severity)
188+
message = f'Sensitive identifier "{name}" passed to {call_type}. Hash, pseudonymize, or omit before logging.'
189+
self._add_finding(node, code, message, severity)
185190
break
186191

187192
# Check 2: Heuristic checks for dictionary/object logging
@@ -223,15 +228,31 @@ def visit_Call(self, node: ast.Call) -> None:
223228
".git",
224229
}
225230

231+
def _collect_python_files(root: Path) -> list[Path]:
232+
"""Recursively finds all Python files in a directory, respecting ignores."""
233+
all_files = []
234+
for py in root.rglob("*.py"):
235+
if any(part in DEFAULT_IGNORE_DIRS for part in py.parts):
236+
continue
237+
all_files.append(py)
238+
return all_files
239+
240+
226241
def run_ast_checks(root: Path, config: PrivlogConfig) -> list[AstFinding]:
242+
"""
243+
Scans for sensitive data in Python files using AST checks.
244+
"""
227245
findings: list[AstFinding] = []
228246

229-
all_python_files = root.rglob("*.py")
247+
files_to_scan = _collect_python_files(root)
248+
total_files = len(files_to_scan)
230249

231-
for py in all_python_files:
232-
# Check if any parent directory component is in the ignore list
233-
if any(part in DEFAULT_IGNORE_DIRS for part in py.parts):
234-
continue
250+
typer.secho(f"Running AST checks on {total_files} Python files...", fg=typer.colors.BLUE)
251+
252+
for i, py in enumerate(files_to_scan):
253+
# \r to return to start of line, \x1b[K to clear line
254+
progress_msg = f"Scanning [{i + 1}/{total_files}] {str(py)}"
255+
typer.secho(f"\r\x1b[K{progress_msg}", fg=typer.colors.WHITE, nl=False)
235256

236257
try:
237258
text = py.read_text(encoding="utf-8", errors="replace")
@@ -240,6 +261,12 @@ def run_ast_checks(root: Path, config: PrivlogConfig) -> list[AstFinding]:
240261
v.visit(tree)
241262
findings.extend(v.findings)
242263
except SyntaxError:
243-
# Ignore files that aren't parseable in current context
244264
continue
265+
except Exception:
266+
# Fallback for other file-read errors
267+
continue
268+
269+
# Clear the line and print a final message
270+
typer.secho("\r\x1b[KAST checks complete.", fg=typer.colors.BLUE)
271+
245272
return findings

privlog/cli.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,41 @@
11
import typer
22
from pathlib import Path
3+
from typing import Optional
4+
import importlib.metadata
35
from privlog.runner import run_analysis
46
from privlog.formatter import print_findings
57

6-
app = typer.Typer(add_completion=False, no_args_is_help=True)
8+
__version__ = "0.2.2"
79

8-
@app.command()
9-
def check(
10-
path: Path = typer.Argument(Path("."), exists=True),
10+
def version_callback(value: bool):
11+
if value:
12+
typer.echo(f"privlog version {__version__}")
13+
raise typer.Exit()
14+
15+
app = typer.Typer(add_completion=False, no_args_is_help=False)
16+
17+
@app.callback(invoke_without_command=True)
18+
def main(
19+
ctx: typer.Context,
20+
path: Path = typer.Argument(Path("."), exists=True, help="Path to scan."),
1121
config: Path = typer.Option(None, "--config", "-c", help="Optional privlog config YAML"),
1222
rules: Path = typer.Option(None, "--rules", "-r", help="Override rules file/folder"),
1323
json: bool = typer.Option(False, "--json", help="Output JSON (raw Semgrep)"),
14-
verbose: bool = typer.Option(False, "--verbose", help="Verbose output"),
24+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
1525
warnings: bool = typer.Option(
1626
False, "--warnings", "-w", help="Show WARNING findings in addition to ERRORs."
1727
),
28+
version: Optional[bool] = typer.Option(
29+
None, "--version", callback=version_callback, is_eager=True, help="Show the version and exit."
30+
),
1831
):
1932
"""
2033
Run privlog checks on a codebase path.
2134
Exits non-zero if ERROR violations are found.
2235
"""
36+
if ctx.invoked_subcommand is not None:
37+
return # Should not happen with a single command, but good practice
38+
2339
result = run_analysis(path=path, config=config, rules=rules, verbose=verbose)
2440

2541
if json:
@@ -29,13 +45,10 @@ def check(
2945
if not warnings:
3046
findings_to_print = [f for f in result.findings if f.severity == "ERROR"]
3147

32-
# If there are findings, but none to print (because they are warnings),
33-
# give a specific success message.
3448
if result.findings and not findings_to_print:
3549
typer.secho("✅ privlog passed. No errors found.", fg=typer.colors.GREEN)
3650
typer.secho(" (Warnings were found. Run with -w to show them)")
3751
else:
3852
print_findings(findings_to_print)
3953

40-
# The exit code from run_analysis is now based on ERROR-level findings only
4154
raise typer.Exit(code=result.exit_code)

privlog/runner.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from dataclasses import dataclass, field
66
from pathlib import Path
77
from importlib.resources import files
8+
import typer
89

910
# Use tomllib for Python 3.11+, otherwise tomli
1011
try:
@@ -84,7 +85,8 @@ def _run_semgrep(path: Path, config: Path | None, rules: Path | None, verbose: b
8485
if verbose:
8586
cmd.insert(1, "--verbose")
8687

87-
proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
88+
stderr_setting = None if verbose else subprocess.PIPE
89+
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=stderr_setting, text=True, encoding="utf-8")
8890
raw = proc.stdout.strip() if proc.stdout else ""
8991

9092
findings: list[Finding] = []
@@ -118,7 +120,12 @@ def run_analysis(path: Path, config: Path | None, rules: Path | None, verbose: b
118120
# Load config from the target path
119121
privlog_config = _load_config(path)
120122

123+
typer.secho("Running Semgrep scan...", fg=typer.colors.BLUE)
124+
121125
semgrep_result = _run_semgrep(path, config, rules, verbose)
126+
127+
typer.secho("Semgrep scan complete.", fg=typer.colors.BLUE)
128+
122129
ast_findings = run_ast_checks(path, privlog_config)
123130

124131
# Convert AST findings to the common Finding type

pyproject.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "privlog"
7-
version = "0.2.1"
7+
version = "0.2.2"
88
description = "Privacy-aware logging hygiene linter for Python"
99
readme = "README.md"
1010
requires-python = ">=3.9"
1111
authors = [
1212
{name="Christopher Mariani"}
1313
]
1414
license = { text = "MIT" }
15+
classifiers = [
16+
"Programming Language :: Python :: 3",
17+
"Programming Language :: Python :: 3.9",
18+
"Programming Language :: Python :: 3.10",
19+
"Programming Language :: Python :: 3.11",
20+
"Programming Language :: Python :: 3.12",
21+
]
1522
dependencies = [
1623
"typer>=0.12.0",
1724
"PyYAML>=6.0.0",

0 commit comments

Comments
 (0)