Skip to content

Latest commit

 

History

History
193 lines (146 loc) · 12.3 KB

parser-contract.md

File metadata and controls

193 lines (146 loc) · 12.3 KB

Parser contract

LogLens treats parser behavior as reviewable output, not as a hidden implementation detail. A line is either recognized as a typed event, skipped as blank input, or surfaced as a warning with coverage telemetry.

The guiding rule is:

Parser observability > silent detection claims.

For the v0.5 Evidence Explainability Release, this contract is part of the release-facing reviewer path. Start with docs/release-v0.5.0.md for the release checklist, then use this page to inspect supported inputs, unsupported-line handling, parser warning categories, and the detection signal boundary.

Supported input modes

Mode Typical source Timestamp behavior Review anchor
syslog_legacy auth.log / secure style lines such as Mar 10 08:11:22 example-host sshd[1234]: ... Requires an explicit four-digit year from --year or timestamp.assume_year assets/parser_fixture_matrix_syslog.log
journalctl_short_full journalctl --output=short-full style lines such as Tue 2026-03-10 08:11:22 UTC example-host sshd[1234]: ... Uses the embedded year and supported timezone token assets/parser_fixture_matrix_journalctl_short_full.log

Supported timezone tokens for journalctl_short_full are intentionally narrow: UTC, GMT, Z, and numeric offsets such as +0000 or +00:00.

Day values must be within 1..31 before conversion to a calendar type; month length and leap-year validity are checked afterward. Out-of-range days produce unknown_timestamp / invalid calendar date warnings rather than wrapping into valid dates. See the boundary test report.

Stream completion and read failures (unreleased correction)

parse_stream returns a ParseReport only after the input reaches ordinary EOF without a reported read failure. With the default stream exception mask, empty input, newline-terminated input and a valid final record without a newline all complete normally. The final record retains its event and original line number. Malformed or unsupported complete lines retain their existing warning taxonomy; they are content failures, separate from an underlying stream failure.

The parser checks stream state before acquisition and after the getline loop:

  • badbit is an error, including when eofbit is also set.
  • failbit without eofbit is an error. The fail() predicate also includes badbit; it does not identify failbit alone.
  • EOF without badbit, including the normal eofbit | failbit exhaustion state, is accepted. A stream already exhausted by its caller returns an empty report; the parser does not clear or rewind it.

Reported non-EOF failures throw std::ios_base::failure rather than returning partial events or adding a parser warning. Caller-enabled stream exceptions propagate and the exception mask is unchanged: a buffer exception may be rethrown when badbit exceptions are enabled. Masks including failbit or eofbit can also throw on ordinary EOF, as before; callers requiring normal EOF reports should use the default exception mask. These distinctions follow the C++20 unformatted-input and string-getline rules (working draft, [istream.unformatted], [string.io]).

Counterexample: a buffer that throws after two complete valid records sets badbit; previously the loop ended and returned those two events as a successful report. A mid-record failure could similarly hide the remaining evidence. tests/test_parser_streams.cpp injects faults at exact character positions in both input modes and checks normal EOF, content warnings, initial state and exception masks. Its focused executable also accepts a case-name substring. Mutation controls must reject dropping the completion check, suppressing a read exception into partial success, and rejecting an ordinary unterminated last line.

parse_file retains open-error handling and delegates read validation to this boundary. The CLI catches parsing exceptions before detection and report writing: it exits nonzero, emits an error diagnostic, prints no success summary and leaves existing reports unchanged. The CLI regression uses directory input: on the tested Linux file stream this opens but fails on read; other runtimes can reject it at open. Injected failures after a valid prefix are tested through the stream API rather than an operating-system-specific CLI interception hook.

The parser trusts the stream's EOF/error signals; it cannot detect a buffer that reports false EOF. This does not authenticate log provenance, guarantee an atomic file snapshot or durable storage, impose resource limits, or make multi-file report writes transactional. Successful-input events, coverage counts, detector rules, finding identities and released report contracts are unchanged.

Recognized event families

The parser currently recognizes common authentication evidence from:

  • sshd
  • sudo
  • su
  • login
  • pam_unix(...)
  • selected pam_faillock(...) variants
  • selected pam_sss(...) variants

Recognized SSH failure families include failed password, invalid user, illegal user, failed publickey, failed keyboard-interactive/pam, failed-none invalid-user probing, input_userauth_request invalid/illegal-user preauth traces, sshd-owned PAM authentication-failure lines, and maximum-authentication-attempts-exceeded lines. illegal user is treated as an OpenSSH wording variant of invalid user. Maximum-authentication-attempts and sshd-owned PAM authentication-failure lines may include OpenSSH's leading error: marker and still normalize into the same event family. Invalid or illegal-user variants of failed-none probing, input_userauth_request preauth traces, keyboard-interactive, sshd-owned PAM authentication failures, and maximum-authentication-attempts-exceeded lines are normalized into ssh_invalid_user events. Recognized SSH failures can become detection signals through the configured signal mapping.

