|
| 1 | +# LogLens |
| 2 | + |
| 3 | +[](./.github/workflows/ci.yml) |
| 4 | +[](./.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 |
0 commit comments