Skip to content

Commit 10ea912

Browse files
authored
Merge pull request #65 from stacknil/stacknil/lgl-05-parser-failure-taxonomy
feat(parser): classify parser failures
2 parents 97adfd1 + ebbd839 commit 10ea912

28 files changed

Lines changed: 515 additions & 172 deletions

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,17 @@ LogLens also tracks parser coverage telemetry for unsupported or malformed lines
8383
- `parsed_lines`
8484
- `unparsed_lines`
8585
- `parse_success_rate`
86+
- `failure_categories`
8687
- `top_unknown_patterns`
8788

8889
Common unsupported-pattern buckets include `sshd_connection_closed_preauth`,
8990
`sshd_timeout_or_disconnection`, `sshd_negotiation_failure`,
9091
`pam_faillock_account_locked`, and `pam_unix_session_closed`. These buckets keep
9192
non-finding evidence reviewable without counting it as detector evidence.
93+
Failure categories group unsupported lines into reviewer-facing parser boundary
94+
classes: `unknown_timestamp`, `unknown_program`,
95+
`known_program_unknown_message`, `malformed_source_ip`, and
96+
`unsupported_pam_variant`.
9297

9398
For rule-by-rule semantics and signal boundaries, see [`docs/rule-catalog.md`](./docs/rule-catalog.md). For a forensic-style evidence walkthrough, see [`docs/case-study-linux-auth-bruteforce.md`](./docs/case-study-linux-auth-bruteforce.md). For the parser behavior contract, supported modes, and fixture map, see [`docs/parser-contract.md`](./docs/parser-contract.md). For the deliberately noisy parser-coverage sample, see [`docs/parser-coverage-notes.md`](./docs/parser-coverage-notes.md).
9499

@@ -142,7 +147,7 @@ When you add `--csv`, LogLens also writes:
142147
The CSV schema is intentionally small and stable:
143148

144149
- `findings.csv`: `rule`, `subject_kind`, `subject`, `event_count`, `window_start`, `window_end`, `usernames`, `summary`
145-
- `warnings.csv`: `kind`, `line_number`, `message`
150+
- `warnings.csv`: `kind`, `line_number`, `category`, `message`
146151

147152
Without `--csv`, LogLens does not create, overwrite, or delete any existing CSV files in the output directory.
148153

docs/case-study-linux-auth-bruteforce.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,10 @@ The sudo finding is adjacent but separate. It is not joined to the SSH failure c
110110

111111
The parser warnings are:
112112

113-
| Line | Unknown-pattern bucket | Evidence interpretation |
114-
| ---: | --- | --- |
115-
| 15 | `sshd_connection_closed_preauth` | preauth connection-close noise was observed but not promoted to a typed event |
116-
| 16 | `sshd_timeout_or_disconnection` | timeout/disconnection noise was observed but not promoted to a typed event |
113+
| Line | Failure category | Unknown-pattern bucket | Evidence interpretation |
114+
| ---: | --- | --- | --- |
115+
| 15 | `known_program_unknown_message` | `sshd_connection_closed_preauth` | preauth connection-close noise was observed but not promoted to a typed event |
116+
| 16 | `known_program_unknown_message` | `sshd_timeout_or_disconnection` | timeout/disconnection noise was observed but not promoted to a typed event |
117117

118118
These warnings are useful because they prevent silent overconfidence. A reviewer can see both the finding-producing evidence and the unsupported surrounding records.
119119

docs/parser-conformance-matrix.md

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ corpus.
88
The parser contract is intentionally conservative:
99

1010
- recognized evidence emits a normalized `Event`
11-
- unsupported evidence emits a parser warning and an unknown-pattern bucket
11+
- unsupported evidence emits a parser warning, a failure category, and an unknown-pattern bucket
1212
- unsupported evidence does not become detector input
1313

1414
## Input Format Matrix
@@ -57,42 +57,43 @@ event type in both formats.
5757
Unsupported buckets are warning labels, not normalized events. The expected
5858
normalized event is always `none`.
5959

