Skip to content

Commit d33cbd9

Browse files
authored
Merge pull request #91 from stacknil/stacknil/v0.6-finding-identity
feat(report): add v0.6 finding identity
2 parents 9d0bbe2 + d34b7f6 commit d33cbd9

23 files changed

Lines changed: 611 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,23 @@ All notable user-visible changes should be recorded here.
66

77
### Added
88

9-
- None yet.
9+
- Added stable JSON finding identity fields: `finding_id` and
10+
`episode_index`.
11+
- Added a separated-burst syslog report-contract fixture where one source IP
12+
produces two time-separated brute-force findings.
13+
- Added detector regression coverage for stable episode identity under unsorted
14+
input order and inclusive window-boundary behavior.
15+
- Added parser regression coverage for malformed source-IP token
16+
classification.
1017

1118
### Changed
1219

1320
- Detector rules now emit separate findings for time-separated detection
1421
episodes within the same rule subject instead of collapsing each subject to a
1522
single best window.
23+
- Bumped the JSON report artifact contract from `loglens.report.v2` /
24+
`schema_version` 2 to `loglens.report.v3` / `schema_version` 3 for finding
25+
identity fields.
1626

1727
### Fixed
1828

@@ -22,6 +32,8 @@ All notable user-visible changes should be recorded here.
2232

2333
- Documented detection episode semantics in the rule catalog and report artifact
2434
contract notes.
35+
- Added the v0.6 Detection Episode Semantics release note and schema v2 to v3
36+
migration guidance.
2537

2638
## v0.5.0
2739

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ or attribution:
1414

