diff --git a/Images/Filtered.png b/Images/Filtered.png new file mode 100644 index 0000000..6853771 Binary files /dev/null and b/Images/Filtered.png differ diff --git a/Images/Home-scanned.png b/Images/Home-scanned.png index eb51330..9c549fe 100644 Binary files a/Images/Home-scanned.png and b/Images/Home-scanned.png differ diff --git a/Images/Home.png b/Images/Home.png index 381d73f..22a6f5b 100644 Binary files a/Images/Home.png and b/Images/Home.png differ diff --git a/Images/SMB-Enumerated.png b/Images/SMB-Enumerated.png new file mode 100644 index 0000000..b0cfee8 Binary files /dev/null and b/Images/SMB-Enumerated.png differ diff --git a/Images/SMB.png b/Images/SMB.png new file mode 100644 index 0000000..c442ef2 Binary files /dev/null and b/Images/SMB.png differ diff --git a/Images/auto-populated.png b/Images/auto-populated.png new file mode 100644 index 0000000..8e4ea62 Binary files /dev/null and b/Images/auto-populated.png differ diff --git a/Images/filter.png b/Images/filter.png new file mode 100644 index 0000000..cf96fd5 Binary files /dev/null and b/Images/filter.png differ diff --git a/Images/notification.png b/Images/notification.png new file mode 100644 index 0000000..2fff484 Binary files /dev/null and b/Images/notification.png differ diff --git a/Images/reports-overview.png b/Images/reports-overview.png new file mode 100644 index 0000000..44db3c4 Binary files /dev/null and b/Images/reports-overview.png differ diff --git a/Images/risk.png b/Images/risk.png new file mode 100644 index 0000000..ad4219f Binary files /dev/null and b/Images/risk.png differ diff --git a/Images/status.png b/Images/status.png new file mode 100644 index 0000000..1d8e4a5 Binary files /dev/null and b/Images/status.png differ diff --git a/README.md b/README.md index 027441a..bd6c26d 100644 --- a/README.md +++ b/README.md @@ -132,20 +132,31 @@ Opens at **http://localhost:3000**. The server binds to `127.0.0.1:3000` by default. Override with environment variables: `LEAKLENS_HOST` and `LEAKLENS_PORT`. +For a full walkthrough of every feature — SMB share browsing, confidence scoring, custom patterns, suppression, webhooks, and CLI usage — see **[USAGE.md](USAGE.md)**. + --- ## Scanning a file share -1. Enter a UNC path — `\\server\share` — or a local path -2. For UNC paths the **SMB Credentials** panel appears automatically - - Leave blank to try guest/anonymous access first - - Or enter domain credentials for authenticated scans -3. Click **Discover Shares** to enumerate all visible shares on a server -4. Set the max file size to scan (default: 10 MB) -5. Set the number of **Worker Threads** (1–16, default 8) to control scan parallelism -6. Enable **Resume** to continue a previously interrupted scan from its checkpoint -7. Click **Start Scan** -8. Findings stream in as they are found — click any row to see the **matched line**, file metadata, and remediation advice +### Option A — Enter a UNC path directly + +Type a UNC path such as `\\server\share` into the **Path** field and click **Start Scan**. +Credentials are read from the **SMB: Browse Shares & Credentials** modal if you need them. + +### Option B — Browse and select a share (recommended for SMB) + +1. Click **⬡ SMB: Browse Shares & Credentials** in the Scan Configuration panel +2. Enter the server address in the **Server / Host** field + - Standard port: `192.168.1.10` + - Non-standard port: `192.168.1.10:4445` (or `hostname:port`) +3. Optionally enter **Username**, **Password**, and **Domain** — leave blank for guest/anonymous access +4. Click **⬡ Discover Shares** — all visible shares appear in a list (admin shares are labelled separately) +5. Click any share to auto-populate the **Path** field and close the modal +6. Back in the main panel: + - Set **Max Size** (default 10 MB) and **Worker Threads** (default 8) + - Enable **Resume** to continue an interrupted scan from its checkpoint +7. Click **Start Scan** — findings stream in as they are found +8. Click any row to see the **exact matched line**, file metadata, and remediation advice 9. Open the **Reports** tab at any time to reload a previous scan --- @@ -327,6 +338,7 @@ The detail drawer also surfaces this information alongside tailored remediation | `username` | string | — | SMB username | | `password` | string | — | SMB password | | `domain` | string | — | SMB domain | +| `smbPort` | int | 445 | SMB port (use when the server listens on a non-standard port) | ### GET /api/findings — query parameters @@ -354,25 +366,21 @@ A Samba container pre-loaded with intentionally dirty files is included so you c testserver\start-testserver.bat ``` -The container runs Samba on port **4445**. Mount the share locally, then point LeakLens at the mount path. +The container runs Samba on port **4445**. No mounting is needed — LeakLens connects directly over SMB. -**Linux:** -```bash -sudo mkdir -p /mnt/testshare -sudo mount -t cifs //127.0.0.1/testshare /mnt/testshare -o port=4445,guest,vers=2.0 -# Scan: /mnt/testshare -``` +**Scan it with the UI:** -**macOS:** -``` -open 'smb://127.0.0.1:4445/testshare' -# Scan: /Volumes/testshare (or wherever Finder mounts it) -``` +1. Click **⬡ SMB: Browse Shares & Credentials** in the Scan Configuration panel +2. Enter `127.0.0.1:4445` in the **Server / Host** field +3. Leave Username and Password blank (the share allows guest access) +4. Click **⬡ Discover Shares** — `testshare` appears in the list +5. Click **testshare** to select it — the Path field is populated automatically +6. Click **Start Scan** + +**Scan it from the CLI:** -**Windows:** -```cmd -net use Z: \\127.0.0.1\testshare "" /user:guest /port:4445 -:: Scan: Z:\ +```bash +python3 leaklens.py scan --path "\\\\127.0.0.1\\testshare" --smb-port 4445 ``` **Stop it:** diff --git a/USAGE.md b/USAGE.md index 353691e..556676d 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,16 +1,34 @@ -# LeakLens — Usage Guide +# LeakLens — User Guide & Walkthrough -This guide covers installation, scanning, automated testing, and running the test server against intentionally dirty files. For a feature overview and API reference see [README.md](README.md). +This guide walks through every major feature of LeakLens from first launch to advanced usage. +For a quick overview and API reference see [README.md](README.md). --- -## Contents +## Table of Contents 1. [Installation](#1-installation) -2. [Scanning via the UI](#2-scanning-via-the-ui) -3. [Running the automated tests](#3-running-the-automated-tests) -4. [Running the test server with dirty files](#4-running-the-test-server-with-dirty-files) -5. [Suppression — .leaklensignore](#5-suppression--leaklensignore) +2. [First Launch](#2-first-launch) +3. [Scanning a Local Path](#3-scanning-a-local-path) +4. [Scanning an SMB Share](#4-scanning-an-smb-share) + - [Browse and select a share](#41-browse-and-select-a-share-recommended) + - [Guest / anonymous access](#42-guestanonymous-access) + - [Authenticated access](#43-authenticated-access) + - [Non-standard ports](#44-non-standard-ports) +5. [Understanding Your Results](#5-understanding-your-results) + - [Risk levels and confidence scores](#51-risk-levels-and-confidence-scores) + - [The detail drawer](#52-the-detail-drawer) + - [Filtering and searching](#53-filtering-and-searching) +6. [Working with Reports](#6-working-with-reports) +7. [Resuming Interrupted Scans](#7-resuming-interrupted-scans) +8. [Test Server Walkthrough](#8-test-server-walkthrough) +9. [Advanced Features](#9-advanced-features) + - [Tuning worker threads](#91-tuning-worker-threads) + - [Custom detection patterns](#92-custom-detection-patterns) + - [Suppressing false positives](#93-suppressing-false-positives-leaklensignore) + - [Webhooks and notifications](#94-webhooks-and-notifications) + - [CLI usage](#95-cli-usage) +10. [Running the Automated Tests](#10-running-the-automated-tests) --- @@ -27,252 +45,371 @@ source .venv/bin/activate # Windows: .venv\Scripts\activate # Install runtime dependencies pip install -r requirements.txt +``` + +**SMB share enumeration** requires the Samba `smbclient` binary on Linux/macOS (used for the Discover Shares feature): -# Start the server -python leaklens.py +```bash +sudo apt install smbclient # Debian / Ubuntu +sudo dnf install samba-client # Fedora / RHEL +brew install samba # macOS ``` -Opens at **http://localhost:3000**. +--- -### Configuration +## 2. First Launch -The server binds to `127.0.0.1:3000` by default. Override with environment variables: +**Windows** +``` +start.bat +``` +**Linux / macOS** ```bash -LEAKLENS_PORT=8080 python leaklens.py -LEAKLENS_HOST=0.0.0.0 LEAKLENS_PORT=8080 python leaklens.py +chmod +x start.sh && ./start.sh ``` -> **Note:** Binding to `0.0.0.0` exposes the server on all network interfaces. Only do this on trusted networks — LeakLens has no authentication. +Or directly: +```bash +python3 leaklens.py +``` + +Open **http://localhost:3000** in your browser. + +![LeakLens Main ui idle](Images/Home.png) +*Main UI on first load — the status indicator shows Idle, the findings area shows the empty state prompt.* + +> **Binding to other interfaces:** The server defaults to `127.0.0.1:3000`. +> Override with environment variables: `LEAKLENS_HOST=0.0.0.0 LEAKLENS_PORT=8080 python3 leaklens.py` +> Only expose LeakLens on a network interface if you are on a trusted network — it has no authentication. --- -## 2. Scanning via the UI +## 3. Scanning a Local Path -### Local path scan +1. Type a local path into the **Path** field — e.g. `C:\FileShares` on Windows or `/srv/files` on Linux +2. Set **Max Size (MB)** — files larger than this are skipped entirely (default 10 MB) +3. Set **Workers** — number of parallel analysis threads (default 8; see [§9.1](#91-tuning-worker-threads)) +4. Click **▶ Start Scan** -1. Open **http://localhost:3000** -2. Enter a local path in the **Scan Path** field — e.g. `/home/user/shares` or `C:\FileShares` -3. Adjust **Max File Size** (default 10 MB) — files larger than this are skipped -4. Set **Worker Threads** (1–16, default 8) — higher values speed up I/O-bound SMB scans -5. Click **Start Scan** +![LeakLens scan status](Images/status.png) +*A local scan in progress — the progress bar shows the files/second rate and the findings table populates in real time.* -Findings stream in as they are detected. The progress bar shows files/second throughput. +Findings appear as they are discovered. The status dot in the header turns green during scanning and returns to grey when complete. Click **■ Stop Scan** at any time to interrupt — the checkpoint is preserved so you can resume later (see [§7](#7-resuming-interrupted-scans)). -### SMB / UNC path scan +--- + +## 4. Scanning an SMB Share + +LeakLens connects to SMB shares directly over the network — no `net use`, no mounting, no admin rights required. + +### 4.1 Browse and select a share (recommended) + +1. Click **⬡ SMB: Browse Shares & Credentials** in the Scan Configuration panel -1. Enter a UNC path — e.g. `\\fileserver\IT` or `\\192.168.1.10\data` -2. The **SMB Credentials** panel expands automatically - - Leave blank to attempt guest/anonymous access - - Enter `Domain\Username` + password for authenticated access -3. Use **Discover Shares** to enumerate all visible shares on the server before picking one -4. Click **Start Scan** +![LeakLens SMB modal](Images/SMB.png) +*The SMB Share Browser modal — enter a server address to begin.* -### Working with findings +2. Type a server address into **Server / Host** — for example `192.168.1.10` or `fileserver01` +3. Optionally fill in credentials (see [§4.2](#42-guestanonymous-access) and [§4.3](#43-authenticated-access)) +4. Click **⬡ Discover Shares** -- **Click any row** to open the detail drawer — shows the exact matched line and line number, file metadata (size, last modified, last accessed, owner), and remediation advice -- **Risk filter** (HIGH / MEDIUM / LOW / ALL) — narrow the table to the severity you care about -- **Search bar** — substring filter on filename or full path -- **Resume** checkbox — re-scanning the same path will skip already-processed files and continue from the last checkpoint +![LeakLens SMB modal enumerated shares](Images/SMB-Enumerated.png) +*Shares returned by the server. User shares appear with a folder icon; administrative shares (IPC$, ADMIN$, C$) are grouped and labelled separately.* -### Reports tab +5. Click any share row — the **Path** field is populated automatically and the modal closes after a short delay -Every completed scan is saved as a timestamped SQLite database in `reports/`. The **Reports** tab lists all saved scans — click any entry to reload its findings into the main view with full filtering, search, and the detail drawer. +![LeakLens scan path](Images/auto-populated.png) +*The Path field filled with the UNC path of the selected share, ready to scan.* + +6. Click **▶ Start Scan** --- -## 3. Running the automated tests +### 4.2 Guest / anonymous access -### Setup +Leave **Username** and **Password** blank in the modal. LeakLens sends a null session which the Samba server maps to the guest account when the share allows it. -```bash -# Install pytest (only needed once) -pip install -r requirements-dev.txt -``` +Most open internal file shares (and the included test server) work this way with no configuration needed. -### Run all tests +--- -```bash -python -m pytest tests/ -v -``` +### 4.3 Authenticated access + +Fill in credentials before clicking Discover Shares: + +| Field | Example | +|---|---| +| Username | `DOMAIN\jsmith` or just `jsmith` | +| Password | the account password | +| Domain | `CORP` (optional — include for NTLM domain authentication) | -Expected output: +Credentials flow only to the LeakLens backend running locally and are never written to disk. + +--- + +### 4.4 Non-standard ports + +Append `:port` to the host when the server is not on the standard SMB port 445: ``` -tests/test_api.py::test_index_returns_html PASSED -tests/test_api.py::test_status_not_scanning PASSED -... -tests/test_engine.py::test_scan_content_detects_aws_key PASSED -... -tests/test_patterns.py::test_plaintext_password[...] PASSED -... -======================== 149 passed in 0.2s ============================== +192.168.1.10:4445 +fileserver01:4450 ``` -### Run a single module +The port is parsed automatically and applied to both share enumeration and the subsequent file scan — no other configuration is needed. -```bash -python -m pytest tests/test_patterns.py -v # pattern regex tests -python -m pytest tests/test_engine.py -v # engine + content helpers -python -m pytest tests/test_api.py -v # Flask endpoint tests -``` +--- + +## 5. Understanding Your Results + +### 5.1 Risk levels and confidence scores -### What each module covers +Every finding is scored 1–10 based on how certain the match is, then grouped into three risk levels: -| Module | What it tests | -|--------|---------------| -| `test_patterns.py` | Every pattern has at least one positive and one negative string. Structural checks: all patterns compile, `risk` field is consistent with `confidence`, no missing keys. | -| `test_engine.py` | `scan_content()` detects known patterns and respects confidence reduction in docs paths. `build_finding()` assembles correct finding dicts. `is_placeholder_match()` and `is_docs_path()` behave correctly. `load_suppressions()` and `is_suppressed()` handle global and per-pattern rules. | -| `test_api.py` | All Flask endpoints return correct status codes for happy paths and error cases. Path traversal in `GET /api/reports/` is blocked. `scan_id` injection is rejected. Input validation rejects invalid hosts, page types, and missing required params. | +| Level | Score | What it typically means | +|---|---|---| +| 🔴 HIGH | 8–10 | Near-certain credential — private key, plaintext password, AWS key, NTLM hash | +| 🟡 MEDIUM | 5–7 | Probable credential — API key pattern, PSCredential, suspicious filename | +| 🟢 LOW | 1–4 | Low signal — generic hash strings, very broad patterns | + +The **Risk Breakdown** panel in the sidebar tracks live counts and proportional bars for each level as the scan progresses. + +![LeakLens scan risk](Images/risk.png) +*Risk Breakdown panel mid-scan — HIGH and MEDIUM findings already visible while the scan is still running.* + +Two automatic confidence reductions apply: + +- **Docs / examples paths** — files found inside directories named `docs`, `examples`, `test`, `sample`, or similar have their confidence reduced by 3 points +- **Hash-only findings** — if a file matches only generic hash patterns (MD5, SHA1, etc.) it is demoted to LOW with the note: *"Hash strings detected — verify these are credential hashes and not integrity checksums."* --- -## 4. Running the test server with dirty files +### 5.2 The detail drawer -The `testserver/` directory contains a Docker/Podman Samba image pre-loaded with 8 intentionally unsafe files. Use it to verify that LeakLens finds what it should — without touching real infrastructure. +Click any row in the findings table to open the detail drawer. -### Prerequisites +![LeakLens scan evidence](Images/Evidence.png) +*Detail drawer showing the matched pattern, line number, exact matched line, file metadata, and remediation advice.* -Docker or Podman. The start scripts detect whichever is available. No Docker Compose needed. +The drawer shows: -### 4.1 — Start the container +- **Pattern name and confidence score** for each pattern matched in this file +- **Line number and exact matched line** — confirm findings without opening the file +- **File metadata** — size, last modified, last accessed +- **SMB context** (UNC paths only) — server, share name, and relative path within the share +- **Remediation advice** — specific to the match type and referencing the exact share or path -```bash -# Linux / macOS -chmod +x testserver/start-testserver.sh -./testserver/start-testserver.sh +Lines are truncated at 120 characters. Findings triggered by file type (`.pfx`, `.kdbx`, etc.) or filename (`id_rsa`, `.env`) show an explanatory note in place of a snippet. -# Windows -testserver\start-testserver.bat -``` +--- + +### 5.3 Filtering and searching + +Use the toolbar above the findings table to narrow results: + +- **All / High / Medium / Low** — filter by risk level +- **Search box** — substring filter on filename or full path (case-insensitive, updates live) + +![LeakLens scan filter](Images/filter.png) +*Findings table filtered to HIGH risk only with a path search active — the badge on the Findings tab still shows the total unfiltered count.* -The container starts a Samba server on **port 4445** (non-standard to avoid conflict with the Windows SMB service on port 445). The share is read-only and requires no credentials. +--- + +## 6. Working with Reports + +Every completed scan is saved automatically to the `reports/` directory: + +- **SQLite database** — `LeakLens_.db` — primary store, supports fast paginated queries +- **JSON report** — `LeakLens_.json` — human-readable, portable + +Open the **Reports** tab to see all saved scans. -You should see: +![LeakLens scan reports](Images/Reports.png) +*Reports tab showing completed scans listed by timestamp with their total finding counts.* + +Click any report to reload its findings into the main view with full filtering, search, and drawer support. The SQLite backend means even scans with tens of thousands of findings load quickly through server-side pagination. + +**SARIF export** — reload a report into any SAST dashboard (GitHub Advanced Security, Azure DevOps) via the SARIF 2.1.0 endpoint: ``` - [OK] Using docker - [*] Building test server image... - [OK] Image built - [*] Starting test file server... - Test server running on port 4445! +GET /api/reports/LeakLens_20240101_120000.json?format=sarif ``` -### 4.2 — Mount the share +--- -LeakLens connects to SMB on port 445 by default, so you need to mount the share at a local path first, then scan that path. +## 7. Resuming Interrupted Scans -**Linux:** +If a scan is stopped or crashes, LeakLens saves a checkpoint in `reports/`. The checkpoint records which files were already processed. -```bash -sudo mkdir -p /mnt/testshare -sudo mount -t cifs //127.0.0.1/testshare /mnt/testshare \ - -o port=4445,guest,vers=2.0 -``` +To resume: + +1. Set the **Path** to the same root as the previous scan +2. Check **Resume interrupted scan** +3. Click **▶ Start Scan** + +Already-processed files are skipped and the scan continues from the last checkpoint. All prior findings are preserved in the SQLite database and visible immediately. + +> Checkpoints are keyed to the scan root path. Changing the path always starts a fresh scan. + +--- + +## 8. Test Server Walkthrough + +A Samba container pre-loaded with 8 intentionally dirty files is included. Use it to verify LeakLens is working correctly without touching real infrastructure. + +**Requirements:** Docker or Podman. The start scripts detect whichever is available automatically. + +### Start the container -If `cifs-utils` is not installed: ```bash -sudo apt install cifs-utils # Debian/Ubuntu -sudo dnf install cifs-utils # Fedora/RHEL +# Linux / macOS +./testserver/start-testserver.sh + +# Windows +testserver\start-testserver.bat ``` -**macOS:** +The container starts Samba on port **4445**. You should see: ``` -Finder → Go → Connect to Server → smb://127.0.0.1:4445/testshare -``` -Or from Terminal: -```bash -open 'smb://127.0.0.1:4445/testshare' +[OK] Using docker +[*] Building test server image... +[OK] Image built +[*] Starting test file server... +Test server running on port 4445! ``` -The share mounts under `/Volumes/testshare` (Finder shows it in the sidebar). -**Windows:** +### Connect LeakLens to the test server -```cmd -net use Z: \\127.0.0.1\testshare "" /user:guest /port:4445 -``` +1. Click **⬡ SMB: Browse Shares & Credentials** in the Scan Configuration panel +2. Enter `127.0.0.1:4445` in **Server / Host** +3. Leave Username and Password blank (the share allows guest access) +4. Click **⬡ Discover Shares** -### 4.3 — Scan it -Open LeakLens at **http://localhost:3000**, enter the local mount path, leave credentials blank, and click **Start Scan**. +![LeakLens smb enumartion](Images/SMB-Enumerated.png) +*Discover Shares result against the test server — testshare appears in the list.* -| Platform | Scan path | -|----------|-----------| -| Linux | `/mnt/testshare` | -| macOS | `/Volumes/testshare` | -| Windows | `Z:\` | +5. Click **testshare** — the Path field fills with `\\127.0.0.1\testshare` +6. Click **▶ Start Scan** -### 4.4 — Expected findings +### Expected findings All credentials in the test files are completely fake and exist only to trigger detection. | File | Expected patterns | -|------|------------------| +|---|---| | `deploy.ps1` | Plaintext Password, Connection String, PowerShell SecureString, Hardcoded PSCredential | -| `app.config` | Plaintext Password, Generic API Key/Token, Bearer Token | -| `passwords.txt` | NTLM Hash, Plaintext Password | -| `.env` | Plaintext Password, AWS Access Key, Base64 Credential, Stripe API Key | +| `app.config` | Plaintext Password, Generic API Key/Token | +| `passwords.txt` | NTLM Hash | +| `.env` | AWS Access Key, Plaintext Password, Base64 Credential, Stripe API Key | | `nightly-backup.bat` | Net Use Credential | | `db_maintenance.py` | Plaintext Password, Bearer Token | -| `id_rsa` | Private Key Header, Risky Filename (flagged by name) | +| `id_rsa` | Private Key Header | | `project-notes.md` | *(clean — no findings expected)* | -All 7 files with findings should appear in LeakLens. `project-notes.md` should produce no finding row. +![LeakLens scanned](Images/Home-scanned.png) +*Completed scan against the test server — 8 files scanned, findings across HIGH and MEDIUM risk levels.* -### 4.5 — Stop the container +### Stop the container ```bash -./testserver/stop-testserver.sh # Linux / macOS -testserver\stop-testserver.bat # Windows +./testserver/stop-testserver.sh # Linux / macOS +testserver\stop-testserver.bat # Windows +``` + +The container is stateless — stopping it removes all data. Restart it any time with the start script. + +--- + +## 9. Advanced Features + +### 9.1 Tuning worker threads + +The **Workers** input (1–16, default 8) controls how many files are analysed in parallel. + +| Scenario | Recommended | +|---|---| +| Local fast SSD | 12–16 | +| Local HDD | 6–8 | +| SMB over gigabit LAN | 8–12 | +| SMB over slow WAN / VPN | 4–6 | +| Very large shares (>100 k files) | 8 (avoid queue saturation) | + +The live files/second rate in the progress bar is the best guide — increase workers until the rate plateaus. That plateau is the I/O ceiling of the target. + +--- + +### 9.2 Custom detection patterns + +Add your own patterns without modifying source code. Create a YAML file: + +```yaml +- id: my_internal_token + name: "Internal API Token" + regex: 'INT-[A-Z0-9]{32}' + confidence: 9 + description: "Internal service token format" ``` -Unmount the share on Linux before stopping (or after): +**Via the UI** — enter the path to your YAML file in the **Custom Patterns File** input in the Scan Configuration panel. +**Via the CLI:** ```bash -sudo umount /mnt/testshare +python3 leaklens.py scan --path \\server\share --patterns my-patterns.yaml ``` -The container is stateless — stopping removes all state. Restart it any time with the start script. +**Via the API:** +```json +POST /api/scan +{ "scanPath": "\\\\server\\share", "patternsFile": "/path/to/my-patterns.yaml" } +``` + +Required fields per pattern: `id`, `name`, `regex`, `confidence` (1–10). Optional: `description`. --- -## 5. Suppression — .leaklensignore +### 9.3 Suppressing false positives (`.leaklensignore`) -Drop a `.leaklensignore` file in your **scan root directory** to silence known-safe paths. The format mirrors `.gitignore` with an optional `[pattern_id]` section syntax for per-pattern rules. +Drop a `.leaklensignore` file in the scan root to silence known-safe paths. The format mirrors `.gitignore` with an optional `[pattern_id]` section syntax for per-pattern suppression: ``` -# Suppress all findings under these paths (global rules) +# Suppress all findings under these paths archive/** legacy/** *.example.config -# Suppress only the md5_hash pattern in checksum files +# Suppress the md5_hash pattern only inside checksum directories [md5_hash] checksums/** *.md5 -# Suppress aws_access_key matches in documentation +# Suppress aws_access_key matches only in documentation [aws_access_key] docs/** ``` -### Pattern ID reference +The `[pattern_id]` syntax lets you silence a noisy pattern in known-safe locations while keeping it active everywhere else. -Use these IDs in `[pattern_id]` section headers: +**Pattern ID reference:** -| ID | Display name | -|----|-------------| +| ID | Pattern name | +|---|---| | `plaintext_password` | Plaintext Password | | `connection_string` | Connection String | | `ntlm_hash` | NTLM Hash | -| `md5_hash` | MD5 Hash | -| `sha1_hash` | SHA1 Hash | -| `sha256_hash` | SHA256 Hash | -| `sha512_hash` | SHA512 Hash | | `bcrypt_hash` | Bcrypt Hash | | `base64_credential` | Base64 Credential | | `aws_access_key` | AWS Access Key | +| `github_pat` | GitHub Personal Access Token | +| `gitlab_pat` | GitLab Personal Access Token | +| `stripe_key` | Stripe API Key | +| `slack_token` | Slack Token | +| `sendgrid_key` | SendGrid API Key | +| `azure_client_secret` | Azure Client Secret | +| `azure_storage_key` | Azure Storage Account Key | +| `dpapi_blob` | DPAPI Encrypted Blob | | `generic_api_key` | Generic API Key/Token | | `bearer_token_value` | Bearer Token | | `private_key_header` | Private Key Header | @@ -280,13 +417,120 @@ Use these IDs in `[pattern_id]` section headers: | `ps_secure_string` | PowerShell SecureString | | `hardcoded_pscredential` | Hardcoded PSCredential | | `sql_sa_password` | SQL sa Password | -| `github_pat` | GitHub Personal Access Token | -| `gitlab_pat` | GitLab Personal Access Token | -| `azure_client_secret` | Azure Client Secret | -| `azure_storage_key` | Azure Storage Account Key | -| `dpapi_blob` | DPAPI Encrypted Blob | -| `stripe_key` | Stripe API Key | -| `slack_token` | Slack Token | -| `sendgrid_key` | SendGrid API Key | +| `md5_hash` | MD5 Hash | +| `sha1_hash` | SHA1 Hash | +| `sha256_hash` | SHA256 Hash | +| `sha512_hash` | SHA512 Hash | + +The full list with regex and confidence values is in `scanner/patterns.py`. + +--- + +### 9.4 Webhooks and notifications + +LeakLens can POST a JSON summary to a webhook URL when a scan completes. + +**Via the UI** — open the **Notifications** panel (click the header to expand), enter your **Webhook URL** and optionally a **Webhook Secret** for HMAC-SHA256 request signing. + +![LeakLens notification](Images/notification.png) +*Expanded Notifications panel with a webhook URL and secret entered.* + +**Via the CLI:** +```bash +python3 leaklens.py scan \ + --path \\server\share \ + --webhook-url https://hooks.example.com/leaklens \ + --webhook-secret mysecret +``` + +The payload includes the scan summary: total files scanned, finding counts by risk level, scan ID, and timestamp. When a secret is set, a `X-LeakLens-Signature` header carrying the HMAC-SHA256 digest lets your endpoint verify the request origin. + +--- + +### 9.5 CLI usage + +LeakLens can run entirely headless without the browser UI: + +```bash +python3 leaklens.py scan \ + --path "\\\\fileserver01\\IT" \ + --output report.json \ + --format json \ + --workers 8 \ + --max-size 20 \ + --patterns custom.yaml \ + --resume +``` + +**SMB with credentials and non-standard port:** +```bash +python3 leaklens.py scan \ + --path "\\\\192.168.1.10\\share" \ + --smb-port 4445 \ + --username "DOMAIN\jsmith" \ + --password "hunter2" \ + --domain CORP +``` + +**SARIF output for CI pipelines:** +```bash +python3 leaklens.py scan \ + --path /srv/files \ + --output results.sarif \ + --format sarif +``` + +**All flags:** + +| Flag | Default | Description | +|---|---|---| +| `--path` | — | Path to scan (required) | +| `--output` | stdout | Output file | +| `--format` | `json` | `json` or `sarif` | +| `--workers` | `8` | Parallel worker threads (1–16) | +| `--max-size` | `10` | Max file size in MB | +| `--patterns` | — | Path to custom patterns YAML | +| `--resume` | false | Resume from checkpoint | +| `--smb-port` | `445` | SMB port | +| `--username` | — | SMB username | +| `--password` | — | SMB password | +| `--domain` | — | SMB domain | +| `--webhook-url` | — | POST summary here on completion | +| `--webhook-secret` | — | HMAC-SHA256 signing secret | + +--- + +## 10. Running the Automated Tests + +Install dev dependencies, then run pytest: + +```bash +pip install -r requirements-dev.txt +python -m pytest tests/ -v +``` + +Expected output ends with all tests passing: + +``` +======================== 170 passed in X.Xs ============================== +``` + +**Run individual modules:** + +```bash +python -m pytest tests/test_patterns.py -v # regex positive/negative tests +python -m pytest tests/test_engine.py -v # scan_content, build_finding, suppression +python -m pytest tests/test_api.py -v # Flask endpoint tests +python -m pytest tests/test_smb.py -v # mocked SMB helpers +python -m pytest tests/test_sarif.py -v # SARIF structure and mapping +``` + +**What each module covers:** -The full list is also in `scanner/patterns.py` — each entry has an `"id"` field. +| Module | Scope | +|---|---| +| `test_patterns.py` | Every pattern has at least one positive and one negative string; structural checks (compile, risk/confidence consistency, required keys); custom pattern loading from YAML | +| `test_engine.py` | `scan_content()` detects known patterns; confidence reduction in docs paths; `build_finding()` dict shape; `is_placeholder_match()` and `is_docs_path()` | +| `test_api.py` | All endpoints: correct status codes, path traversal blocked on `/api/reports/`, `scan_id` injection rejected, invalid host/port/page rejected | +| `test_smb.py` | `register_session`, `walk_smb`, `read_smb_file`, `list_shares` — all mocked, no live server needed | +| `test_sarif.py` | SARIF 2.1.0 structure, one result per match, rules deduped, correct severity mapping | diff --git a/frontend/Assets/app.js b/frontend/Assets/app.js index 827514d..83df412 100644 --- a/frontend/Assets/app.js +++ b/frontend/Assets/app.js @@ -10,11 +10,47 @@ const ROW_H = 44; // px — must match tbody tr rendered height const VS_OVERSCAN = 5; // extra rows rendered above/below viewport - // ── Path input — show/hide SMB panel ────────────────────────────────────── + // ── SMB modal ────────────────────────────────────────────────────────────── + function openSmbModal() { + document.getElementById('smbModalOverlay').classList.add('open'); + document.getElementById('smbModal').classList.add('open'); + // Pre-fill host from current scanPath if it looks like a UNC path + const path = document.getElementById('scanPath').value.trim(); + if ((path.startsWith('\\\\') || path.startsWith('//')) && !document.getElementById('smbHost').value) { + const host = path.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0]; + if (host) document.getElementById('smbHost').value = host; + } + } + + function closeSmbModal() { + document.getElementById('smbModalOverlay').classList.remove('open'); + document.getElementById('smbModal').classList.remove('open'); + } + + // ── Path input — sync smbHost when UNC is typed directly ───────────────── function onPathInput() { const v = document.getElementById('scanPath').value; const isUnc = v.startsWith('\\\\') || v.startsWith('//'); - document.getElementById('smbPanel').style.display = isUnc ? 'block' : 'none'; + if (isUnc) { + const host = v.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0]; + const hostEl = document.getElementById('smbHost'); + if (host && !hostEl.value) hostEl.value = host; + } + } + + // ── SMB host input — normalise and sync scanPath ────────────────────────── + function onSmbHostInput() { + let raw = document.getElementById('smbHost').value.trim(); + // If user pasted a full UNC path, extract just the host + if (raw.startsWith('\\\\') || raw.startsWith('//')) { + raw = raw.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0]; + document.getElementById('smbHost').value = raw; + } + document.getElementById('sharesList').innerHTML = ''; + if (raw) { + const hostOnly = raw.split(':')[0]; + document.getElementById('scanPath').value = '\\\\' + hostOnly; + } } // ── Tab switching ────────────────────────────────────────────────────────── @@ -49,6 +85,15 @@ const password = document.getElementById('smbPassword')?.value || ''; const domain = document.getElementById('smbDomain')?.value.trim() || ''; + // Parse port from the Server/Host field (e.g. "127.0.0.1:4445") + let smbPort = 445; + const rawHost = document.getElementById('smbHost')?.value.trim() || ''; + const ci = rawHost.lastIndexOf(':'); + if (ci > 0) { + const p = parseInt(rawHost.slice(ci + 1), 10); + if (p > 0 && p < 65536) smbPort = p; + } + if (!scanPath) { alert('Please enter a file share path.'); return; } // Reset @@ -72,7 +117,7 @@ response = await fetch(`${API}/scan`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ scanPath, maxFileSizeMB, workers, resume, username, password, domain }), + body: JSON.stringify({ scanPath, maxFileSizeMB, workers, resume, username, password, domain, smbPort }), }); } catch (e) { alert('Cannot reach backend. Is it running?\n' + e.message); @@ -179,8 +224,22 @@ // ── SMB: Discover shares ─────────────────────────────────────────────────── async function discoverShares() { - const path = document.getElementById('scanPath').value.trim(); - const host = path.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0]; + let raw = document.getElementById('smbHost').value.trim() + || (() => { + const p = document.getElementById('scanPath').value.trim(); + return p.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0]; + })(); + // Normalise if someone pasted a full UNC path into the host field + if (raw.startsWith('\\\\') || raw.startsWith('//')) { + raw = raw.replace(/^\\\\|^\/\//, '').split(/[\\\/]/)[0]; + } + // Parse optional :port suffix (e.g. 127.0.0.1:4445) + let host = raw, port = 445; + const colonIdx = raw.lastIndexOf(':'); + if (colonIdx > 0) { + const maybePort = parseInt(raw.slice(colonIdx + 1), 10); + if (maybePort > 0 && maybePort < 65536) { port = maybePort; host = raw.slice(0, colonIdx); } + } if (!host) { alert('Enter a server hostname or IP first.'); return; } const btn = document.getElementById('discoverBtn'); @@ -193,6 +252,7 @@ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ host, + port, username: document.getElementById('smbUsername').value.trim(), password: document.getElementById('smbPassword').value, domain: document.getElementById('smbDomain').value.trim(), @@ -207,10 +267,16 @@ container.innerHTML = '