60-
| Unsupported evidence | Input formats | Expected unsupported line bucket | Expected normalized event |
61-
| --- | --- | --- | --- |
62-
| `sshd` preauth connection closed or reset, including `Connection closed by ... [preauth]`, `Connection closed by authenticating user ... [preauth]`, and `Connection reset by ... [preauth]` | `syslog_legacy`, `journalctl_short_full` | `sshd_connection_closed_preauth` | none |
63-
| `sshd` timeout, disconnection, or disconnect notice, including `Timeout, client not responding`, `Disconnected from ...`, and `Received disconnect ...` | `syslog_legacy`, `journalctl_short_full` | `sshd_timeout_or_disconnection` | none |
64-
| `sshd` negotiation failure such as `Unable to negotiate with ...` | `syslog_legacy`, `journalctl_short_full` | `sshd_negotiation_failure` | none |
65-
| Other well-formed but unsupported `sshd` messages | `syslog_legacy`, `journalctl_short_full` | `sshd_other` | none |
66-
| `pam_unix(...:session)` session closed | `syslog_legacy`, `journalctl_short_full` | `pam_unix_session_closed` | none |
67-
| Other unsupported `pam_unix(...)` messages | `syslog_legacy`, `journalctl_short_full` | `pam_unix_other` | none |
68-
| `pam_faillock(...:auth)` account temporarily locked | `syslog_legacy`, `journalctl_short_full` | `pam_faillock_account_locked` | none |
69-
| `pam_faillock(...:auth)` successful authentication telemetry | `syslog_legacy`, `journalctl_short_full` | `pam_faillock_authsucc` | none |
70-
| Other unsupported `pam_faillock(...)` messages | `syslog_legacy`, `journalctl_short_full` | `pam_faillock_other` | none |
71-
| `pam_sss(...:auth)` user not known to underlying authentication module | `syslog_legacy`, `journalctl_short_full` | `pam_sss_unknown_user` | none |
72-
| `pam_sss(...:auth)` authentication service cannot retrieve authentication info | `syslog_legacy`, `journalctl_short_full` | `pam_sss_authinfo_unavail` | none |
73-
| Other unsupported `pam_sss(...)` messages | `syslog_legacy`, `journalctl_short_full` | `pam_sss_other` | none |
74-
| Well-formed `sudo` line that is not command, incorrect-password, or policy-denial evidence | `syslog_legacy`, `journalctl_short_full` | `sudo_other` | none |
75-
| Well-formed `su` line that is not recognized as success or failure audit evidence | `syslog_legacy`, `journalctl_short_full` | `su_other` | none |
76-
| Well-formed unsupported program tag | `syslog_legacy`, `journalctl_short_full` | `program_<sanitized_program>` | none |
60+
| Unsupported evidence | Input formats | Failure category | Expected unsupported line bucket | Expected normalized event |
61+
| --- | --- | --- | --- | --- |
62+
| `sshd` preauth connection closed or reset, including `Connection closed by ... [preauth]`, `Connection closed by authenticating user ... [preauth]`, and `Connection reset by ... [preauth]` | `syslog_legacy`, `journalctl_short_full` | `known_program_unknown_message` | `sshd_connection_closed_preauth` | none |
63+
| `sshd` timeout, disconnection, or disconnect notice, including `Timeout, client not responding`, `Disconnected from ...`, and `Received disconnect ...` | `syslog_legacy`, `journalctl_short_full` | `known_program_unknown_message` | `sshd_timeout_or_disconnection` | none |
64+
| `sshd` negotiation failure such as `Unable to negotiate with ...` | `syslog_legacy`, `journalctl_short_full` | `known_program_unknown_message` | `sshd_negotiation_failure` | none |
65+
| Other well-formed but unsupported `sshd` messages | `syslog_legacy`, `journalctl_short_full` | `known_program_unknown_message` | `sshd_other` | none |
66+
| `pam_unix(...:session)` session closed | `syslog_legacy`, `journalctl_short_full` | `unsupported_pam_variant` | `pam_unix_session_closed` | none |
67+
| Other unsupported `pam_unix(...)` messages | `syslog_legacy`, `journalctl_short_full` | `unsupported_pam_variant` | `pam_unix_other` | none |
68+
| `pam_faillock(...:auth)` account temporarily locked | `syslog_legacy`, `journalctl_short_full` | `unsupported_pam_variant` | `pam_faillock_account_locked` | none |
69+
| `pam_faillock(...:auth)` successful authentication telemetry | `syslog_legacy`, `journalctl_short_full` | `unsupported_pam_variant` | `pam_faillock_authsucc` | none |
70+
| Other unsupported `pam_faillock(...)` messages | `syslog_legacy`, `journalctl_short_full` | `unsupported_pam_variant` | `pam_faillock_other` | none |
71+
| `pam_sss(...:auth)` user not known to underlying authentication module | `syslog_legacy`, `journalctl_short_full` | `unsupported_pam_variant` | `pam_sss_unknown_user` | none |
72+
| `pam_sss(...:auth)` authentication service cannot retrieve authentication info | `syslog_legacy`, `journalctl_short_full` | `unsupported_pam_variant` | `pam_sss_authinfo_unavail` | none |
73+
| Other unsupported `pam_sss(...)` messages | `syslog_legacy`, `journalctl_short_full` | `unsupported_pam_variant` | `pam_sss_other` | none |
74+
| Well-formed `sudo` line that is not command, incorrect-password, or policy-denial evidence | `syslog_legacy`, `journalctl_short_full` | `known_program_unknown_message` | `sudo_other` | none |
75+
| Well-formed `su` line that is not recognized as success or failure audit evidence | `syslog_legacy`, `journalctl_short_full` | `known_program_unknown_message` | `su_other` | none |
76+
| Well-formed unsupported program tag | `syslog_legacy`, `journalctl_short_full` | `unknown_program` | `program_<sanitized_program>` | none |
7777