1515
```json
1616
{
17+
"finding_id": "finding:brute_force:4e6aec401a0d45ca",
1718
"rule_id": "brute_force",
19+
"episode_index": 1,
1820
"subject_kind": "source_ip",
1921
"subject": "203.0.113.10",
2022
"grouping_key": "source_ip",
@@ -31,7 +33,7 @@ or attribution:
3133

3234
**Release posture:** Early reviewer-stable release with a narrow Linux authentication evidence contract. Parser and detection coverage remain intentionally narrow.
3335

34-
Reviewing the project quickly? Start with [`docs/reviewer-path.md`](./docs/reviewer-path.md), [`docs/reviewer-brief.md`](./docs/reviewer-brief.md), and the [`v0.5 Evidence Explainability release note`](./docs/release-v0.5.0.md). The [`quality gates map`](./docs/quality-gates.md) links claims to tests and fixtures. For detection reasoning, follow the [`one-page incident-style case`](./docs/incident-style-case.md), then use the full [`Linux auth brute-force case study`](./docs/case-study-linux-auth-bruteforce.md), [`rule catalog`](./docs/rule-catalog.md), and [`false-positive taxonomy`](./docs/false-positive-taxonomy.md) for depth. For local scale expectations, see the [`performance envelope`](./docs/performance-envelope.md).
36+
Reviewing the project quickly? Start with [`docs/reviewer-path.md`](./docs/reviewer-path.md), [`docs/reviewer-brief.md`](./docs/reviewer-brief.md), the [`v0.5 Evidence Explainability release note`](./docs/release-v0.5.0.md), and the [`v0.6 Detection Episode Semantics release note`](./docs/release-v0.6.0.md). The [`quality gates map`](./docs/quality-gates.md) links claims to tests and fixtures. For detection reasoning, follow the [`one-page incident-style case`](./docs/incident-style-case.md), then use the full [`Linux auth brute-force case study`](./docs/case-study-linux-auth-bruteforce.md), [`rule catalog`](./docs/rule-catalog.md), and [`false-positive taxonomy`](./docs/false-positive-taxonomy.md) for depth. For local scale expectations, see the [`performance envelope`](./docs/performance-envelope.md).
3537

3638
For a shorter external review entry point focused on uncertainty handling, read
3739
[How LogLens Treats Parser Uncertainty as Evidence](./docs/case-study-parser-uncertainty-as-evidence.md).
@@ -62,7 +64,7 @@ LogLens includes two minimal GitHub Actions workflows:
6264
- `CI` builds and tests the project on `ubuntu-latest` and `windows-latest`
6365
- `CodeQL` runs GitHub code scanning for C/C++ on pushes, pull requests, and a weekly schedule
6466

65-
Both workflows are intended to stay stable enough to require on pull requests to `main`. Regression coverage is backed by sanitized parser fixture matrices plus golden report-contract fixtures for `report.md`, `report.json`, and optional CSV outputs. Release-facing documentation is split across [`CHANGELOG.md`](./CHANGELOG.md), [`docs/release-process.md`](./docs/release-process.md), [`docs/release-v0.1.0.md`](./docs/release-v0.1.0.md), [`docs/release-v0.3.0.md`](./docs/release-v0.3.0.md), [`docs/release-v0.5.0.md`](./docs/release-v0.5.0.md), and the repository's GitHub release notes. The repository hardening note is in [`docs/repo-hardening.md`](./docs/repo-hardening.md), and vulnerability reporting guidance is in [`SECURITY.md`](./SECURITY.md).
67+
Both workflows are intended to stay stable enough to require on pull requests to `main`. Regression coverage is backed by sanitized parser fixture matrices plus golden report-contract fixtures for `report.md`, `report.json`, and optional CSV outputs. Release-facing documentation is split across [`CHANGELOG.md`](./CHANGELOG.md), [`docs/release-process.md`](./docs/release-process.md), [`docs/release-v0.1.0.md`](./docs/release-v0.1.0.md), [`docs/release-v0.3.0.md`](./docs/release-v0.3.0.md), [`docs/release-v0.5.0.md`](./docs/release-v0.5.0.md), [`docs/release-v0.6.0.md`](./docs/release-v0.6.0.md), and the repository's GitHub release notes. The repository hardening note is in [`docs/repo-hardening.md`](./docs/repo-hardening.md), and vulnerability reporting guidance is in [`SECURITY.md`](./SECURITY.md).
6668

6769
## Threat Model
6870

@@ -86,8 +88,9 @@ LogLens currently detects:
8688

8789
Each rule can emit multiple findings for the same subject when matching
8890
evidence appears in time-separated detector episodes. Report consumers should
89-
use `window_start`, `window_end`, and `evidence_event_ids` rather than assuming
90-
one finding per `rule_id` and subject.
91+
use `finding_id`, `episode_index`, `window_start`, `window_end`, and
92+
`evidence_event_ids` rather than assuming one finding per `rule_id` and
93+
subject.
9194

9295
LogLens currently parses and reports these additional auth patterns beyond the core detector inputs:
9396

docs/quality-gates.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ The main review principle is:
1515
| Parser coverage is visible | [`parser-coverage-notes.md`](./parser-coverage-notes.md), [`tests/fixtures/parser_matrix/noisy_auth_expected.json`](../tests/fixtures/parser_matrix/noisy_auth_expected.json) | `test_parser` compares noisy-auth coverage output to the checked-in expected summary | Reviewer can see parsed lines, skipped blanks, warnings, failure categories, and unknown-pattern buckets |
1616
| Unsupported evidence does not silently become detector evidence | [`parser-contract.md`](./parser-contract.md), [`rule-catalog.md`](./rule-catalog.md), [`case-study-linux-auth-bruteforce.md`](./case-study-linux-auth-bruteforce.md) | `test_parser` covers unknown-pattern warnings; `test_detector` covers signal-boundary behavior | Reviewer can explain why unsupported lines remain warnings instead of findings |
1717
| Report artifacts are deterministic | [`report-artifacts.md`](./report-artifacts.md), report-contract fixtures under [`tests/fixtures/report_contracts`](../tests/fixtures/report_contracts) | `test_report_contracts` compares generated `report.md`, `report.json`, `findings.csv`, and `warnings.csv` against golden fixtures | Reviewer can regenerate reports and see schema or text changes as explicit snapshot diffs |
18-
| Findings are explainable | [`rule-catalog.md`](./rule-catalog.md), [`report-artifacts.md`](./report-artifacts.md) | `test_report` checks JSON finding fields; report-contract fixtures lock `rule_id`, `window_start`, `window_end`, `threshold`, `observed_count`, `grouping_key`, `evidence_event_ids`, and `verdict_boundary` | Reviewer can trace a finding from rule context back to source line IDs and see the non-verdict boundary |
18+
| Findings are explainable | [`rule-catalog.md`](./rule-catalog.md), [`report-artifacts.md`](./report-artifacts.md) | `test_report` checks JSON finding fields; report-contract fixtures lock `finding_id`, `episode_index`, `rule_id`, `window_start`, `window_end`, `threshold`, `observed_count`, `grouping_key`, `evidence_event_ids`, and `verdict_boundary` | Reviewer can trace a finding from rule context back to source line IDs and see the non-verdict boundary |
19+
| Detection episodes are explicit | [`release-v0.6.0.md`](./release-v0.6.0.md), [`rule-catalog.md`](./rule-catalog.md#detection-episode-semantics), [`separated_bursts_syslog`](../tests/fixtures/report_contracts/separated_bursts_syslog) | `test_detector` covers separated episodes, stable identity under unsorted input order, and inclusive boundary windows; `test_report_contracts` locks the two-finding separated-burst report | Reviewer can see one rule and subject emit two non-overlapping findings without treating either as an incident verdict |
1920
| False-positive boundaries are visible | [`rule-catalog.md`](./rule-catalog.md), [`case-study-linux-auth-bruteforce.md`](./case-study-linux-auth-bruteforce.md) | Documentation review gate; detector tests ensure unsupported evidence does not inflate counts | Reviewer can state NAT, internal scanner, lab replay, shared bastion, scheduled admin task, and malformed replay boundaries |
2021
| Parser failure taxonomy is exposed | [`parser-contract.md`](./parser-contract.md), [`parser-conformance-matrix.md`](./parser-conformance-matrix.md), [`report-artifacts.md`](./report-artifacts.md) | `test_parser`, `test_report`, `test_cli`, and `test_report_contracts` cover `failure_categories` and warning `category` output | Reviewer can distinguish timestamp, program, known-program unknown-message, malformed-source-IP, and unsupported-PAM failures |
2122
| Local scale expectations are reproducible | [`performance-envelope.md`](./performance-envelope.md), [`scripts/benchmark-performance-envelope.ps1`](../scripts/benchmark-performance-envelope.ps1) | `pwsh -File scripts/benchmark-performance-envelope.ps1` regenerates sanitized benchmark inputs and local summary artifacts | Reviewer can reproduce the 1k/10k/100k-line envelope and understand its caveats |
@@ -41,6 +42,8 @@ update the matching evidence surface in the same pull request:
4142
- parser behavior change: update parser tests, fixture matrices, and parser docs
4243
- report shape change: update report-contract fixtures and report artifact docs
4344
- rule behavior change: update detector tests, rule catalog, and case-study text when relevant
45+
- episode semantics change: update detector tests, separated-burst report
46+
fixtures, schema migration notes, and rule catalog policy text
4447
- warning taxonomy change: update parser failure taxonomy docs and warning snapshots
4548
- performance-envelope change: rerun the benchmark harness and record the platform/result source
4649

docs/release-v0.6.0.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# LogLens v0.6.0 - Detection Episode Semantics
2+
3+
Theme: Detection Episode Semantics.
4+
5+
This release note describes the v0.6 report and detector contract. It does not
6+
add new detection rules. It makes repeated time-separated findings for the same
7+
rule subject explicit and reviewable.
8+
9+
## What Changed
10+
11+
- A single `rule_id`, `subject_kind`, and `subject` can emit multiple
12+
non-overlapping findings when evidence appears in separated detector
13+
episodes.
14+
- JSON reports now use `schema: loglens.report.v3` and `schema_version: 3`.
15+
- JSON findings include stable finding identity fields:
16+
- `finding_id`
17+
- `episode_index`
18+
- The separated-burst contract fixture demonstrates one source IP producing two
19+
distinct brute-force findings in one report.
20+
21+
## Stable JSON Contract
22+
23+
`loglens.report.v3` keeps the v0.5 explainability fields and adds:
24+
25+
| Field | Meaning |
26+
| --- | --- |
27+
| `finding_id` | Deterministic report-local identifier for the selected finding, derived from the rule, subject, selected window, counts, and evidence event IDs. |
28+
| `episode_index` | 1-based sequence number within the same `rule_id`, `subject_kind`, and `subject`. |
29+
30+
Existing v2 finding fields remain part of the stable explainability surface:
31+
32+
- `rule_id`
33+
- `subject_kind`
34+
- `subject`
35+
- `grouping_key`
36+
- `window_start`
37+
- `window_end`
38+
- `threshold`
39+
- `observed_count`
40+
- `evidence_event_ids`
41+
- `verdict_boundary`
42+
43+
The optional CSV contract is unchanged in v0.6.
44+
45+
## Episode Policy
46+
47+
LogLens v0.6 uses cooldown-separated maximal-window episodes:
48+
49+
| Policy point | v0.6 behavior |
50+
| --- | --- |
51+
| First threshold crossing | Used to decide that an episode candidate is eligible to emit a finding. It is not necessarily the reported window. |
52+
| Maximal window | The reported window is the highest-signal sliding window within the episode candidate. |
53+
| Non-overlapping windows | One rule and subject can emit multiple findings, but selected episode candidates do not reuse the same matching signals. |
54+
| Cooldown merge | Signals separated by an idle gap less than or equal to the rule window stay in the same episode candidate. A larger gap starts a new candidate. |
55+
56+
Episode splitting is a reporting model. It is not an incident boundary.
57+
58+
## Separated-Burst Fixture
59+
60+
The fixture
61+
[`tests/fixtures/report_contracts/separated_bursts_syslog/input.log`](../tests/fixtures/report_contracts/separated_bursts_syslog/input.log)
62+
contains one sanitized source IP with:
63+
64+
- five failed SSH attempts from `09:00:00` through `09:04:00`
65+
- five failed SSH attempts from `15:00:00` through `15:04:00`
66+
67+
The expected
68+
[`report.json`](../tests/fixtures/report_contracts/separated_bursts_syslog/report.json)
69+
contains two `brute_force` findings for the same subject:
70+
71+
- `episode_index: 1`, window `2026-03-10 09:00:00` to
72+
`2026-03-10 09:04:00`
73+
- `episode_index: 2`, window `2026-03-10 15:00:00` to
74+
`2026-03-10 15:04:00`
75+
76+
This fixture locks the main v0.6 behavior: repeated separated bursts are no
77+
longer collapsed to one best window.
78+
79+
## Schema v2 to v3 Migration
80+
81+
Consumers should treat `schema` and `schema_version` as the report shape gate:
82+
83+
- v2: `loglens.report.v2`, `schema_version: 2`
84+
- v3: `loglens.report.v3`, `schema_version: 3`
85+
86+
Consumers that keyed findings by `rule_id` and `subject` should migrate to
87+
`finding_id`, or include `episode_index`, `window_start`, `window_end`, and
88+
`evidence_event_ids` in their own composite key.
89+
90+
## Validation Surface
91+
92+
v0.6 is covered by:
93+
94+
- detector tests for separated brute-force, multi-user probing, and sudo-burst
95+
episodes
96+
- detector tests for stable episode identity under unsorted input order
97+
- detector tests for inclusive rule-window boundaries
98+
- parser tests for malformed source-IP token classification
99+
- report tests for `finding_id`, `episode_index`, and schema v3 output
100+
- golden report-contract fixtures for Markdown, JSON, and optional CSV reports
101+
102+
## Non-Claims
103+
104+
LogLens v0.6 findings remain bounded triage signals. The release preserves
105+
these explicit non-claims:
106+
107+
- no compromise verdict
108+
- no attribution
109+
- no blocking recommendation
110+
- no cross-host correlation
111+
112+
Findings remain bounded triage signals over normalized local evidence.

docs/report-artifacts.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ The JSON report keeps parser observability visible next to findings:
4040
- `findings`
4141
- `warnings`
4242

43-
Finding objects contain `rule_id`, `rule`, `subject_kind`, `subject`, `grouping_key`, `threshold`, `observed_count`, `event_count`, `window_start`, `window_end`, `evidence_event_ids`, `verdict_boundary`, `usernames`, and `summary`.
43+
Finding objects contain `finding_id`, `rule_id`, `rule`, `episode_index`, `subject_kind`, `subject`, `grouping_key`, `threshold`, `observed_count`, `event_count`, `window_start`, `window_end`, `evidence_event_ids`, `verdict_boundary`, `usernames`, and `summary`.
4444

45-
The stable finding explainability surface for `loglens.report.v2` is:
45+
The stable finding explainability surface for `loglens.report.v3` is:
4646

47+
- `finding_id`
4748
- `rule_id`
49+
- `episode_index`
4850
- `subject_kind`
4951
- `subject`
5052
- `grouping_key`
@@ -61,10 +63,20 @@ fixtures explicitly.
6163

6264
`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.
6365

