Skip to content

Commit ae6cab4

Browse files
committed
feat: bootstrap loglens mvp
0 parents  commit ae6cab4

27 files changed

Lines changed: 3444 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
build-and-test:
12+
name: CI (${{ matrix.os }})
13+
runs-on: ${{ matrix.os }}
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
os:
18+
- ubuntu-latest
19+
- windows-latest
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
24+
25+
- name: Configure
26+
run: cmake -S . -B build -D CMAKE_BUILD_TYPE=Release -D BUILD_TESTING=ON
27+
28+
- name: Build
29+
run: cmake --build build --config Release
30+
31+
- name: Test
32+
run: ctest --test-dir build --build-config Release --output-on-failure

.github/workflows/codeql.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
pull_request:
6+
schedule:
7+
- cron: "0 6 * * 1"
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
analyze:
14+
name: CodeQL
15+
runs-on: ubuntu-latest
16+
permissions:
17+
contents: read
18+
actions: read
19+
security-events: write
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
24+
25+
- name: Initialize CodeQL
26+
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
27+
with:
28+
languages: c-cpp
29+
30+
- name: Configure
31+
run: cmake -S . -B build -D CMAKE_BUILD_TYPE=Release -D BUILD_TESTING=ON
32+
33+
- name: Build
34+
run: cmake --build build --config Release
35+
36+
- name: Analyze
37+
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
build/
2+
build_manual*/
3+
out/
4+
report.md
5+
report.json
6+
*.exe

AGENTS.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# AGENTS.md
2+
3+
## Project
4+
LogLens is a defensive C++20 CLI for parsing Linux authentication logs and generating structured detection reports.
5+
6+
## Priorities
7+
1. Working MVP first
8+
2. Clean modular C++20
9+
3. Safe public-repo content
10+
4. Reproducible build and tests
11+
5. Clear README and docs
12+
13+
## Constraints
14+
- Do not add offensive or exploitation functionality
15+
- Do not use real IPs, secrets, usernames, or private infrastructure identifiers
16+
- Prefer standard library over third-party dependencies
17+
- Keep file structure simple
18+
- Avoid unnecessary templates or meta-programming
19+
- Avoid heavy regex-only designs if a clearer parser is possible
20+
- Keep detection rules centralized and configurable
21+
22+
## Code style
23+
- C++20
24+
- Readable names
25+
- Small functions
26+
- Comments only where they add real value
27+
- Fail gracefully on malformed log lines
28+
29+
## Repository rules
30+
- Always update README when adding user-visible features
31+
- Add or update tests for parser and detector changes
32+
- Preserve public-safe placeholders like 203.0.113.x and example-host
33+
- Do not introduce large unrelated refactors
34+
35+
## Task behavior
36+
When given a task:
37+
1. inspect repository state
38+
2. explain plan briefly
39+
3. implement in small steps
40+
4. run build/tests if available
41+
5. summarize created/modified files and remaining issues

CMakeLists.txt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
cmake_minimum_required(VERSION 3.20)
2+
3+
project(LogLens VERSION 0.1.0 LANGUAGES CXX)
4+
5+
set(CMAKE_CXX_STANDARD 20)
6+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
7+
set(CMAKE_CXX_EXTENSIONS OFF)
8+
9+
add_library(loglens_lib
10+
src/config.cpp
11+
src/parser.cpp
12+
src/signal.cpp
13+
src/detector.cpp
14+
src/report.cpp
15+
)
16+
17+
target_include_directories(loglens_lib
18+
PUBLIC
19+
"${CMAKE_CURRENT_SOURCE_DIR}/src"
20+
)
21+
22+
add_executable(loglens src/main.cpp)
23+
target_link_libraries(loglens PRIVATE loglens_lib)
24+
25+
include(CTest)
26+
if(BUILD_TESTING)
27+
add_executable(test_parser tests/test_parser.cpp)
28+
target_link_libraries(test_parser PRIVATE loglens_lib)
29+
add_test(NAME parser COMMAND test_parser)
30+
31+
add_executable(test_detector tests/test_detector.cpp)
32+
target_link_libraries(test_detector PRIVATE loglens_lib)
33+
add_test(NAME detector COMMAND test_detector)
34+
35+
add_executable(test_cli tests/test_cli.cpp)
36+
target_link_libraries(test_cli PRIVATE loglens_lib)
37+
add_test(
38+
NAME cli
39+
COMMAND test_cli
40+
$<TARGET_FILE:loglens>
41+
${CMAKE_CURRENT_SOURCE_DIR}/assets/sample_auth.log
42+
${CMAKE_CURRENT_SOURCE_DIR}/assets/sample_config.json
43+
${CMAKE_CURRENT_BINARY_DIR}/cli_test_output
44+
)
45+
endif()