7878
## Header And Structural Warning Matrix
7979

8080
Structural failures do not reach the authentication message classifier. They
8181
still produce parser warnings and unknown-pattern buckets through the same
8282
coverage telemetry path.
8383

84-
| Failure class | Input formats | Expected bucket | Expected normalized event |
85-
| --- | --- | --- | --- |
86-
| Missing syslog assumed year | `syslog_legacy` | `syslog_legacy_mode_requires_assume_year` | none |
87-
| Missing syslog header fields | `syslog_legacy` | `missing_syslog_header_fields` | none |
88-
| Invalid syslog month token | `syslog_legacy` | `invalid_month_token` | none |
89-
| Invalid syslog day token | `syslog_legacy` | `invalid_day_token` | none |
90-
| Invalid time token | `syslog_legacy`, `journalctl_short_full` | `invalid_time_token` | none |
91-
| Invalid calendar date | `syslog_legacy`, `journalctl_short_full` | `invalid_calendar_date` | none |
92-
| Missing journalctl short-full header fields | `journalctl_short_full` | `missing_journalctl_short_full_header_fields` | none |
93-
| Invalid journalctl date token | `journalctl_short_full` | `invalid_journalctl_date_token` | none |
94-
| Invalid journalctl timezone token | `journalctl_short_full` | `invalid_timezone_token` | none |
95-
| Missing program/message delimiter | `syslog_legacy`, `journalctl_short_full` | `missing_program_message_delimiter` | none |
84+
| Failure class | Input formats | Failure category | Expected bucket | Expected normalized event |
85+
| --- | --- | --- | --- | --- |
86+
| Missing syslog assumed year | `syslog_legacy` | `unknown_timestamp` | `syslog_legacy_mode_requires_assume_year` | none |
87+
| Missing syslog header fields | `syslog_legacy` | `unknown_timestamp` | `missing_syslog_header_fields` | none |
88+
| Invalid syslog month token | `syslog_legacy` | `unknown_timestamp` | `invalid_month_token` | none |
89+
| Invalid syslog day token | `syslog_legacy` | `unknown_timestamp` | `invalid_day_token` | none |
90+
| Invalid time token | `syslog_legacy`, `journalctl_short_full` | `unknown_timestamp` | `invalid_time_token` | none |
91+
| Invalid calendar date | `syslog_legacy`, `journalctl_short_full` | `unknown_timestamp` | `invalid_calendar_date` | none |
92+
| Missing journalctl short-full header fields | `journalctl_short_full` | `unknown_timestamp` | `missing_journalctl_short_full_header_fields` | none |
93+
| Invalid journalctl date token | `journalctl_short_full` | `unknown_timestamp` | `invalid_journalctl_date_token` | none |
94+
| Invalid journalctl timezone token | `journalctl_short_full` | `unknown_timestamp` | `invalid_timezone_token` | none |
95+
| Missing program/message delimiter | `syslog_legacy`, `journalctl_short_full` | `unknown_program` | `missing_program_message_delimiter` | none |
96+
| Malformed source IP token | `syslog_legacy`, `journalctl_short_full` | `malformed_source_ip` | `malformed_source_ip` | none |
9697

9798
## Fixture Anchors
9899

@@ -113,4 +114,5 @@ these places:
113114
- normalized event expectation in `tests/test_parser.cpp`
114115
- supported fixture line under `assets/`
115116
- unsupported warning bucket expectation
117+
- parser failure category expectation
116118
- report-contract fixture if the visible report shape changes

