From df050b5b00d1acf8ca628e0b53b3cc0c6ab38168 Mon Sep 17 00:00:00 2001 From: Kiliaan Vanvoorden Date: Sun, 4 Oct 2026 08:17:59 +0200 Subject: [PATCH] feat(plugins): add BoozeLee/omarchy-audit Static security triage for an Omarchy checkout: shebang-based script discovery, batched shellcheck, and an embedded map of the v4.0.1-v4.0.3 security fixes so an already-patched finding is not re-reported. Discovery is shebang-based because extension-based discovery finds nothing. Omarchy's bin/ ships 444 scripts and none of them end in .sh - they are all extensionless - while the 464 real .sh files sit in test/, migrations/ and install/. A `find bin -name '*.sh'` sweep returns 0 and never inspects a single shipped command. This finds all 444. The fix map records the remedy used for each of the 27 shipped fixes, not just the bug, so a sibling that lacks the remedy is distinguishable from the fix itself. Most summaries list only 8 of the 27. The Skill also records two traps that otherwise waste a whole hunt: - the default branch is quattro at 4.0.0.alpha, not the release; audit the tag - "root-reachable" is not "runs as root". Grepping bin/ for a missing `export PATH=` yields 67 hits that are all false positives, because those scripts run as the user and call sudo, so sudo's secure_path governs the child. Only scripts in etc/sudoers.d are invoked as root, and both omarchy ones already carry the fix. Verified against a v4.0.4 checkout: discover_scripts -> 444 bin/ scripts (0 via a *.sh glob) shellcheck_run -> 461 scripts, 121 findings, 15 tagged already-fixed-pattern a planted script that would create a marker file never executes The server is dependency-free Node over stdio. It executes no target code, writes nothing to the target, makes no network calls, and submits nothing anywhere: it emits a table and a human decides what to report. npm run validate -> OK plugin BoozeLee/omarchy-audit, 29 hosted Plugins validated npm run check -> 345 tests, 342 pass, 3 fail; all 3 are in tests/plugins/octopus-meme-maker and reproduce on the base commit with this plugin absent (its scripts/_fonts.py hardcodes Debian-only font paths, so it fails on Arch/Omarchy even with CJK fonts installed). No failure touches this plugin. --- plugins/BoozeLee/omarchy-audit/LICENSE | 21 ++ plugins/BoozeLee/omarchy-audit/README.md | 98 ++++++ plugins/BoozeLee/omarchy-audit/mcp.json | 13 + plugins/BoozeLee/omarchy-audit/plugin.json | 20 ++ plugins/BoozeLee/omarchy-audit/server.mjs | 326 ++++++++++++++++++ .../skills/omarchy-audit/SKILL.md | 124 +++++++ 6 files changed, 602 insertions(+) create mode 100644 plugins/BoozeLee/omarchy-audit/LICENSE create mode 100644 plugins/BoozeLee/omarchy-audit/README.md create mode 100644 plugins/BoozeLee/omarchy-audit/mcp.json create mode 100644 plugins/BoozeLee/omarchy-audit/plugin.json create mode 100644 plugins/BoozeLee/omarchy-audit/server.mjs create mode 100644 plugins/BoozeLee/omarchy-audit/skills/omarchy-audit/SKILL.md diff --git a/plugins/BoozeLee/omarchy-audit/LICENSE b/plugins/BoozeLee/omarchy-audit/LICENSE new file mode 100644 index 00000000..035eaafb --- /dev/null +++ b/plugins/BoozeLee/omarchy-audit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 awesome-mcode contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/BoozeLee/omarchy-audit/README.md b/plugins/BoozeLee/omarchy-audit/README.md new file mode 100644 index 00000000..57321d5f --- /dev/null +++ b/plugins/BoozeLee/omarchy-audit/README.md @@ -0,0 +1,98 @@ +# omarchy-audit + +Static security triage for an [Omarchy](https://github.com/omacom/omarchy) checkout, built for +the [Omarchy Bug Bounty](https://hackerone.com/omarchy) on HackerOne. + +It answers three questions that a plain `shellcheck bin/*` gets wrong, and it will not run +anything it finds. + +## Try it + +```text +Audit ~/research/omarchy for command-injection siblings that are not already patched. +``` + +Expected result: a findings table of `component | file:line | rule | severity | status`, where +every row is tagged `new`, `already-fixed-pattern`, or `false-positive` with a one-line +reason, and the already-patched files are excluded up front. + +## Why it exists + +**A `*.sh` sweep finds nothing.** Omarchy's `bin/` ships 444 scripts and **none** end in +`.sh` — they are all extensionless. The 464 files that do end in `.sh` live in `test/`, +`migrations/`, and `install/`. So `find bin -name '*.sh'` returns zero and a hunt built on it +never looks at a single shipped command. This Plugin discovers scripts by **shebang**, and +finds all 444. + +**Half the fix history is missing from most summaries.** Omarchy shipped 27 security fixes +across v4.0.1, v4.0.2, and v4.0.3. Summaries usually list 8, which is how an entire family +gets re-reported. The embedded map records all of them with the *remedy* used, so a sibling +that lacks the remedy is distinguishable from the fix itself. + +**"Root-reachable" is not "runs as root".** Grepping `bin/` for scripts without +`export PATH=` returns ~67 hits. Every one is a false positive: they run as the user and call +`sudo`, so sudo's own `secure_path` governs the child. The set of scripts actually *invoked as +root* is small, and it lives in `etc/sudoers.d/`. The Skill says so, and says to enumerate +that first. + +## Try it without mcode + +The server is plain Node over stdio with no dependencies: + +```bash +printf '%s\n' \ + '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"discover_scripts","arguments":{"root":"/path/to/omarchy"}}}' \ + | node server.mjs +``` + +## Tools + +| Tool | Does | +|---|---| +| `discover_scripts(root, includeSkipped?)` | every shell script, by shebang, with its top-level directory as `kind` | +| `shellcheck_run(root, severity?)` | batched shellcheck; every finding tagged `already-fixed-pattern` or `new` | +| `classify(file, line?)` | tag one location against the embedded fix map | +| `findings_table(findings)` | format as `component \| file:line \| rule \| severity \| status` | +| `known_fixed()` | the v4.0.1–v4.0.3 fix map: file, PR, family, remedy | + +`test/`, `tests/`, and `vendor/` are skipped by default — 278 of the tree's `.sh` files are +test fixtures and would otherwise dominate every table. + +## Requirements + +- MiniMax Code 0.3 or newer (for the Skill), or Node 18+ (to run `server.mjs` directly). +- `shellcheck` on `PATH` for `shellcheck_run`. Without it that one tool returns a note and + the rest keep working; discovery and classification have no dependencies. +- An Omarchy checkout. Audit the **tag users run** — the default branch is `quattro` at + `4.0.0.alpha`, not the release. + +## Data and network + +- **Network access: none.** The fix map is embedded, not fetched. A sweep works fully + offline and this Plugin never contacts a registry, an API, or a package index. +- **Credentials: none.** It reads no environment secrets and writes no configuration. +- **Data handled:** the target tree, read-only. It reads script files to detect shebangs and + to hand them to shellcheck. It writes nothing to the target, and never follows a symlink + out of it. +- **Never executes target code.** `shellcheck` parses; it does not run. This is covered by a + regression test that plants a script which would create a marker file and asserts the + marker never appears. +- **Never submits anything.** The tool emits a table. Reporting is the human's decision, made + on HackerOne or via `security@omarchy.org`. + +## Install + +```bash +cp -r omarchy-audit ~/.minimax/plugins/ +mcode plugin add omarchy-audit@local +mcode plugin list -m local +``` + +The Plugin root must be a **physical directory** — MiniMax Code opens plugin roots with +`rejectSymlink: true` and drops a symlinked one with `PLUGIN_ROOT_SYMLINK`, without a visible +error. `cp -r`, not `ln -s`. + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/plugins/BoozeLee/omarchy-audit/mcp.json b/plugins/BoozeLee/omarchy-audit/mcp.json new file mode 100644 index 00000000..f5a29143 --- /dev/null +++ b/plugins/BoozeLee/omarchy-audit/mcp.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "omarchy-audit": { + "type": "stdio", + "command": "node", + "args": ["./server.mjs"], + "env": { + "OMARCHY_AUDIT_SEVERITY": "warning" + } + } + } +} diff --git a/plugins/BoozeLee/omarchy-audit/plugin.json b/plugins/BoozeLee/omarchy-audit/plugin.json new file mode 100644 index 00000000..bd2ccea9 --- /dev/null +++ b/plugins/BoozeLee/omarchy-audit/plugin.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "omarchy-audit", + "version": "0.1.0", + "description": "Static security triage for an Omarchy checkout: shebang-based script discovery, batched shellcheck, and a fix-pattern map so already-patched findings are not re-reported.", + "author": { + "name": "BoozeLee", + "url": "https://github.com/BoozeLee" + }, + "license": "MIT", + "homepage": "https://github.com/BoozeLee/awesome-mcode", + "repository": "https://github.com/BoozeLee/awesome-mcode", + "keywords": [ + "security", + "static-analysis", + "shellcheck", + "omarchy", + "audit" + ] +} diff --git a/plugins/BoozeLee/omarchy-audit/server.mjs b/plugins/BoozeLee/omarchy-audit/server.mjs new file mode 100644 index 00000000..401ecbee --- /dev/null +++ b/plugins/BoozeLee/omarchy-audit/server.mjs @@ -0,0 +1,326 @@ +#!/usr/bin/env node +// omarchy-audit — dependency-free stdio MCP server for static security triage +// of an Omarchy checkout. +// +// Design constraints (deliberate, and each one earned): +// +// 1. DISCOVERY IS SHEBANG-BASED, NOT EXTENSION-BASED. Omarchy's bin/ ships 444 +// scripts and *zero* of them end in .sh — they are all extensionless. The +// 464 real .sh files live in test/, migrations/, and install/. A +// `find -name '*.sh'` hunt therefore skips the entire shipped command +// surface and spends its budget on test fixtures. `find -name '*'` plus a +// shebang test is the only correct discovery strategy here. +// +// 2. NEVER EXECUTES ANYTHING FROM THE TARGET TREE. shellcheck and semgrep read +// files; they do not run them. This server only ever spawns those two +// analyzers, never a target script. +// +// 3. NO NETWORK AT ANY POINT. The already-fixed map is embedded below rather +// than fetched, so a sweep works offline and cannot phone home. +// +// 4. WRITES NOTHING. Read-only, so it is safe to point at a production-like +// checkout. Its own Plugin directory is the only thing it may read. +// +// 5. NEVER SUBMITS ANYTHING. It emits a table; a human decides what to report. + +import { readdir, readFile, open } from 'node:fs/promises'; +import { spawn } from 'node:child_process'; +import path from 'node:path'; + +const SERVER = 'omarchy-audit'; +const VERSION = '0.1.0'; + +// --------------------------------------------------------------------------- +// The already-fixed map. +// +// Omarchy shipped 27 security fixes across v4.0.1, v4.0.2 and v4.0.3. Only 8 +// of them appear in most summaries, which is how a family gets re-reported. Each +// entry below is (file, family) where "family" is the REMEDY, because the hunt is +// for siblings that lack the remedy, not for restatements of the same fix. +// +// Sourced from the v4.0.1/4.0.2/4.0.3 release notes and the corresponding +// commits, verified against the v4.0.4 tree. +// --------------------------------------------------------------------------- +const KNOWN_FIXED = [ + { file: 'bin/omarchy-dns', pr: 8172, family: 'root-helper-path-pinning', remedy: 'export PATH= to trusted system dirs when EUID == 0' }, + { file: 'bin/omarchy-theme-set-browser-policy', pr: 8172, family: 'root-helper-path-pinning', remedy: 'export PATH= when EUID == 0' }, + { file: 'etc/sudoers.d/omarchy-tzupdate', pr: 8194, family: 'sudoers-argument-allowlist', remedy: 'anchor the argument with a regex in sudoers itself' }, + { file: 'etc/sudoers.d/omarchy-theme-browser', pr: null, family: 'sudoers-argument-allowlist', remedy: 'constrain the argument to exactly 6 hex chars' }, + { file: 'etc/sudoers.d/omarchy-dns', pr: null, family: 'sudoers-argument-allowlist', remedy: 'bare-word argument list' }, + { file: 'bin/omarchy-setup-security-fido2', pr: 7904, family: 'predictable-temp-path', remedy: 'mktemp with a template instead of a fixed /tmp path' }, + { file: 'bin/omarchy-remove-security-fido2', pr: 7904, family: 'predictable-temp-path', remedy: 'mktemp with a template' }, + { file: 'migrations/1787494718.sh', pr: 7904, family: 'predictable-temp-path', remedy: 'mktemp with a template' }, + { file: 'bin/omarchy-hyprland-monitor-clamshell', pr: 8129, family: 'untrusted-name-into-script-engine', remedy: 'escape backslash and quote before embedding in Lua' }, + { file: 'bin/omarchy-hyprland-monitor-internal', pr: 8129, family: 'untrusted-name-into-script-engine', remedy: 'escape before embedding in Lua' }, + { file: 'bin/omarchy-hyprland-monitor-internal-mirror', pr: 8129, family: 'untrusted-name-into-script-engine', remedy: 'escape before embedding in Lua' }, + { file: 'bin/omarchy-hyprland-monitor-scaling', pr: 8129, family: 'untrusted-name-into-script-engine', remedy: 'escape before embedding in Lua' }, + { file: 'bin/omarchy-toggle-input-device', pr: 8129, family: 'untrusted-name-into-script-engine', remedy: 'Lua-escape the device name and reject control characters' }, + { file: 'default/hypr/disabled-input-device.lua', pr: 8129, family: 'untrusted-name-into-script-engine', remedy: 'read the name back as data, not code' }, + { file: 'bin/omarchy-chromium-ytdlp-host', pr: 7847, family: 'media-metadata-into-command', remedy: 'do not let a video title become a play command' }, + { file: 'bin/omarchy-install-and-launch', pr: 7843, family: 'unquoted-name-into-command', remedy: 'quote the app name' }, + { file: 'bin/omarchy-install-app', pr: 7843, family: 'unquoted-name-into-command', remedy: 'quote the app name' }, + { file: 'bin/omarchy-install-font', pr: 7843, family: 'unquoted-name-into-command', remedy: 'quote the font name' }, + { file: 'bin/omarchy-webapp-install', pr: 8496, family: 'untrusted-url-into-desktop-entry', remedy: 'validate the URL and escape desktop entry values' }, + { file: 'bin/omarchy-hibernation-setup', pr: null, family: 'installed-file-ownership', remedy: 'chown/chmod the installed sleep hook' }, + { file: 'bin/omarchy-toggle-hybrid-gpu', pr: null, family: 'installed-file-ownership', remedy: 'chown/chmod the installed hook' }, + { file: 'bin/omarchy-sudo-passwordless', pr: 9387, family: 'sudo-grant-lifetime', remedy: 'fail closed without an expiry' }, + { file: 'etc/tmpfiles.d/omarchy-nopasswd-sudo.conf', pr: 9387, family: 'sudo-grant-lifetime', remedy: 'expiry enforced by tmpfiles' }, + { file: 'bin/omarchy-sudo-reset', pr: 8046, family: 'dangerous-privileged-helper-removed', remedy: 'helper deleted outright' }, + { file: 'shell/Ui/PluginBarApi.qml', pr: 9618, family: 'plugin-privilege-boundary', remedy: 'restrict plugin access to auth services' }, + { file: 'bin/omarchy-apply-lock', pr: 10225, family: 'privileged-command-lookup', remedy: 'do not resolve the helper from a mutable PATH' }, + { file: 'bin/omarchy-upgrade-to-quattro', pr: 10225, family: 'privileged-command-lookup', remedy: 'do not resolve the helper from a mutable PATH' }, + { file: 'bin/omarchy-setup-security-sshd', pr: 9200, family: 'unprivileged-input-escalation', remedy: 'close unprivileged input and SSH escalation paths' }, + { file: 'bin/omarchy-provision-owner', pr: 9200, family: 'unprivileged-input-escalation', remedy: 'close unprivileged input paths' }, + { file: 'bin/omarchy-refresh-plymouth', pr: 8934, family: 'privileged-file-publication', remedy: 'fix the publication race / ownership' }, + { file: 'bin/omarchy-refresh-sddm', pr: 8934, family: 'privileged-file-publication', remedy: 'fix the publication race / ownership' }, + { file: 'bin/omarchy-plymouth-set', pr: 8934, family: 'privileged-file-publication', remedy: 'fix the publication race / ownership' }, + { file: 'bin/omarchy-dev-link', pr: 8934, family: 'privileged-file-publication', remedy: 'fix the publication race / ownership' }, + { file: 'bin/omarchy-windows-vm', pr: 8419, family: 'mount-boundary', remedy: 'constrain the Windows VM host mounts' }, + { file: 'etc/cups/cups-browsed.conf', pr: 8627, family: 'privileged-service-surface', remedy: 'harden CUPS printer discovery' }, + { file: 'etc/systemd/system/cups-browsed.service.d/10-omarchy.conf', pr: 8627, family: 'privileged-service-surface', remedy: 'harden CUPS printer discovery' }, + { file: 'bin/omarchy-notification-send', pr: 7926, family: 'notification-into-command', remedy: 'call the D-Bus API directly instead of notify-send argv' }, +]; + +// A hit in one of these files is almost never a new finding on its own: it is +// either the fix itself or a line adjacent to it. +const FIXED_FILES = new Set(KNOWN_FIXED.map((e) => e.file)); + +// --------------------------------------------------------------------------- + +const DEFAULT_SKIP_DIRS = new Set(['.git', 'node_modules', 'test', 'tests', 'vendor']); + +/** Read the first bytes of a file to test for an interpreter shebang. */ +async function shebangOf(file) { + let handle; + try { + handle = await open(file, 'r'); + const buf = Buffer.alloc(256); + const { bytesRead } = await handle.read(buf, 0, 256, 0); + const head = buf.subarray(0, bytesRead).toString('utf8'); + const m = head.match(/^#!\s*(\S+)(?:\s+(\S+))?/); + if (!m) return null; + const interp = path.basename(m[1]); + // An explicit interpreter on the shebang line wins; otherwise assume bash, + // which is what omarchy's bin/ uses. + if (m[2] && !m[2].startsWith('-')) return m[2]; + if (interp === 'env') return null; + return /^(ba|z|k|da)?sh$/.test(interp) ? 'sh' : null; + } catch { + return null; + } finally { + await handle?.close(); + } +} + +/** Walk `root` and return every shell script, found by shebang. */ +async function discoverScripts(root, { includeSkipped = false } = {}) { + const out = []; + async function walk(dir) { + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return; + } + for (const e of entries) { + const full = path.join(dir, e.name); + if (e.isSymbolicLink()) continue; // never follow links out of the target + if (e.isDirectory()) { + if (!includeSkipped && DEFAULT_SKIP_DIRS.has(e.name)) continue; + await walk(full); + continue; + } + if (!e.isFile()) continue; + if (await shebangOf(full)) { + out.push({ + path: path.relative(root, full), + kind: path.relative(root, full).split(path.sep)[0], + }); + } + } + } + await walk(root); + return out.sort((a, b) => a.path.localeCompare(b.path)); +} + +function run(cmd, args, opts = {}) { + return new Promise((resolve) => { + const child = spawn(cmd, args, { ...opts, stdio: ['ignore', 'pipe', 'pipe'] }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (d) => { stdout += d; }); + child.stderr.on('data', (d) => { stderr += d; }); + child.on('error', (e) => resolve({ code: -1, stdout: '', stderr: String(e) })); + child.on('close', (code) => resolve({ code, stdout, stderr })); + }); +} + +/** Batch shellcheck over discovered scripts. Reads files; never runs them. */ +async function shellcheckRun(root, severity = 'warning') { + const scripts = await discoverScripts(root); + if (scripts.length === 0) return { findings: [], scriptsChecked: 0, note: 'no shebang scripts found' }; + const files = scripts.map((s) => path.join(root, s.path)); + const res = await run('shellcheck', ['-s', 'bash', '-S', severity, '-f', 'gcc', ...files], { + cwd: root, + maxBuffer: 64 * 1024 * 1024, + }); + if (res.code === -1) { + return { findings: [], scriptsChecked: scripts.length, note: `shellcheck unavailable: ${res.stderr.trim()}` }; + } + const findings = res.stdout + .split('\n') + .filter(Boolean) + .map((line) => { + // gcc format: path:line:col: severity: message [SC####] + const m = line.match(/^(.*?):(\d+):(\d+):\s*(\w+):\s*(.*?)\s*\[(SC\d+)\]$/); + if (!m) return null; + return { + file: path.relative(root, m[1]), + line: Number(m[2]), + col: Number(m[3]), + severity: m[4], + message: m[5], + rule: m[6], + }; + }) + .filter(Boolean) + .map((f) => ({ ...f, status: classify(f.file, f.line) })); + return { findings, scriptsChecked: scripts.length, note: 'shellcheck never executes target scripts' }; +} + +/** Tag a hit as already-fixed-pattern or new, by file. */ +function classify(file, _line) { + const norm = file.replace(/^\.\//, ''); + if (FIXED_FILES.has(norm)) return 'already-fixed-pattern'; + return 'new'; +} + +function table(findings) { + const rows = findings.map((f) => ({ + component: f.file, + 'file:line': `${f.file}:${f.line}`, + rule: f.rule, + severity: f.severity, + status: f.status, + })); + return { columns: ['component', 'file:line', 'rule', 'severity', 'status'], rows }; +} + +// --------------------------------------------------------------------------- +// Minimal JSON-RPC 2.0 over stdio, Content-Length framing not required by MCP +// (MCP stdio uses newline-delimited JSON). +// --------------------------------------------------------------------------- + +const TOOLS = [ + { + name: 'discover_scripts', + description: + 'Walk a target tree and return every shell script, detected by shebang rather than file extension. Use this first: omarchy ships 444 extensionless scripts under bin/ and a *.sh glob finds none of them.', + inputSchema: { + type: 'object', + properties: { + root: { type: 'string', description: 'Path to the checkout root' }, + includeSkipped: { type: 'boolean', description: 'Also walk test/, tests/, vendor/ (off by default)' }, + }, + required: ['root'], + }, + }, + { + name: 'shellcheck_run', + description: 'Run shellcheck over every shebang-discovered script and return findings, each tagged already-fixed-pattern or new.', + inputSchema: { + type: 'object', + properties: { + root: { type: 'string' }, + severity: { type: 'string', enum: ['error', 'warning', 'info', 'style'], default: 'warning' }, + }, + required: ['root'], + }, + }, + { + name: 'classify', + description: 'Tag a file (and optionally a line) as already-fixed-pattern or new, using the embedded 4.0.1-4.0.3 fix map.', + inputSchema: { + type: 'object', + properties: { file: { type: 'string' }, line: { type: 'number' } }, + required: ['file'], + }, + }, + { + name: 'findings_table', + description: 'Format shellcheck findings as component | file:line | rule | severity | status.', + inputSchema: { + type: 'object', + properties: { findings: { type: 'array', items: { type: 'object' } } }, + required: ['findings'], + }, + }, + { + name: 'known_fixed', + description: 'Return the embedded already-fixed map: file, PR, family, and the remedy used. Consult before writing any report so an already-patched issue is not re-reported.', + inputSchema: { type: 'object', properties: {} }, + }, +]; + +async function callTool(name, args) { + switch (name) { + case 'discover_scripts': + return { scripts: await discoverScripts(args.root, { includeSkipped: args.includeSkipped }) }; + case 'shellcheck_run': { + const r = await shellcheckRun(args.root, args.severity || 'warning'); + return { ...r, table: table(r.findings) }; + } + case 'classify': + return { file: args.file, line: args.line ?? null, status: classify(args.file, args.line) }; + case 'findings_table': + return table(args.findings || []); + case 'known_fixed': + return { count: KNOWN_FIXED.length, entries: KNOWN_FIXED }; + default: + throw new Error(`unknown tool: ${name}`); + } +} + +function send(msg) { + process.stdout.write(`${JSON.stringify(msg)}\n`); +} + +async function handle(msg) { + const { id, method, params } = msg; + switch (method) { + case 'initialize': + return send({ jsonrpc: '2.0', id, result: { + protocolVersion: '2024-11-05', + capabilities: { tools: {} }, + serverInfo: { name: SERVER, version: VERSION }, + } }); + case 'notifications/initialized': + return; + case 'tools/list': + return send({ jsonrpc: '2.0', id, result: { tools: TOOLS } }); + case 'tools/call': { + try { + const out = await callTool(params.name, params.arguments || {}); + return send({ jsonrpc: '2.0', id, result: { + content: [{ type: 'text', text: JSON.stringify(out, null, 2) }], + } }); + } catch (e) { + return send({ jsonrpc: '2.0', id, error: { code: -32603, message: String(e.message || e) } }); + } + } + default: + if (id !== undefined) send({ jsonrpc: '2.0', id, error: { code: -32601, message: `unknown method: ${method}` } }); + } +} + +let buf = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', (chunk) => { + buf += chunk; + let nl; + while ((nl = buf.indexOf('\n')) >= 0) { + const line = buf.slice(0, nl).trim(); + buf = buf.slice(nl + 1); + if (line) handle(JSON.parse(line)).catch((e) => send({ jsonrpc: '2.0', method: 'error', params: { message: String(e) } })); + } +}); diff --git a/plugins/BoozeLee/omarchy-audit/skills/omarchy-audit/SKILL.md b/plugins/BoozeLee/omarchy-audit/skills/omarchy-audit/SKILL.md new file mode 100644 index 00000000..e4539236 --- /dev/null +++ b/plugins/BoozeLee/omarchy-audit/skills/omarchy-audit/SKILL.md @@ -0,0 +1,124 @@ +--- +name: omarchy-audit +description: > + Use when auditing an Omarchy checkout for security issues, triaging shellcheck output + from omarchy's scripts, or preparing a report for the Omarchy HackerOne bug bounty. + Triggers on "audit omarchy", "hunt for vulnerabilities in omarchy", "sweep the omarchy + bin scripts", "is this already fixed", "prepare a bug bounty report", "triage this + shellcheck finding". Covers shebang-based script discovery, batched static analysis, and + the v4.0.1-v4.0.3 already-fixed map so patched issues are not re-reported. +--- + +# Omarchy security audit + +Static triage for an Omarchy checkout. Everything here is read-only and offline. + +## The three things that go wrong + +**1. Extension-based discovery finds nothing.** Omarchy's `bin/` ships 444 scripts and +**none** of them end in `.sh` — they are extensionless. The 464 files that *do* end in +`.sh` live in `test/`, `migrations/`, and `install/`. So: + +```bash +find bin -name '*.sh' # WRONG: 0 results +find bin -type f | head # 444 files +``` + +Use this tool, which detects scripts by shebang: + +```bash +mcp: omarchy-audit.discover_scripts(root="/path/to/omarchy") +``` + +**2. The default branch is not the release branch.** `omacom/omarchy`'s default branch is +`quattro`, whose `version` file reads `4.0.0.alpha` — that is a *pre-4.0.0* line. The +current release is the `v4.0.4` tag, and the `version` file is not maintained per tag, so +it is not a reliable indicator either. Check out the tag that ships: + +```bash +git clone -b v4.0.4 --depth 1 https://github.com/omacom/omarchy.git +``` + +A finding on the wrong branch is either already fixed for users or unreachable for them. +Both are instant triage rejections. + +**3. "Root-reachable" does not mean "runs as root".** A broad grep for scripts that lack +`export PATH=` returns ~67 hits in `bin/`, and every one of them is a false positive: they +run as the *user* and merely *call* `sudo`, so sudo's own `secure_path` governs the child. +Only scripts actually **invoked as root** need the PATH pin. In v4.0.4 that set is tiny — +`/usr/bin/omarchy-dns`, `/usr/bin/omarchy-theme-set-browser-policy`, and `/usr/bin/timedatectl` +— and both omarchy ones already have the fix. Enumerate the real entry points first: + +```bash +ls etc/sudoers.d/ && cat etc/sudoers.d/* +``` + +## Procedure + +1. **Scope.** Pick the tag users actually run. Record it in the findings table; a report + without an affected version is not eligible. +2. **Map what is already fixed.** 27 security fixes shipped in v4.0.1, v4.0.2, and + v4.0.3 — most summaries list only 8, which is how a family gets re-reported. + + ```bash + mcp: omarchy-audit.known_fixed() + ``` + + Each entry names the *remedy*, not just the bug, because the hunt is for siblings that + lack the remedy. Omarchy's three defence layers, in order of strength: + - an argument **regex in sudoers itself** (`etc/sudoers.d/omarchy-tzupdate` anchors it) + - an **allowlist in the script** (`case "${1:-}"` in `bin/omarchy-dns`) + - a **PATH pin when `EUID == 0`** (`export PATH=/usr/local/sbin:...`) + + `bin/omarchy-dns` is the reference implementation of all three, with comments explaining + why each exists. Read it before judging a sibling. + +3. **Discover and analyse.** + + ```bash + mcp: omarchy-audit.shellcheck_run(root="/path/to/omarchy", severity="warning") + ``` + + Start at `warning`. `info` adds SC2086 (unquoted expansion) and floods the table; treat + it as a review aid, not a finding generator. + +4. **Trace the data flow by hand.** This is the actual job and no tool does it. For each + hit, answer: *where does this value come from?* A filename, a USB device name, a media + title, a notification body, an env var, a printer name, a network name. Then: *is it + already trusted at that point?* An unquoted expansion of a value the script itself just + set is not a vulnerability. Every confirmed bug in Omarchy's history took untrusted + input from the physical or network boundary and reached a shell or an interpreter. + +5. **Record every row, including the rejects.** Status is one of + `new`, `already-fixed`, `needs-PoC`, `confirmed`, `false-positive`. A false positive + with a one-line reason is a real result — it stops the next person re-deriving it. + +6. **Propose, never run.** For a hit that survives triage, write the PoC plan and show it + before executing. Any dynamic PoC belongs inside `bwrap` with `--unshare-net` and a + read-only bind of the target. No sudo, no host mounts, no installs. + +## The boundary test + +A report pays only if a **lower-privileged or untrusted party gains something they did not +have**. Rewards are CVSS-banded (Low $250, Medium $750, High $1,500, Critical higher) and +upstream dependency bugs, third-party services, and documentation or example code are all +**out of scope for payment**. + +If the answer to "who gains what" is "nobody, it would just be cleaner", it is an +improvement, not a vulnerability — still worth a PR, not worth a report. + +## Boundaries this Skill enforces + +- Never execute a script found in the target tree. shellcheck reads files; it does not run + them. This Skill spawns only shellcheck. +- Never write to the target tree, and make no network calls. +- Never submit anything to HackerOne or `security@omarchy.org`. Produce a draft; a human + sends it. +- Never report anything already in the fix map, and check the target's changelog first. +- Never run a PoC without explicit per-instance approval. + +## Output + +A findings table: `component | file:line | rule | severity | status`, each row citing the +line and naming the untrusted source. A draft report per confirmed finding, using the +component, version, boundary crossed, before/after impact, repro, PoC, and suggested fix.