From 0a477fd288d064d265395c88dbd0f261b723804e Mon Sep 17 00:00:00 2001 From: spiperac Date: Mon, 15 Jun 2026 06:42:13 +0200 Subject: Initial release --- docs/building.md | 75 ++++++++++++++++++ docs/features/decoder.md | 37 +++++++++ docs/features/history.md | 32 ++++++++ docs/features/intercept.md | 23 ++++++ docs/features/intruder.md | 33 ++++++++ docs/features/loot.md | 24 ++++++ docs/features/repeater.md | 35 +++++++++ docs/features/scope.md | 19 +++++ docs/getting-started.md | 42 ++++++++++ docs/index.md | 20 +++++ docs/plugins.md | 191 +++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 531 insertions(+) create mode 100644 docs/building.md create mode 100644 docs/features/decoder.md create mode 100644 docs/features/history.md create mode 100644 docs/features/intercept.md create mode 100644 docs/features/intruder.md create mode 100644 docs/features/loot.md create mode 100644 docs/features/repeater.md create mode 100644 docs/features/scope.md create mode 100644 docs/getting-started.md create mode 100644 docs/index.md create mode 100644 docs/plugins.md (limited to 'docs') diff --git a/docs/building.md b/docs/building.md new file mode 100644 index 0000000..3e2359d --- /dev/null +++ b/docs/building.md @@ -0,0 +1,75 @@ +# Building from Source + +## Requirements + +- Go 1.22 or later +- A C compiler (gcc or clang) — required by Fyne for CGo +- Platform graphics libraries: + - **Linux**: `libgl1-mesa-dev` and `xorg-dev` + - **macOS**: Xcode command-line tools (`xcode-select --install`) + - **Windows**: MinGW (`gcc-mingw-w64-x86-64` from MSYS2 or a MinGW package) + +## Quick build + +``` +go build -o shiv . +./shiv +``` + +## Using Nix + +The repository includes a Nix flake that provides a reproducible development shell with all native dependencies set up correctly. This is the recommended way to build on Linux if you use Nix: + +``` +nix develop +go build -o shiv . +``` + +## Installing locally (Linux) + +``` +make install +``` + +This builds the binary, installs it to `~/.local/bin/shiv`, copies the icon, and registers a `.desktop` entry so Shiv appears in your application launcher. + +## AppImage (Linux) + +The AppImage bundles the binary with its shared library dependencies so it runs on any modern Linux distribution without installing anything. + +``` +make appimage +``` + +This produces a `Shiv-x86_64.AppImage` in the current directory. Mark it executable and run it directly: + +``` +chmod +x Shiv-x86_64.AppImage +./Shiv-x86_64.AppImage +``` + +## Packaged builds with Fyne + +Fyne's packaging tool produces platform-native bundles — a `.tar.xz` on Linux, a `.app` on macOS, and an `.exe` on Windows: + +``` +fyne package +``` + +Install the Fyne CLI with: + +``` +go install fyne.io/tools/cmd/fyne@latest +``` + +## Cross-compilation + +The release CI builds Windows binaries on Linux using MinGW and macOS universal binaries using two separate `fyne package` runs (one for arm64, one for amd64) combined with `lipo`. See `.github/workflows/release.yml` for the exact steps if you need to replicate this locally. + +## Running tests + +``` +go test ./... +``` + +The non-UI packages (`internal/store`, `internal/events`, `internal/proxy`, `internal/http`) can be tested without a display. The UI package is not unit tested and requires a running display or Xvfb. diff --git a/docs/features/decoder.md b/docs/features/decoder.md new file mode 100644 index 0000000..5d69c57 --- /dev/null +++ b/docs/features/decoder.md @@ -0,0 +1,37 @@ +# Decoder + +The Decoder is a quick utility for encoding, decoding, and hashing text without leaving the application. Paste input in the top pane, select an operation, and the result appears in the bottom pane. + +## Operations + +**URL Encode / Decode** — percent-encodes or decodes query string values using standard URL encoding. + +**URL Path Encode / Decode** — same as URL encoding but using path escaping rules, which treats `/` as a safe character. + +**Base64 Encode / Decode** — standard Base64. The decoder accepts both standard and URL-safe variants and strips whitespace before decoding. + +**Base64 URL-safe Encode / Decode** — encodes using the URL-safe alphabet (`-` and `_` instead of `+` and `/`). + +**Hex Encode / Decode** — encodes bytes to hex string and back. Spaces in the input are ignored when decoding. + +**HTML Entity Encode / Decode** — escapes `<`, `>`, `&`, and quotes to HTML entities and unescapes them back. + +**JSON Pretty / Minify** — pretty-prints JSON with two-space indentation, or minifies it back to a single line. + +**JWT Decode (no verify)** — splits a JWT and pretty-prints the header and payload sections. The signature is shown as-is. No signature verification is performed. + +**MD5 Hash** — outputs the MD5 hex digest of the input. One-way only. + +**SHA1 Hash** — outputs the SHA1 hex digest of the input. One-way only. + +**SHA256 Hash** — outputs the SHA256 hex digest of the input. One-way only. + +## Workflow + +Select the operation from the dropdown. Click the forward button (labelled Encode, Decode, Pretty, or Hash depending on the operation) to transform the input and write the result to the output pane. + +For operations that have a reverse (everything except the hash functions and JWT), a second button on the left runs the reverse direction. + +**Swap** exchanges the contents of the input and output panes, which is useful for chaining operations. + +**Copy Output** copies the output pane to the clipboard. diff --git a/docs/features/history.md b/docs/features/history.md new file mode 100644 index 0000000..0d82816 --- /dev/null +++ b/docs/features/history.md @@ -0,0 +1,32 @@ +# History + +The History tab shows every HTTP request and response that has passed through the proxy. Entries are persisted to the project database, so they survive restarts. + +## Browsing traffic + +The left panel shows a site map tree. Clicking a host filters the table to that host only. The **All** button at the top of the site map clears the filter and shows all traffic. Right-clicking a host in the site map gives options to delete all traffic from that host or remove it from history entirely. + +The main table shows one row per transaction. Columns include method, URL path, status code, content type, response size, and duration. Clicking a row loads the full request and response in the detail pane on the right. + +## Filtering + +The filter bar above the table lets you narrow results by: + +- **Method** — GET, POST, etc., or All +- **Status** — a specific code (e.g. 200), a range (e.g. 200-299), or left blank for all +- **Content type** — text, json, html, etc. +- **Search** — searches request and response bodies, URLs, and headers +- **Hide static** — hides common static asset extensions (images, fonts, scripts, stylesheets) +- **In scope only** — restricts the view to hosts that are in your defined scope + +## Request and response detail + +Selecting a row shows the raw request on the left and the raw response on the right. The Inspector button opens a structured view of headers and a formatted body. Copy buttons are available on each pane. The response can be saved to disk via the Save button. + +## Annotations + +Right-clicking a row in the history table opens a context menu with annotation options. You can add a text comment to a row or apply a colour highlight (red, orange, green, blue, purple). Annotations are stored in the database and visible as background colours in the table and a comment column. Clearing an annotation removes both the comment and the colour. + +## Sending to other tools + +From the right-click context menu you can send any captured request to the Repeater for manual editing and replay, or to the Intruder for payload injection. diff --git a/docs/features/intercept.md b/docs/features/intercept.md new file mode 100644 index 0000000..64f5d2b --- /dev/null +++ b/docs/features/intercept.md @@ -0,0 +1,23 @@ +# Intercept + +The Intercept tab pauses HTTP requests or responses in-flight, letting you read and modify them before they continue to their destination. + +## Enabling intercept + +The toggle at the top of the tab turns interception on or off. When on, every request matching the current scope will pause and wait for you to act on it. Requests from out-of-scope hosts pass through without stopping. + +## Editing a paused request + +When a request is paused, its raw text appears in the editor. You can change anything — the method, path, headers, or body. The editor shows the raw HTTP format, so changes are applied exactly as written. + +Once you are done editing, click **Forward** to send the request on to the server. Click **Drop** to discard it entirely — the browser will receive a connection error. + +## Response intercept + +You can also intercept responses on their way back to the browser. Enable response intercept with the second toggle. The workflow is the same: edit the raw response and forward or drop it. + +## Notes + +Intercept operates on HTTP/1.1 traffic. WebSocket frames are not paused here; the proxy forwards them bidirectionally in real time. If you need to inspect WebSocket traffic, use the WebSocket tab in the main window. + +Requests pile up in a queue while intercept is enabled. If you navigate away from the tab while a request is paused, it will remain paused until you return and act on it. If you close the application, paused requests are dropped. diff --git a/docs/features/intruder.md b/docs/features/intruder.md new file mode 100644 index 0000000..f42b67a --- /dev/null +++ b/docs/features/intruder.md @@ -0,0 +1,33 @@ +# Intruder + +The Intruder runs automated requests against a target by substituting payloads into marked positions in a raw HTTP request. + +## Setup + +Paste a raw HTTP request into the editor. Mark the positions you want to fuzz by wrapping them with `§` characters. For example: + +``` +POST /login HTTP/1.1 +Host: example.com +Content-Type: application/x-www-form-urlencoded + +username=§admin§&password=§password§ +``` + +Each `§`-wrapped section is an injection point. Intruder substitutes each payload into all marked positions simultaneously (sniper mode — one payload list, all positions get the same value on each request). + +## Payloads + +Paste a wordlist into the payload box, one entry per line. When you click **Start**, Intruder sends one request per payload entry, replacing all marked positions with the current payload. + +## Results + +Results appear in the table as requests complete. Columns show the payload, status code, response length, and duration. Clicking a row shows the full response in the detail pane. + +You can sort by any column. Responses that stand out by length or status code are usually the interesting ones. + +Completed results can be sent to the Repeater for closer inspection via the right-click context menu. + +## Controls + +**Start** begins the attack. **Stop** halts it mid-run. Running a new attack clears the previous results. diff --git a/docs/features/loot.md b/docs/features/loot.md new file mode 100644 index 0000000..5c82e88 --- /dev/null +++ b/docs/features/loot.md @@ -0,0 +1,24 @@ +# Loot + +The Loot tab is a findings tracker. When you find something worth documenting during a test, you save it here with the relevant request and response attached. + +## Adding an entry + +Click **Add** to open the entry dialog. Fill in: + +- **Title** — a short name for the finding +- **URL** — the affected endpoint +- **Severity** — Critical, High, Medium, Low, or Info +- **Notes** — a free-text field for your write-up, reproducing steps, or anything else. Supports Markdown. + +You can also send a request directly from the History or Repeater tab using the Loot button. This pre-fills the raw request and response fields so the evidence is captured automatically. + +## Managing entries + +Entries are listed in a table with title, URL, and severity. Clicking a row expands the detail view showing notes and the raw request/response. + +Right-clicking an entry gives options to edit it or delete it. + +## Export + +The **Export** button writes all loot entries to a Markdown file. The output includes each finding's title, severity, URL, notes, and the raw request and response in fenced code blocks. The format is clean enough to paste directly into a report or a Git repository. diff --git a/docs/features/repeater.md b/docs/features/repeater.md new file mode 100644 index 0000000..f491c74 --- /dev/null +++ b/docs/features/repeater.md @@ -0,0 +1,35 @@ +# Repeater + +The Repeater lets you take a raw HTTP request, edit it, and send it as many times as you want. Each tab is independent and saved across sessions. + +## HTTP tabs + +Paste or edit a raw HTTP request in the left pane and click **Send**. The raw response appears in the right pane. The tab name updates automatically to reflect the method and path of the last sent request. + +The host and TLS settings are inferred from the `Host` header and the request line scheme. If you paste a request captured from an HTTPS host that has no scheme in the request line (which is normal for HTTP/1.1), the tab uses the connection metadata from the original capture to determine whether to dial TLS. + +**Clone** duplicates the current tab with the current request contents, so you can branch off a variation without losing your original. + +The **Inspector** button (enabled after the first send) opens a structured view of the last request and response — headers parsed into a table and the body formatted if it is JSON. + +The **Loot** button saves the last request and response to the Loot tab for documentation. + +### Send history + +Each tab keeps a ring buffer of the last 50 sends. The back and forward arrow buttons navigate through previous requests and responses within the tab, letting you compare results or replay an earlier version. + +## WebSocket tabs + +WebSocket tabs connect directly to a WebSocket endpoint. Enter the URL (`wss://` or `ws://`) and click **Connect**. Once connected the button changes to **Disconnect**. + +The **Headers** button lets you add custom HTTP headers to the handshake request before connecting. This is useful for passing authentication tokens or setting origin headers. + +Frames are shown in the top pane. Selecting a frame shows its full payload below. Sent frames are highlighted differently from received ones. + +Type a message in the send bar at the bottom and click **Send** to push a text frame. The **Clear** button empties the frame list. + +## Creating tabs + +Tabs are created automatically when you send a request from the History tab via the right-click menu. You can also open a new tab manually using the + button in the tab bar. WebSocket tabs are opened from the WebSocket history tab via the right-click menu. + +All tabs persist to the project database and reopen with their last request content when you restart Shiv. diff --git a/docs/features/scope.md b/docs/features/scope.md new file mode 100644 index 0000000..781e5db --- /dev/null +++ b/docs/features/scope.md @@ -0,0 +1,19 @@ +# Scope + +Scope controls which hosts Shiv actively intercepts and records. Traffic from out-of-scope hosts passes through the proxy transparently without being stored. + +## Adding hosts to scope + +Go to the **Scope** tab and add a hostname. Entries are matched against the `Host` header of each request. Matching is by hostname only — port and path are not considered. + +You can add as many hosts as needed. Wildcard entries are not currently supported; each entry is an exact hostname match. + +## Effect on other tabs + +When a scope is defined, the **History** tab gains an **In scope only** toggle in the filter bar that limits the view to scoped traffic. Out-of-scope requests are still captured if the setting to record all traffic is on, but the filter lets you focus on what matters. + +The **Intercept** tab only pauses requests from in-scope hosts. This prevents the intercept queue from filling up with CDN and analytics traffic when you only care about a specific target. + +## No scope + +If the scope list is empty, Shiv treats all hosts as in scope — everything is intercepted and recorded. This is the default on a fresh project. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..834a77d --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,42 @@ +# Getting Started + +## Installation + +Download the latest release for your platform from the [releases page](https://github.com/spiperac/shiv/releases). + +- **Linux**: AppImage, run directly without installation +- **macOS**: universal `.app` bundle (arm64 + amd64), drag to Applications +- **Windows**: standalone `.exe` + +To build from source instead, see [Building from Source](building.md). + +## First run + +On the first launch Shiv generates a local CA certificate and private key, stored under your OS user data directory. This CA is used to sign certificates for every HTTPS host the proxy intercepts. + +## Trusting the CA certificate + +Go to **Settings** and click **Install CA Certificate**. This copies the CA cert to your system trust store. On Linux it uses the system CA bundle. On macOS it adds the cert to the Keychain. On Windows it imports into the certificate store. + +After installing, restart your browser to pick up the change. + +If you prefer to install manually, the CA file path is shown in Settings. You can import it into Firefox's own certificate manager under Preferences > Privacy & Security > Certificates > View Certificates > Authorities > Import. + +## Configuring the proxy + +Point your browser (or any HTTP client) at `127.0.0.1:9090` as the HTTP proxy. Most browsers have a proxy setting under network or connection settings. On Linux you can also set `http_proxy` and `https_proxy` environment variables: + +``` +export http_proxy=http://127.0.0.1:9090 +export https_proxy=http://127.0.0.1:9090 +``` + +For tools like `curl`: + +``` +curl -x http://127.0.0.1:9090 https://example.com +``` + +## Verifying it works + +With the proxy configured, open any HTTPS site in your browser. You should see it appear in the **History** tab within Shiv. If the browser shows a certificate error, the CA cert was not imported correctly or the browser needs to be restarted. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ac0c0c1 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,20 @@ +# Shiv + +Shiv is a desktop MITM proxy for HTTP/HTTPS traffic inspection and web security testing. It runs natively on Linux, macOS, and Windows — written in Go with a Fyne UI, storing everything in a local SQLite database. + +The proxy listens on `127.0.0.1:9090` and handles plain HTTP, HTTPS via MITM (using a locally generated CA), HTTP/2, and WebSockets. All traffic passing through is recorded to disk and available in the History tab for the lifetime of the project. + +## Features at a glance + +- Full traffic capture with filtering, search, and per-row annotations +- Request intercept with in-flight editing +- Repeater for manual request replay, with per-tab send history and WebSocket support +- Intruder for automated payload injection +- Loot tab for tracking findings, with Markdown export +- Decoder utility covering Base64, URL encoding, Hex, HTML entities, JSON, JWT, and common hashes +- Scope control to limit which hosts are actively intercepted +- Lua plugin API for scripting against the request/response pipeline + +## Where to start + +If you are setting Shiv up for the first time, go to [Getting Started](getting-started.md). diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000..78a68f7 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,191 @@ +# Plugins + +Shiv supports Lua plugins that hook into the request and response pipeline. Plugins are loaded from the `plugins/` subdirectory of your Shiv data directory. Any `.lua` file placed there is loaded automatically on startup. + +You can also load a plugin at runtime from the Plugins tab without restarting. + +## Plugin lifecycle + +When a plugin is loaded, its top-level code runs first. If the plugin defines an `on_load` function, it is called immediately after. Plugins can be enabled or disabled from the Plugins tab without reloading the application. + +Each plugin runs in its own isolated Lua VM. Plugins do not share state with each other. + +Hook calls have a 500ms timeout. If a hook takes longer than that it is interrupted and an error is logged. + +## Hooks + +### on_load() + +Called once when the plugin is first loaded. Use it for initialization. + +```lua +function on_load() + log("my plugin loaded") +end +``` + +### on_request(req) + +Called for every HTTP request before it is forwarded to the server. The `req` table contains: + +| Field | Type | Description | +|---|---|---| +| `method` | string | HTTP method | +| `url` | string | Full URL | +| `headers` | table | Request headers | +| `body` | string | Request body | +| `drop` | boolean | Set to `true` to drop the request | + +Return the modified table to apply changes. Fields you do not modify are left as-is. + +```lua +function on_request(req) + -- add a header to every request + req.headers["X-Proxy"] = "shiv" + return req +end +``` + +To drop a request: + +```lua +function on_request(req) + if req.url:find("analytics") then + req.drop = true + return req + end + return req +end +``` + +### on_response(resp) + +Called for every HTTP response after it is received from the server. The `resp` table contains: + +| Field | Type | Description | +|---|---|---| +| `host` | string | Bare hostname | +| `method` | string | Request method | +| `url` | string | Request URL | +| `proto` | string | Protocol (HTTP/1.1, HTTP/2) | +| `status` | number | HTTP status code | +| `duration_ms` | number | Round-trip time in milliseconds | +| `tls` | boolean | Whether the connection was TLS | +| `req_headers` | table | Request headers | +| `req_body` | string | Request body | +| `resp_headers` | table | Response headers | +| `resp_body` | string | Response body | + +The return value of `on_response` is ignored. This hook is read-only. + +```lua +function on_response(resp) + if resp.status == 500 then + log("server error on " .. resp.url) + db.loot_add("500 Error", "Low", resp.url, "", "") + end +end +``` + +### on_websocket_frame(frame) + +Called for every WebSocket frame in either direction. The `frame` table contains: + +| Field | Type | Description | +|---|---|---| +| `connection_id` | number | Unique ID for the WebSocket connection | +| `direction` | number | 0 = client to server, 1 = server to client | +| `opcode` | number | WebSocket opcode (1 = text, 2 = binary, etc.) | +| `payload` | string | Frame payload | + +Return the modified table to change the payload. Setting `frame.payload` to a different value rewrites the frame on the wire. + +```lua +function on_websocket_frame(frame) + log("frame: " .. frame.payload) + return frame +end +``` + +## Built-in API + +### log(message) + +Writes a line to the plugin log visible in the Plugins tab. + +```lua +log("something happened") +``` + +### http + +Makes outbound HTTP requests from within a plugin. Note that these requests do not go through the Shiv proxy — they go directly to the target. + +```lua +-- GET +local resp, err = http.get("https://example.com") +if err then + log("error: " .. err) +else + log("status: " .. resp.status) + log("body: " .. resp.body) +end + +-- POST +local resp, err = http.post("https://example.com/api", '{"key":"value"}', "application/json") + +-- Custom request +local resp, err = http.request("PUT", "https://example.com/resource", "body", { + ["Authorization"] = "Bearer token" +}) +``` + +All three functions return a response table with `status` (number), `body` (string), and `headers` (table), or `nil` plus an error string on failure. + +### db + +Provides read and write access to the project database. + +```lua +-- Read history (up to 100 most recent) +local txs, err = db.history(100) +for i, tx in ipairs(txs) do + log(tx.method .. " " .. tx.url .. " -> " .. tx.status) +end + +-- Filter history by host +local txs, err = db.history_filter("example.com", "", 0) + +-- Scope management +local entries, err = db.scope_get() +db.scope_add("example.com") +db.scope_remove(entry_id) + +-- Loot management +local entries, err = db.loot_get() +local id, err = db.loot_add("Title", "High", "Notes", raw_request, raw_response) +db.loot_delete(id) +``` + +Each `db` function returns its result on success, or `nil` plus an error string on failure. + +## Example plugin + +This plugin logs any response with a `Set-Cookie` header and automatically saves it to Loot if the cookie is missing the `HttpOnly` flag: + +```lua +function on_load() + log("cookie checker loaded") +end + +function on_response(resp) + local cookie = resp.resp_headers["Set-Cookie"] + if not cookie then return end + + if not cookie:find("HttpOnly") then + local notes = "Set-Cookie header missing HttpOnly flag:\n" .. cookie + db.loot_add("Missing HttpOnly", "Low", notes, "", "") + log("flagged missing HttpOnly on " .. resp.host) + end +end +``` -- cgit v1.2.3