66+
`finding_id` is a deterministic report-local finding identifier derived from
67+
the rule, subject, selected window, counts, and evidence event IDs. It is
68+
stable for the same normalized evidence and rule output, but it is not a global
69+
case identifier.
70+
71+
`episode_index` is a 1-based sequence number within one `rule_id`,
72+
`subject_kind`, and `subject`. It is meant for reviewer navigation when a rule
73+
emits more than one finding for the same subject.
74+
6475
Consumers should not assume that `rule_id` plus `subject` is unique within a
6576
report. A rule can emit multiple findings for the same subject when matching
66-
evidence appears in time-separated detector episodes. Use `window_start`,
67-
`window_end`, and `evidence_event_ids` to distinguish episode-level findings.
77+
evidence appears in time-separated detector episodes. Use `finding_id`,
78+
`episode_index`, `window_start`, `window_end`, and `evidence_event_ids` to
79+
distinguish episode-level findings.
6880

6981
`verdict_boundary` is a stable token that states what the finding must not be
7082
read as. It keeps machine-readable findings aligned with LogLens's triage
@@ -79,7 +91,20 @@ Warning objects contain the original `line_number`, parser `category`, and parse
7991
`schema` and `schema_version` identify the report artifact contract, not the
8092
application release. They are intended for downstream tooling that needs a
8193
stable way to reject incompatible report shapes. The current JSON contract is
82-
`loglens.report.v2` with `schema_version` set to `2`.
94+
`loglens.report.v3` with `schema_version` set to `3`.
95+
96+
### Schema v2 to v3 Migration
97+
98+
`loglens.report.v3` keeps the v2 finding explainability fields and adds:
99+
100+
- `finding_id`
101+
- `episode_index`
102+
103+
Downstream consumers should treat `schema` and `schema_version` as the report
104+
shape gate. Consumers that keyed findings by `rule_id` and `subject` should
105+
move to `finding_id`, or include `episode_index`, `window_start`, `window_end`,
106+
and `evidence_event_ids` in their own composite key. The optional CSV contract
107+
is unchanged in v3.
83108