Recognized success or audit families include accepted password, accepted publickey, accepted keyboard-interactive/pam, sudo command audit lines, sudo password failures, sudoers policy denials, su success/failure audit lines, selected util-linux login failures and session records, and selected PAM session/auth lines. login failures do not infer a network source IP and remain lower-confidence pam_auth_failure context.

The selected login wording is anchored to the upstream login.c syslog strings. Localized or otherwise unmodeled variants remain visible as login_other warnings.

Line handling contract

Input line outcome Parser behavior Report behavior
Recognized auth line Emits a typed Event with timestamp, hostname, program, optional pid, message, source IP, username, event type, and line number Can contribute to summaries, reports, and configured detection signals
Blank line Skips the line and increments skipped_blank_lines Does not become a warning or parsed event
Malformed header Emits a parser warning with the original line number, structural reason, and unknown_timestamp category Counts toward unparsed_lines, failure_categories, and top_unknown_patterns
Well-formed but unsupported auth pattern Emits a parser warning with a failure category and unknown-pattern bucket Stays visible as telemetry instead of being silently ignored

This is the main trust boundary: unsupported input should remain inspectable, even when it does not produce a finding.

Internal parsing pipeline

AuthLogParser remains the single public parser interface. Its implementation is split into internal modules with one-way flow:

  1. timestamp_parser parses the selected input mode's timestamp and hostname.
  2. source_envelope_parser extracts the program tag, optional pid, and raw message.
  3. program_dispatch selects a registered sshd, PAM, sudo, or su handler.
  4. The selected handler either emits a normalized event or returns a structured failure result.
  5. failure_classifier assigns malformed-source and unsupported-pattern telemetry without turning unsupported evidence into an event.

All registered program handlers return the same internal result shape: matched, optional event, failure_category, and reason. The registry and handler modules are implementation details; callers continue to depend only on AuthLogParser, Event, and the parser telemetry contract.

Parser failure categories are intentionally coarser than unknown-pattern buckets:

  • unknown_timestamp
  • unknown_program
  • known_program_unknown_message
  • malformed_source_ip
  • unsupported_pam_variant

Stable unsupported-pattern buckets currently exercised by the fixture corpus include sshd_connection_closed_preauth, sshd_timeout_or_disconnection, sshd_negotiation_failure, pam_faillock_account_locked, and pam_unix_session_closed. They are parser telemetry and warnings only; detector signal mappings decide which parsed events can contribute to findings.

Detection signal boundary

Parsing a line does not automatically mean it should drive a detector. LogLens keeps that boundary explicit through AuthSignalConfig.

Default terminal SSH failure evidence:

  • ssh_failed_password
  • ssh_invalid_user
  • ssh_failed_publickey
  • ssh_failed_keyboard_interactive
  • ssh_max_auth_tries

Default lower-confidence attempt evidence:

  • pam_auth_failure, which is attempt evidence but not terminal failure evidence unless configured otherwise

Default sudo burst evidence:

  • sudo_command

Parsed successes and audit-only events remain reportable but do not count as brute-force or multi-user failure evidence by default.

Test corpus map

Artifact What it proves
tests/test_parser.cpp Unit-level parser expectations, malformed-line behavior, mode aliases, fixture-matrix counts, and unknown-pattern buckets
tests/test_parser_properties.cpp Deterministic generated checks for handler-registry order independence, malformed source tokens, failure taxonomy stability, and arbitrary-byte result invariants
tests/fuzz/README.md and tests/fuzz/corpus/parser Optional Clang libFuzzer harness instructions plus a sanitized parser seed corpus used by the bounded CI fuzz smoke campaign
tests/test_detector.cpp Detection signal mapping and default counting behavior after parsing
assets/parser_fixture_matrix_syslog.log Syslog known/unknown parser matrix
assets/parser_fixture_matrix_journalctl_short_full.log Journalctl short-full known/unknown parser matrix
assets/parser_auth_families_syslog.log Syslog PAM/auth-family parser coverage
assets/parser_auth_families_journalctl_short_full.log Journalctl PAM/auth-family parser coverage
assets/noisy_auth_sample.log and tests/fixtures/parser_matrix/noisy_auth_expected.json Noisy syslog parser-coverage matrix for malformed, unsupported, blank, irrelevant, multi-host, and unusual-username input
assets/mixed_auth_corpus.log and assets/mixed_auth_parser_coverage.json 160-line mixed auth corpus plus reviewer-facing parser coverage artifact for dirty syslog input and selected util-linux login evidence
tests/test_report_contracts.cpp Stable report-shape expectations for generated artifacts

Non-goals

The parser does not try to:

  • infer missing syslog years
  • support every Linux authentication log variant
  • classify unsupported lines as findings
  • correlate across files or hosts
  • produce incident verdicts

Those boundaries are intentional for the MVP. The current priority is to keep parser coverage explicit and safely extensible.