docs/parser-contract.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,20 @@ Recognized success or audit families include accepted password, accepted publick
3636
| --- | --- | --- |
3737
| 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 |
3838
| Blank line | Skips the line and increments `skipped_blank_lines` | Does not become a warning or parsed event |
39-
| Malformed header | Emits a parser warning with the original line number and structural reason | Counts toward `unparsed_lines` and `top_unknown_patterns` |
40-
| Well-formed but unsupported auth pattern | Emits a parser warning with an unknown-pattern bucket | Stays visible as telemetry instead of being silently ignored |
39+
| 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` |
40+
| 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 |
4141

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

44+
Parser failure categories are intentionally coarser than unknown-pattern
45+
buckets:
46+
47+
- `unknown_timestamp`
48+
- `unknown_program`
49+
- `known_program_unknown_message`
50+
- `malformed_source_ip`
51+
- `unsupported_pam_variant`
52+
4453
Stable unsupported-pattern buckets currently exercised by the fixture corpus include
4554
`sshd_connection_closed_preauth`, `sshd_timeout_or_disconnection`,
4655
`sshd_negotiation_failure`, `pam_faillock_account_locked`, and

docs/parser-coverage-notes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ The locked expected coverage summary lives in [`tests/fixtures/parser_matrix/noi
2020
- `parsed_lines`: 8
2121
- `unparsed_lines`: 16
2222
- `parse_success_rate`: 0.3333333333
23+
- `failure_categories`: coarse parser boundary categories for unsupported lines
2324
- `top_unknown_patterns`: the five most common unsupported-pattern buckets
2425

2526
## Reading the numbers
2627

27-
A low parse success rate is not automatically a bug for this fixture. The sample is deliberately noisy, and the useful property is that unsupported evidence remains explainable through `warnings` and `top_unknown_patterns`.
28+
A low parse success rate is not automatically a bug for this fixture. The sample is deliberately noisy, and the useful property is that unsupported evidence remains explainable through `warnings`, `failure_categories`, and `top_unknown_patterns`.
2829

2930
The matrix should stay defensive and public-safe: use documentation IP ranges, synthetic hostnames, and synthetic usernames only.

docs/report-artifacts.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The JSON report keeps parser observability visible next to findings:
2828
- `parser_quality.parsed_lines`
2929
- `parser_quality.unparsed_lines`
3030
- `parser_quality.parse_success_rate`
31+
- `parser_quality.failure_categories`
3132
- `parser_quality.top_unknown_patterns`
3233
- `parsed_event_count`
3334
- `warning_count`
@@ -41,14 +42,21 @@ Finding objects contain `rule_id`, `rule`, `subject_kind`, `subject`, `grouping_
4142

4243
`evidence_event_ids` are deterministic local event identifiers derived from the source line number, formatted as `line:<number>`. They let reviewers trace a finding back to the normalized input events that satisfied the rule window without implying global event identity.
4344

44-
Warning objects contain the original `line_number` and the parser `reason`.
45+
Warning objects contain the original `line_number`, parser `category`, and parser `reason`.
46+
47+
Parser failure categories are stable reviewer-facing buckets for unsupported
48+
lines: `unknown_timestamp`, `unknown_program`,
49+
`known_program_unknown_message`, `malformed_source_ip`, and
50+
`unsupported_pam_variant`. They complement `top_unknown_patterns`: categories
51+
explain the parser boundary class, while unknown-pattern buckets preserve the
52+
more specific unsupported message shape.
4553

4654
## CSV Contract
4755

4856
The optional CSV exports intentionally stay small:
4957

5058
- `findings.csv`: `rule`, `subject_kind`, `subject`, `event_count`, `window_start`, `window_end`, `usernames`, `summary`
51-
- `warnings.csv`: `kind`, `line_number`, `message`
59+
- `warnings.csv`: `kind`, `line_number`, `category`, `message`
5260

5361
Formula-like CSV text fields are neutralized with a leading single quote so spreadsheet tools treat them as text.
5462

docs/reviewer-path.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Look for parser coverage fields:
6060
- `parsed_lines`
6161
- `unparsed_lines`
6262
- `parse_success_rate`
63+
- `failure_categories`
6364
- `top_unknown_patterns`
6465

6566
Good stopping point: the reviewer can explain what LogLens parses, how rules count supported evidence, what the reports contain, and how unsupported lines remain visible without becoming findings.

docs/rule-catalog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ The finding is a triage signal. It is not a compromise verdict, attribution clai
111111

112112
### Why unsupported evidence is not counted
113113

114-
Unsupported lines are parser warnings, not `AuthSignal` records. They may appear in `top_unknown_patterns`, but they do not carry the `counts_as_terminal_auth_failure` flag required by this rule.
114+
Unsupported lines are parser warnings, not `AuthSignal` records. They may appear in `failure_categories` and `top_unknown_patterns`, but they do not carry the `counts_as_terminal_auth_failure` flag required by this rule.
115115

116116
This prevents unsupported preauth noise, malformed lines, and unmodeled auth-family messages from silently increasing brute-force counts.
117117

0 commit comments

Comments
 (0)