README.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# LogLens
2+
3+
[![CI](https://img.shields.io/badge/CI-GitHub_Actions-blue)](./.github/workflows/ci.yml)
4+
[![CodeQL](https://img.shields.io/badge/CodeQL-code_scanning-blue)](./.github/workflows/codeql.yml)
5+
6+
LogLens is a defensive C++20 CLI that parses Linux authentication logs and produces concise Markdown and JSON reports for suspicious authentication activity. The project is intended for portfolio-grade detection engineering work, not offensive security or attack automation.
7+
8+
These badges are local workflow markers in this working copy because the repository does not currently have a configured GitHub remote. After publishing the repository, replace them with repository-specific GitHub status badge URLs.
9+
10+
## Repository Checks
11+
12+
LogLens includes two minimal GitHub Actions workflows:
13+
14+
- `CI` builds and tests the project on `ubuntu-latest` and `windows-latest`
15+
- `CodeQL` runs GitHub code scanning for C/C++ on pushes, pull requests, and a weekly schedule
16+
17+
Both workflows are intended to stay stable enough to require on pull requests to `main`. The repository hardening note is in [`docs/repo-hardening.md`](./docs/repo-hardening.md).
18+
19+
## Threat Model
20+
21+
LogLens is designed for offline review of `auth.log` and `secure` style text logs collected from systems you own or administer. The MVP focuses on common, high-signal patterns that often appear during credential guessing, username enumeration, or bursty privileged command use.
22+
23+
The current tool helps answer:
24+
25+
- Is one source IP generating repeated SSH failures in a short window?
26+
- Is one source IP trying several usernames in a short window?
27+
- Is one account running sudo unusually often in a short window?
28+
29+
It does not attempt to replace a SIEM, correlate across hosts, enrich IPs, or decide whether a finding is malicious on its own.
30+
31+
## Detections
32+
33+
LogLens currently detects:
34+
35+
- Repeated SSH failed password attempts from the same IP within 10 minutes
36+
- One IP trying multiple usernames within 15 minutes
37+
- Bursty sudo activity from the same user within 5 minutes
38+
39+
LogLens currently parses and reports these additional auth patterns:
40+
41+
- `Failed publickey` SSH failures, which count toward SSH brute-force detection by default
42+
- `pam_unix(...:auth): authentication failure`
43+
- `pam_unix(...:session): session opened`
44+
45+
LogLens also tracks parser coverage telemetry for unsupported or malformed lines, including:
46+
47+
- `total_lines`
48+
- `parsed_lines`
49+
- `unparsed_lines`
50+
- `parse_success_rate`
51+
- `top_unknown_patterns`
52+
53+
LogLens does not currently detect:
54+
55+
- Lateral movement
56+
- MFA abuse
57+
- SSH key misuse
58+
- PAM-specific failures beyond the parsed sample patterns
59+
- Cross-file or cross-host correlation
60+
61+
## Build
62+
63+
```bash
64+
cmake -S . -B build
65+
cmake --build build
66+
ctest --test-dir build --output-on-failure
67+
```
68+
69+
## Run
70+
71+
```bash
72+
./build/loglens --mode syslog --year 2026 ./assets/sample_auth.log ./out
73+
./build/loglens --mode journalctl-short-full ./assets/sample_journalctl_short_full.log ./out-journal
74+
./build/loglens --config ./assets/sample_config.json ./assets/sample_auth.log ./out-config
75+
```
76+
77+
The CLI writes:
78+
79+
- `report.md`
80+
- `report.json`
81+
82+
into the output directory you provide. If you omit the output directory, the files are written into the current working directory.
83+
84+
The config file schema is intentionally small and strict:
85+
86+
```json
87+
{
88+
"input_mode": "syslog_legacy",
89+
"timestamp": {
90+
"assume_year": 2026
91+
},
92+
"brute_force": { "threshold": 5, "window_minutes": 10 },
93+
"multi_user_probing": { "threshold": 3, "window_minutes": 15 },
94+
"sudo_burst": { "threshold": 3, "window_minutes": 5 },
95+
"auth_signal_mappings": {
96+
"ssh_failed_password": {
97+
"counts_as_attempt_evidence": true,
98+
"counts_as_terminal_auth_failure": true
99+
},
100+
"ssh_invalid_user": {
101+
"counts_as_attempt_evidence": true,
102+
"counts_as_terminal_auth_failure": true
103+
},
104+
"ssh_failed_publickey": {
105+
"counts_as_attempt_evidence": true,
106+
"counts_as_terminal_auth_failure": true
107+
},
108+
"pam_auth_failure": {
109+
"counts_as_attempt_evidence": true,
110+
"counts_as_terminal_auth_failure": false
111+
}
112+
}
113+
}
114+
```
115+
116+
This mapping lets LogLens normalize parsed events into detection signals before applying brute-force or multi-user rules. By default, `pam_auth_failure` is treated as lower-confidence attempt evidence and does not count as a terminal authentication failure unless the config explicitly upgrades it.
117+
118+
Timestamp handling is now explicit:
119+
120+
- `--mode syslog` or `input_mode: syslog_legacy` requires `--year` or `timestamp.assume_year`
121+
- `--mode journalctl-short-full` or `input_mode: journalctl_short_full` parses the embedded year and timezone and ignores `assume_year`
122+
123+
## Example Input
124+
125+
```text
126+
Mar 10 08:11:22 example-host sshd[1234]: Failed password for invalid user admin from 203.0.113.10 port 51022 ssh2
127+
Mar 10 08:12:10 example-host sshd[1235]: Accepted password for alice from 203.0.113.20 port 51111 ssh2
128+
Mar 10 08:15:00 example-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/systemctl restart ssh
129+
Mar 10 08:27:10 example-host sshd[1243]: Failed publickey for invalid user svc-backup from 203.0.113.40 port 51240 ssh2
130+
Mar 10 08:28:33 example-host pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=203.0.113.41 user=alice
131+
Mar 10 08:29:50 example-host pam_unix(sudo:session): session opened for user root by alice(uid=0)
132+
Mar 10 08:30:12 example-host sshd[1244]: Connection closed by authenticating user alice 203.0.113.50 port 51290 [preauth]
133+
Mar 10 08:31:18 example-host sshd[1245]: Timeout, client not responding from 203.0.113.51 port 51291
134+
```
135+
136+
`journalctl --output short-full` style example:
137+
138+
```text
139+
Tue 2026-03-10 08:11:22 UTC example-host sshd[2234]: Failed password for invalid user admin from 203.0.113.10 port 51022 ssh2
140+
Tue 2026-03-10 08:13:10 UTC example-host sshd[2236]: Failed password for test from 203.0.113.10 port 51040 ssh
141+
Tue 2026-03-10 08:18:05 UTC example-host sshd[2238]: Failed publickey for invalid user deploy from 203.0.113.10 port 51060 ssh2
142+
Tue 2026-03-10 08:31:18 UTC example-host sshd[2245]: Connection closed by authenticating user alice 203.0.113.51 port 51291 [preauth]
143+
```
144+
145+
## Example Output
146+
147+
`report.md` excerpt:
148+
149+
```markdown
150+
# LogLens Report
151+
152+
## Summary
153+
- Input mode: syslog_legacy
154+
- Assume year: 2026
155+
- Timezone present: false
156+
- Total lines: 16
157+
- Parsed lines: 14
158+
- Unparsed lines: 2
159+
- Parse success rate: 87.50%
160+
- Parsed events: 14
161+
- Findings: 3
162+
- Parser warnings: 2
163+
```
164+
165+
`report.json` excerpt:
166+
167+
```json
168+
{
169+
"tool": "LogLens",
170+
"input_mode": "syslog_legacy",
171+
"assume_year": 2026,
172+
"timezone_present": false,
173+
"parser_quality": {
174+
"total_lines": 16,
175+
"parsed_lines": 14,
176+
"unparsed_lines": 2,
177+
"parse_success_rate": 0.8750
178+
},
179+
"parsed_event_count": 14,
180+
"finding_count": 3
181+
}
182+
```
183+
184+
## Known Limitations
185+
186+
- `syslog_legacy` mode requires an explicit year; LogLens no longer guesses one implicitly.
187+
- `journalctl_short_full` parsing currently supports `UTC`, `GMT`, `Z`, and numeric timezone offsets such as `+0000` or `+00:00`, not arbitrary timezone abbreviations.
188+
- The parser supports a small set of common `sshd`, `sudo`, and `pam_unix` patterns from `auth.log` or `secure`, not every distro-specific variant.
189+
- Unsupported lines are surfaced as parser telemetry and warnings only; they do not generate detector findings on their own.
190+
- `pam_unix` auth failures remain lower-confidence by default; they influence detectors only if `auth_signal_mappings` explicitly upgrades them.
191+
- Detector thresholds and auth signal mappings are configurable only through the fixed `config.json` schema shown above; partial overrides and alternative config formats are not supported.
192+
- Findings are intentionally rule-based and conservative; they are not attribution or incident verdicts.
193+
194+
## Future Roadmap
195+
196+
- Additional auth patterns and PAM coverage
197+
- Better host-level summaries
198+
- Optional CSV export
199+
- Larger sanitized test corpus

assets/sample_auth.log

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Mar 10 08:11:22 example-host sshd[1234]: Failed password for invalid user admin from 203.0.113.10 port 51022 ssh2
2+
Mar 10 08:12:05 example-host sshd[1235]: Failed password for root from 203.0.113.10 port 51030 ssh2
3+
Mar 10 08:13:10 example-host sshd[1236]: Failed password for test from 203.0.113.10 port 51040 ssh2
4+
Mar 10 08:14:44 example-host sshd[1237]: Failed password for guest from 203.0.113.10 port 51050 ssh2
5+
Mar 10 08:18:05 example-host sshd[1238]: Failed password for invalid user deploy from 203.0.113.10 port 51060 ssh2
6+
Mar 10 08:20:10 example-host sshd[1240]: Accepted password for alice from 203.0.113.20 port 51111 ssh2
7+
Mar 10 08:21:00 example-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/systemctl restart ssh
8+
Mar 10 08:22:10 example-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/journalctl -xe
9+
Mar 10 08:24:15 example-host sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/vi /etc/ssh/sshd_config
10+
Mar 10 08:25:30 example-host sshd[1241]: Failed password for bob from 203.0.113.30 port 51234 ssh2
11+
Mar 10 08:26:02 example-host sshd[1242]: Invalid user backup from 203.0.113.31 port 51236
12+
Mar 10 08:27:10 example-host sshd[1243]: Failed publickey for invalid user svc-backup from 203.0.113.40 port 51240 ssh2
13+
Mar 10 08:28:33 example-host pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=203.0.113.41 user=alice
14+
Mar 10 08:29:50 example-host pam_unix(sudo:session): session opened for user root by alice(uid=0)
15+
Mar 10 08:30:12 example-host sshd[1244]: Connection closed by authenticating user alice 203.0.113.50 port 51290 [preauth]
16+
Mar 10 08:31:18 example-host sshd[1245]: Timeout, client not responding from 203.0.113.51 port 51291

assets/sample_auth_malformed.log

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Mar 10 09:00:01 example-host sshd[2000]: Failed password for invalid user ops from 203.0.113.55 port 52000 ssh2
2+
bad-line
3+
Mar 10 09:00:xx example-host sshd[2001]: Failed password for root from 203.0.113.55 port 52010 ssh2
4+
Mar 10 09:02:15 example-host sudo: analyst : TTY=pts/1 ; PWD=/home/analyst ; USER=root ; COMMAND=/usr/bin/id

0 commit comments

Comments
 (0)