| name | cli-contract |
| description | Design and review LogHunter Typer commands, options, Rich presentation, stdout/stderr behavior, no-color support, machine output, exit codes, and compatibility. |
| license | MIT |
| metadata | {"project":"loghunter-cli","interface":"cli"} |
CLI Contract
Principles
The CLI must be:
- Predictable.
- Scriptable.
- Accessible without color.
- Clear for expected user errors.
- Stable once a command is released.
- Thin over application logic.
Current root contract
loghunter --help
loghunter --version
python -m loghunter --help
These commands must remain fast and side-effect free.
Current analyze contract
loghunter analyze PATH --year YEAR
PATH is validated by the parser file API rather than Typer. --year is required and accepts
1..9999. Defaults are --threshold 5, --window 60, and --top 10, all with minimum 1. The two
boolean flags are --no-color and --fail-on-detection.
The handler performs three incremental public-API passes: parser statistics, analysis over a new
event stream, then detection over another new event stream. It must not use list, tuple,
itertools.tee, private parser APIs, or a unified fan-out. The local file is assumed to remain
static between passes.
Current top-ips contract
loghunter top-ips PATH --year YEAR
PATH is a plain positional Path argument and is validated by the parser file API. --year is
required and accepts 1..9999. --limit defaults to 10 and has a minimum of 1. The boolean option
is --no-color.
The handler performs exactly two incremental public-API passes: parser statistics, then analysis
over a new event stream. It renders the analyzer's existing deterministic source ranking and never
invokes detection. It must not use list, tuple, itertools.tee, private parser APIs, or a
unified fan-out. The local file is assumed to remain static between passes.
The command consumes AnalysisSummary.source_stats in analyzer-provided order; that order is
exactly failed logins DESC, last observed DESC, and normalized/canonical IP text ASC. Presentation
must reuse this order rather than recomputing it.
Current validate contract
loghunter validate PATH --year YEAR
PATH is a plain positional Path argument and is validated by collect_parse_stats. --year is
required and accepts 1..9999. The only other option is --no-color.
The handler performs exactly one incremental public-API pass, collect_parse_stats(path, year=year).
It renders the resulting ParseStats through render_validation_report and must not call
iter_events, parse_line, analyze_events, or detect_brute_force, materialize events, or
rescan the input. Full and partial files with at least one parsed line render on stdout and exit 0.
A non-empty unsupported-only file renders the complete 0.00% report before returning exit 4. Empty,
missing, non-file, unreadable, and invalid UTF-8 inputs render no report and return exit 3. Usage
errors return 2 and unexpected failures return 10; validate never returns exit 1 or 5.
Current export command
loghunter export PATH --year YEAR --format FORMAT --output OUTPUT
The export command accepts only the required --year, --format, and --output options plus
--threshold, --window, and --force. Format values are json and csv. It performs exactly
three incremental public-API passes: parser statistics, analysis, and detection. Successful runs
are silent on stdout and stderr and return 0 even when findings exist; export failures return 5 and
never use the finding-detected exit status. Existing regular destinations require --force;
directories, symlinks, non-regular entries, missing parents, same-file source destinations, and
write failures are rejected without modifying the source. Expected input and export path
diagnostics visibly escape terminal control characters.
Machine-export schema contract
Schema version, JSON keys, CSV columns, record types, row order, timestamp format, and serialization
details are public compatibility surfaces. The version is schema_version=1. JSON has exactly these
ordered top-level keys: schema_version, analysis, sources, usernames, and detection.
Its exact object key groups are:
analysis: total_lines, parsed_lines, ignored_lines, parser_coverage_percentage,
failed_logins, successful_logins, invalid_user_events, unique_source_addresses,
first_observed, last_observed
source: source_ip, failed_logins, successful_logins, invalid_user_events, total_events,
first_observed, last_observed
username: username, failed_logins, successful_logins, invalid_user_events, total_events,
first_observed, last_observed
detection: threshold, window_seconds, finding_count, findings
finding: severity, source_ip, failed_attempts, first_observed, last_observed, targeted_users
JSON is full-fidelity UTF-8/Unicode, preserves exact usernames, uses canonical IP text and lower-
case enum values, serializes naive timestamps as YYYY-MM-DDTHH:MM:SS, and represents optional
analysis extrema as null. targeted_users is an array. Source, username, and finding arrays
preserve deterministic finalized-domain order; exporters do not sort or recalculate results. JSON
contains no current-time, environment, hostname, source-path, package-version, or Rich metadata.
CSV is UTF-8 without a BOM and has exactly this 21-column header:
schema_version,record_type,total_lines,parsed_lines,ignored_lines,parser_coverage_percentage,failed_logins,successful_logins,invalid_user_events,unique_source_addresses,source_ip,username,total_events,first_observed,last_observed,threshold,window_seconds,finding_count,severity,failed_attempts,targeted_users
The only record types are analysis, source, username, detection, and finding. Rows are
one analysis row, source rows in supplied order, username rows in supplied order, one detection row,
then findings in supplied order. Every row has schema version 1; unrelated cells are blank. Coverage
has no percent sign, timestamps are YYYY-MM-DDTHH:MM:SS or blank when absent/not applicable, and
targeted_users is compact JSON. CSV preserves Unicode and prefixes only username cells whose
original value begins with =, +, -, @, tab, carriage return, or line feed with an apostrophe;
JSON remains lossless.
Thin command handlers
Command functions may:
- Receive Typer arguments and options.
- Validate simple option relationships.
- Call application functions.
- Catch expected application exceptions.
- Select terminal or machine output.
- Exit with a defined code.
Command functions must not:
- Contain regex patterns.
- Parse log lines.
- Implement sliding windows.
- Build summary statistics.
- Reimplement serializers.
Export handlers must pass finalized analysis and detection results to the exporter without
materializing event streams or rendering terminal output.
Output streams
Use stdout for:
- Successful human-readable results.
- Machine-readable JSON or CSV when explicitly requested on stdout.
- Version output.
Use stderr for:
- Expected errors.
- Warnings that must not corrupt machine-readable stdout.
- Diagnostic information in verbose or debug modes.
The current analyze report goes to stdout, including before an intentional exit 1. Expected and
generic unexpected diagnostics go to stderr. There is no verbose or debug option yet.
Rich output
- Keep Rich rendering inside
output.py or dedicated presentation helpers.
- Respect
--no-color.
- Do not use decorative terminal output in JSON or CSV.
- Avoid relying on color alone to convey severity.
- Include textual severity labels.
- Keep tables usable in narrow terminals where practical.
- Treat log-derived cells as literal
Text; never parse usernames or other untrusted values as
Rich markup.
- Export files must contain only machine-readable schema data, never Rich styling or decorative
terminal text.
Exit codes
Current contract:
| Code | Meaning |
|---|
| 0 | Success |
| 1 | Detection found when --fail-on-detection is active |
| 2 | Invalid CLI usage |
| 3 | Invalid or unreadable input |
| 4 | No supported records recognized |
| 5 | Export failure |
| 10 | Unexpected internal failure |
Do not use exit code 1 for ordinary findings unless the user explicitly requests automation failure behavior.
Exit 5 is reserved for export destination and write failures.
Option validation
Validate:
--threshold >= 1.
--window >= 1.
--top >= 1.
--limit >= 1.
- Year belongs to an accepted range.
- Export
--format is limited to json and csv.
- Export
--threshold and --window remain positive integers.
Prefer Typer validation for simple values and application errors for domain validation.
File behavior
- Input must exist and be a regular readable file.
- Output files must not overwrite existing data silently unless an explicit force policy is accepted.
- Parent directory errors must be clear.
- Source input must never be modified.
Compatibility
A public CLI change includes:
- Command names.
- Argument positions.
- Option names and defaults.
- Exit codes.
- stdout/stderr behavior.
- JSON keys.
- CSV columns.
Such changes require tests and documentation.
Required CLI tests
- Root help.
- Version.
- No arguments behavior.
- Missing input path.
- Directory passed as input.
- Empty file.
- Invalid numeric options.
- No supported records.
- Validate mixed, full, and unsupported-only coverage, report-before-exit-4 behavior, input errors,
no-color output, one-pass/non-invocation behavior, and unexpected-error redaction.
- Detection with and without
--fail-on-detection.
--no-color output.
- Custom threshold/window/top behavior.
- Top-ips success, ranking, limit, no-color, and detector non-invocation behavior.
- Top-ips missing/non-file/empty/invalid-UTF-8 inputs, unsupported-only files, and analyzer error
translation.
- IPv6 findings and same-source chronology errors.
- Generic unexpected failures without traceback or exception-message disclosure.
- Export JSON/CSV schema, ordering, UTF-8, formula-leading username, destination, overwrite,
symlink, same-file, and failure-preservation behavior.
JSON/CSV and export failure tests are required for the implemented export command.
Use Typer's CliRunner for command tests and tmp_path for files.
Review checklist