No shares found.

'; return; } - container.innerHTML = '
' + - data.shares.map(s => - `` - ).join('') + '
'; + + const adminShares = data.shares.filter(s => s.endsWith('$')); + const userShares = data.shares.filter(s => !s.endsWith('$')); + const renderItem = s => + ``; + container.innerHTML = + `
${data.shares.length} share${data.shares.length !== 1 ? 's' : ''} found on ${escHtml(host)}
` + + '
' + + [...userShares, ...adminShares].map(renderItem).join('') + + '
'; } catch (e) { alert('Share discovery failed: ' + e.message); } finally { @@ -219,8 +285,12 @@ } } - function selectShare(host, share) { + function selectShare(host, share, el) { document.getElementById('scanPath').value = `\\\\${host}\\${share}`; + document.querySelectorAll('.share-item').forEach(e => e.classList.remove('selected')); + if (el) el.classList.add('selected'); + // Close modal after a brief visual confirmation + setTimeout(closeSmbModal, 300); } // ── Status ───────────────────────────────────────────────────────────────── diff --git a/frontend/Assets/styles.css b/frontend/Assets/styles.css index d1f7ed2..eb24a33 100644 --- a/frontend/Assets/styles.css +++ b/frontend/Assets/styles.css @@ -261,15 +261,28 @@ } .btn-ghost:disabled { opacity: 0.3; cursor: not-allowed; } + /* 2-column grid for compact settings (Max Size + Workers) */ + .config-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-top: 12px; + margin-bottom: 0; + } + .config-row label { margin-bottom: 4px; } + /* SMB shares dropdown */ .shares-list { margin-top: 8px; background: var(--bg); border: 1px solid var(--border2); border-radius: 4px; - overflow: hidden; + max-height: 130px; + overflow-y: auto; } .share-item { + display: flex; + align-items: center; padding: 7px 10px; font-size: 11px; cursor: pointer; @@ -280,6 +293,32 @@ .share-item:last-child { border-bottom: none; } .share-item:hover { background: rgba(0,212,255,0.06); color: var(--accent); } + /* Selected share highlight */ + .share-item.selected { + background: rgba(0,212,255,0.10); + color: var(--accent); + border-left: 2px solid var(--accent); + } + + /* Administrative shares (ADMIN$, C$, IPC$) — visible but subdued */ + .share-item.admin-share { color: var(--text-dim); font-style: italic; } + .share-item.admin-share:hover { color: var(--text); } + .share-item.admin-share.selected { color: var(--accent); font-style: normal; } + + /* Header above share list */ + .shares-header { + font-size: 10px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.1em; + padding: 8px 10px 4px; + } + .shares-header strong { color: var(--text); font-style: normal; } + + /* Icon and name columns in share items */ + .share-icon { color: var(--accent); margin-right: 6px; font-size: 9px; flex-shrink: 0; } + .share-name { flex: 1; } + /* ── Stats ── */ .stats-grid { display: grid; @@ -781,6 +820,80 @@ } .btn-sm:hover { color: var(--accent); border-color: var(--accent2); } + /* ── SMB Modal ── */ + .smb-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.7); + z-index: 300; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + } + .smb-modal-overlay.open { opacity: 1; pointer-events: all; } + + .smb-modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.96); + width: 420px; + max-width: calc(100vw - 40px); + max-height: 85vh; + background: var(--bg2); + border: 1px solid var(--border2); + border-radius: 8px; + z-index: 301; + overflow-y: auto; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s, transform 0.2s cubic-bezier(0.22,1,0.36,1); + } + .smb-modal.open { + opacity: 1; + pointer-events: all; + transform: translate(-50%, -50%) scale(1); + } + + .smb-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border); + font-size: 10px; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--accent); + position: sticky; + top: 0; + background: var(--bg2); + z-index: 1; + } + .smb-modal-close { + background: none; + border: 1px solid var(--border); + color: var(--text-dim); + cursor: pointer; + font-size: 11px; + padding: 3px 8px; + border-radius: 3px; + font-family: var(--mono); + transition: color 0.15s, border-color 0.15s; + } + .smb-modal-close:hover { color: var(--text); border-color: var(--border2); } + + .smb-modal-body { padding: 16px; } + + /* 2-column credential row inside modal */ + .cred-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-bottom: 0; + } + .cred-row label { margin-bottom: 4px; } + /* ── Scrollbar ── */ ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } diff --git a/frontend/index.html b/frontend/index.html index 2f67814..baa4892 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -37,16 +37,25 @@

LeakLens

⬡ Scan Configuration
- - + + - - + - - +
+
+ + +
+
+ + +
+
-
- - -
⬡ Statistics
@@ -198,6 +190,39 @@

LeakLens

+ +
+
+
+ ⬡ SMB Share Browser + +
+
+ + + +
+
+ + +
+
+ + +
+
+ + + + + + +
+
+
+ diff --git a/leaklens.py b/leaklens.py index 4cfdd39..fd7f903 100644 --- a/leaklens.py +++ b/leaklens.py @@ -76,6 +76,7 @@ def scan(): max_file_size_mb = int(data.get("maxFileSizeMB", 10)) workers = max(1, min(16, int(data.get("workers", 8)))) resume = bool(data.get("resume", False)) + smb_port = max(1, min(65535, int(data.get("smbPort", 445)))) username, password, domain = _extract_smb_creds(data) if not scan_path: @@ -103,6 +104,7 @@ def run(): reports_dir=REPORTS_DIR, workers=workers, resume=resume, + smb_port=smb_port, ): result_queue.put(event) except Exception: @@ -166,11 +168,19 @@ def list_shares(): if not _VALID_HOST.match(host): return jsonify({"error": "Invalid host format."}), 400 + try: + port = int(data.get("port", 445)) + if not (1 <= port <= 65535): + raise ValueError + except (ValueError, TypeError): + return jsonify({"error": "Invalid port."}), 400 + username, password, domain = _extract_smb_creds(data) try: from scanner.smb import list_shares as smb_list_shares - shares = smb_list_shares(host, username=username, password=password, domain=domain) + shares = smb_list_shares(host, username=username, password=password, + domain=domain, port=port) return jsonify({"shares": shares}) except RuntimeError as e: return jsonify({"error": str(e)}), 503 diff --git a/scanner/engine.py b/scanner/engine.py index d21a6c9..7ac81c9 100644 --- a/scanner/engine.py +++ b/scanner/engine.py @@ -118,6 +118,7 @@ def _scan_smb( username: str = None, password: str = None, domain: str = None, + smb_port: int = 445, ) -> Generator: """Walk an SMB share and yield file items or error/log events.""" if not SMB_AVAILABLE: @@ -130,12 +131,12 @@ def _scan_smb( return try: - register_session(server, username=username, password=password, domain=domain) + register_session(server, username=username, password=password, domain=domain, port=smb_port) except Exception as e: yield ("error", f"SMB authentication failed for {server}: {e}") return - for item in walk_smb(unc_root, stop_event=stop_event): + for item in walk_smb(unc_root, stop_event=stop_event, port=smb_port): if stop_event.is_set(): return if item[0] == "__error__": @@ -187,6 +188,7 @@ def scan_path( reports_dir: str = None, workers: int = 8, resume: bool = False, + smb_port: int = 445, ) -> Generator: """ Scan root (local path or UNC) and yield JSON-serialisable event dicts. @@ -311,7 +313,7 @@ def _analyse_item(item: tuple) -> tuple: pass elif ext in TARGET_EXTENSIONS or (risky_name and ext == ""): if size_bytes <= max_size: - content = read_smb_file(smb_path, max_size) + content = read_smb_file(smb_path, max_size, port=smb_port) if content: matched = scan_content(content, smb_path) elif not risky_name: @@ -364,7 +366,7 @@ def _walk() -> None: try: if is_smb: - for ev in _scan_smb(root, max_size, stop_event, username, password, domain): + for ev in _scan_smb(root, max_size, stop_event, username, password, domain, smb_port): if stop_event.is_set(): break if ev[0] == "error": diff --git a/scanner/smb.py b/scanner/smb.py index 53fba17..f063502 100644 --- a/scanner/smb.py +++ b/scanner/smb.py @@ -36,34 +36,103 @@ def parse_unc(path: str) -> tuple[str, str, str]: return server, share, rel -def register_session(server: str, username: str = None, password: str = None, domain: str = None): +def register_session(server: str, username: str = None, password: str = None, + domain: str = None, port: int = 445): """Register an SMB session for the given server.""" if not SMB_AVAILABLE: raise RuntimeError("smbprotocol is not installed. Run: pip install smbprotocol") - kwargs = {"connection_timeout": 30} if username: - kwargs["username"] = username - if password is not None: - kwargs["password"] = password - if domain: - kwargs["auth_protocol"] = "ntlm" - kwargs["domain"] = domain + kwargs = { + "connection_timeout": 30, + "port": port, + "username": username, + } + if password is not None: + kwargs["password"] = password + if domain: + kwargs["auth_protocol"] = "ntlm" + kwargs["domain"] = domain + else: + # Null/anonymous session: use NTLM with a dummy username so that + # Samba's "map to guest = Bad User" maps it to the guest account. + # require_signing must be False because guest sessions have no signing key. + smbclient.ClientConfig(require_secure_negotiate=False) + kwargs = { + "connection_timeout": 30, + "port": port, + "username": "nobody", + "password": password if password is not None else "", + "auth_protocol": "ntlm", + "require_signing": False, + } smbclient.register_session(server, **kwargs) -def list_shares(host: str, username: str = None, password: str = None, domain: str = None) -> list[str]: - """List visible shares on the given SMB host.""" - if not SMB_AVAILABLE: - raise RuntimeError("smbprotocol is not installed. Run: pip install smbprotocol") +def list_shares(host: str, username: str = None, password: str = None, + domain: str = None, port: int = 445) -> list[str]: + """ + List visible shares on the given SMB host using the system smbclient binary. + smbprotocol does not expose a share-enumeration API, so we shell out to + the Samba smbclient tool which handles null sessions reliably. + """ + import subprocess + import shutil - register_session(host, username=username, password=password, domain=domain) - shares = smbclient.listshares(host) - return list(shares) + if not shutil.which("smbclient"): + raise RuntimeError( + "smbclient binary not found. Install samba-client (e.g. apt install smbclient)." + ) + cmd = ["smbclient", "-L", f"//{host}", "-p", str(port)] -def walk_smb(unc_root: str, stop_event=None): + if username: + cred = username + if password is not None: + cred += f"%{password}" + cmd.extend(["-U", cred]) + if domain: + cmd.extend(["-W", domain]) + else: + cmd.extend(["-N", "-U", "%"]) # null / anonymous session + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, timeout=15 + ) + except subprocess.TimeoutExpired: + raise RuntimeError("Connection to host timed out.") + + output = result.stdout + result.stderr + + shares = [] + in_list = False + for line in output.splitlines(): + if "Sharename" in line and "Type" in line: + in_list = True + continue + if not in_list: + continue + stripped = line.strip() + if not stripped or stripped.startswith("-"): + continue + # Lines past the share list (workgroup section, reconnect notice, etc.) + if not line.startswith("\t") and not line.startswith(" "): + break + parts = stripped.split() + if parts: + shares.append(parts[0]) + + if not shares and result.returncode != 0: + # Surface the actual smbclient error message + msg = output.strip().splitlines()[-1] if output.strip() else "Unknown error" + raise RuntimeError(msg) + + return shares + + +def walk_smb(unc_root: str, stop_event=None, port: int = 445): """ Recursively walk an SMB share. Yields (smb_path, entry_name, smb_stat) for each file, @@ -77,7 +146,7 @@ def _recurse(path): if stop_event and stop_event.is_set(): return try: - for entry in smbclient.scandir(path): + for entry in smbclient.scandir(path, port=port): if stop_event and stop_event.is_set(): return full = f"{path}\\{entry.name}" @@ -85,7 +154,9 @@ def _recurse(path): yield from _recurse(full) elif entry.is_file(follow_symlinks=False): try: - stat = entry.stat() + # entry.stat() lazily fetches on default port 445; + # use smbclient.stat() with the explicit port instead. + stat = smbclient.stat(full, port=port) yield full, entry.name, stat except Exception as e: yield ("__error__", full, str(e)) @@ -95,12 +166,12 @@ def _recurse(path): yield from _recurse(normalize_unc(unc_root)) -def read_smb_file(smb_path: str, max_size: int) -> str | None: +def read_smb_file(smb_path: str, max_size: int, port: int = 445) -> str | None: """Read the content of a file over SMB. Returns None on error or if too large.""" if not SMB_AVAILABLE: return None try: - with smbclient.open_file(smb_path, mode="rb") as f: + with smbclient.open_file(smb_path, mode="rb", port=port) as f: # Check size first f.seek(0, 2) size = f.tell() diff --git a/testserver/Dockerfile b/testserver/Dockerfile index c27baae..d055646 100644 --- a/testserver/Dockerfile +++ b/testserver/Dockerfile @@ -14,4 +14,4 @@ RUN chmod -R 755 /shares/testshare && \ EXPOSE 445 139 -CMD ["smbd", "--foreground", "--no-process-group", "--log-stdout"] +CMD ["smbd", "--foreground", "--no-process-group", "--debug-stdout"] diff --git a/testserver/README.md b/testserver/README.md index a7a3e8b..864af30 100644 --- a/testserver/README.md +++ b/testserver/README.md @@ -20,12 +20,6 @@ start-testserver.bat ← start stop-testserver.bat ← stop ``` -After starting, mount the share and scan the mapped drive: -```cmd -net use Z: \\127.0.0.1\testshare /user:guest "" /p:no /port:4445 -``` -Then point LeakLens at `Z:\` - ### Linux / macOS ```bash chmod +x start-testserver.sh stop-testserver.sh @@ -33,15 +27,28 @@ chmod +x start-testserver.sh stop-testserver.sh ./stop-testserver.sh ``` -After starting, mount the share: -```bash -# Linux -sudo mount -t cifs //127.0.0.1/testshare /mnt/testshare -o port=4445,guest,vers=2.0 +--- + +## Connecting LeakLens to the test server -# macOS -open 'smb://127.0.0.1:4445/testshare' +No mounting required. LeakLens connects directly over SMB using `127.0.0.1:4445`. + +### Via the UI (recommended) + +1. Start LeakLens (`start.sh` / `start.bat` or `python3 leaklens.py`) +2. Open **http://localhost:3000** in your browser +3. Click **⬡ SMB: Browse Shares & Credentials** in the Scan Configuration panel +4. Enter `127.0.0.1:4445` in the **Server / Host** field +5. Leave Username and Password blank (the share allows guest/anonymous access) +6. Click **⬡ Discover Shares** — `testshare` appears in the list +7. Click **testshare** to select it — the Path field is populated automatically +8. Click **Start Scan** + +### Via the CLI + +```bash +python3 leaklens.py scan --path "\\\\127.0.0.1\\testshare" --smb-port 4445 ``` -Then point LeakLens at the mounted path. --- diff --git a/testserver/smb.conf b/testserver/smb.conf index dad80f5..df49333 100644 --- a/testserver/smb.conf +++ b/testserver/smb.conf @@ -3,13 +3,11 @@ server string = LeakLens Test File Server netbios name = TESTSERVER security = user - map to guest = bad user - dns proxy = no - # Use non-standard port to avoid conflict with Windows SMB on port 445 - smb ports = 4445 - # Allow older clients + map to guest = Bad User + guest account = nobody ntlm auth = yes - lanman auth = yes + smb ports = 445 + server min protocol = SMB2 [testshare] path = /shares/testshare diff --git a/testserver/start-testserver.sh b/testserver/start-testserver.sh old mode 100644 new mode 100755 index f7b4ebd..f4cb771 --- a/testserver/start-testserver.sh +++ b/testserver/start-testserver.sh @@ -39,7 +39,7 @@ echo "" echo " [*] Starting test file server..." $RUNTIME run -d \ --name leaklens-testserver \ - -p 4445:4445 \ + -p 4445:445 \ leaklens-testserver echo "" @@ -49,8 +49,9 @@ echo "" echo " Mount the share, then point LeakLens at the mount path:" echo "" echo " Linux:" +echo " sudo mkdir -p /mnt/testshare" echo " sudo mount -t cifs //127.0.0.1/testshare /mnt/testshare \\" -echo " -o port=4445,guest,vers=2.0" +echo " -o port=4445,guest,vers=3.0" echo " Then scan: /mnt/testshare" echo "" echo " macOS:" diff --git a/testserver/stop-testserver.sh b/testserver/stop-testserver.sh old mode 100644 new mode 100755