84109
Parser failure categories are stable reviewer-facing buckets for unsupported
85110
lines: `unknown_timestamp`, `unknown_program`,
@@ -111,6 +136,7 @@ The report contracts are backed by generated fixture artifacts:
111136
| [`journalctl_short_full`](../tests/fixtures/report_contracts/journalctl_short_full) | `report.md`, `report.json`, `findings.csv`, `warnings.csv` |
112137
| [`multi_host_syslog_legacy`](../tests/fixtures/report_contracts/multi_host_syslog_legacy) | `report.md`, `report.json`, `findings.csv`, `warnings.csv` |
113138
| [`multi_host_journalctl_short_full`](../tests/fixtures/report_contracts/multi_host_journalctl_short_full) | `report.md`, `report.json`, `findings.csv`, `warnings.csv` |
139+
| [`separated_bursts_syslog`](../tests/fixtures/report_contracts/separated_bursts_syslog) | `report.md`, `report.json`, `findings.csv`, `warnings.csv` |
114140

115141
The enforcement lives in [`tests/test_report_contracts.cpp`](../tests/test_report_contracts.cpp). Parser or rule changes that alter report artifacts must update these snapshots explicitly. This includes changes to stable finding explainability fields, parser coverage fields, warning categories, CSV columns, or Markdown report layout. The focused report writer tests live in [`tests/test_report.cpp`](../tests/test_report.cpp).
116142

0 commit comments

Comments
 (0)