#!/usr/bin/env bash # StackPatch quickscan — anonymous CVE check for your Linux server. # Source: https://mindsparkstack.com/scan.sh — read it before piping to bash. # # What this does: # - Reads /etc/os-release, uname -r, and your installed package list (dpkg / apk / rpm) # - POSTs to https://mindsparkstack.com/api/stackpatch/quickscan (anonymous, no signup) # - Pretty-prints findings + recommended actions inline # # What it does NOT do: # - Modify your system # - Put identifying info in the payload (no hostname, IP, public key, env vars) # - Store your package list (only counts are kept — see below) # # What the server keeps: an anonymous scan event (which distro, package COUNT, # finding count) + a daily-rotating one-way hash of your IP used only for # rate-limiting. No cross-day correlation, no User-Agent, no cookies, no # third parties. The script below is the whole thing — read it before you run it. set -eu if ! command -v python3 >/dev/null 2>&1; then echo "stackpatch-scan: python3 required (used to format JSON)" >&2 exit 1 fi if ! command -v curl >/dev/null 2>&1; then echo "stackpatch-scan: curl required" >&2 exit 1 fi API_URL="https://mindsparkstack.com/api/stackpatch/quickscan" python3 - <<'PY' import json import os import subprocess import sys import urllib.error import urllib.request API_URL = "https://mindsparkstack.com/api/stackpatch/quickscan" def run(cmd): try: return subprocess.check_output(cmd, stderr=subprocess.DEVNULL, text=True).strip() except Exception: return "" # 1. Distro + version distro = "" codename = "" version_id = "" if os.path.exists("/etc/os-release"): with open("/etc/os-release") as f: for line in f: line = line.strip() if line.startswith("ID="): distro = line[3:].strip().strip('"').lower() elif line.startswith("VERSION_CODENAME="): codename = line[17:].strip().strip('"') elif line.startswith("VERSION_ID="): version_id = line[11:].strip().strip('"') kernel = run(["uname", "-r"]) def _have(b): return any(os.access(os.path.join(p, b), os.X_OK) for p in os.environ.get("PATH", "").split(":")) # 2. Packages (top 200) — auto-detect dpkg (deb) / apk (alpine) / rpm (rhel family) packages = {} if _have("dpkg-query"): out = run(["dpkg-query", "-W", "-f=${Package}\t${Version}\n"]) for line in out.splitlines()[:200]: parts = line.split("\t") if len(parts) == 2 and parts[0]: packages[parts[0]] = parts[1] elif _have("apk"): # Alpine keeps the release in VERSION_ID (e.g. 3.18.4); the matcher keys on major.minor. if version_id: codename = ".".join(version_id.split(".")[:2]) names = sorted(run(["apk", "info"]).split(), key=len, reverse=True) for nv in run(["apk", "info", "-v"]).split(): for name in names: if nv.startswith(name + "-"): packages[name] = nv[len(name) + 1:] break if len(packages) >= 200: break elif _have("rpm"): # RHEL family: matcher keys on the major version. MUST send the epoch — epoch # dominates rpm version comparison; dropping it mass-produces false positives. if version_id: codename = version_id.split(".")[0] out = run(["rpm", "-qa", "--qf", "%{NAME}\t%{EPOCH}\t%{VERSION}-%{RELEASE}\n"]) for line in out.splitlines()[:200]: parts = line.split("\t") if len(parts) == 3 and parts[0]: ep = "0" if parts[1] in ("(none)", "") else parts[1] packages[parts[0]] = ep + ":" + parts[2] print() print("=== StackPatch quickscan ===") print(f" distro: {distro}") print(f" codename: {codename}") print(f" kernel: {kernel}") print(f" packages: {len(packages)}") print() reboot_required = os.path.exists("/var/run/reboot-required") or os.path.exists("/run/reboot-required") payload = json.dumps({ "distro": distro, "codename": codename, "kernel": kernel, "packages": packages, "reboot_required": reboot_required, }).encode("utf-8") req = urllib.request.Request( API_URL, data=payload, headers={"Content-Type": "application/json"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=15) as resp: data = json.loads(resp.read().decode("utf-8")) except urllib.error.URLError as e: print(f"stackpatch-scan: API call failed: {e}", file=sys.stderr) sys.exit(2) except Exception as e: print(f"stackpatch-scan: unexpected error: {e}", file=sys.stderr) sys.exit(2) grade = data.get("grade") grade_summary = data.get("grade_summary", "") if grade and grade != "\u2014": bar = "=" * 38 print(bar) print(f" StackPatch grade: {grade}") if grade_summary: print(f" {grade_summary}") print(bar) print() findings = data.get("findings", []) if not findings: print("\u2705 No quickscan matches.") print() print(data.get("next_step", "")) else: plural = "es" if len(findings) != 1 else "" print(f"\u26a0\ufe0f {len(findings)} match{plural} on your stack:") print() for f in findings: sev = f.get("severity", "?").upper() print(f" [{sev}] {f.get('id')} {f.get('title', '')}") badges = [] if f.get("kev"): rw = ", ransomware-linked" if f.get("kev_ransomware") else "" added = f.get("kev_added", "") stamp = f" (added {added})" if added else "" badges.append(f"ACTIVELY EXPLOITED \u2014 on CISA KEV{rw}{stamp}") epss = f.get("epss") if epss is not None: pct = f.get("epss_pct") top = f", top {round((1 - pct) * 100)}% most likely exploited" if pct is not None else "" badges.append(f"EPSS {round(epss * 100)}%{top}") if badges: print(f" >> {' | '.join(badges)}") print(f" why: {f.get('why', '')}") print(f" match: {f.get('affected_match', '')}") print(f" recommend: {f.get('recommended_action', '')}") cmd = f.get("command") if cmd: print(f" command:") for line in cmd.splitlines(): print(f" $ {line}") print() advisories = data.get("advisories", []) if advisories: n = len(advisories) print(f"\u2139\ufe0f {n} heuristic advisor{'y' if n == 1 else 'ies'} to verify (not counted in your grade):") print() for a in advisories: print(f" [VERIFY] {a.get('id')} {a.get('title', '')}") print(f" why: {a.get('why', '')}") print(f" recommend: {a.get('recommended_action', '')}") cmd = a.get("command") if cmd: print(f" command:") for line in cmd.splitlines(): print(f" $ {line}") print() cta = data.get("cta", {}) outcome = data.get("outcome", "clean") headline = data.get("headline", "") next_step = data.get("next_step", "") print() print("--") if headline: if outcome == "vulnerable": print(f"\u26a0\ufe0f {headline}") elif outcome == "unsupported": print(f"\u2139\ufe0f {headline}") else: print(f"\u2705 {headline}") if next_step: print(f" {next_step}") print() cta_focus = data.get("cta_focus", "monitor") if cta_focus == "buy": print(f" Start a 14-day trial (from $9/mo): {cta.get('lifetime', '')}") print(f" Live audit URL of our own VPS: {cta.get('free_audit_demo', '')}") print(f" Honest comparison vs vuls.io: https://mindsparkstack.com{cta.get('vs_vuls', '/patch/vs-vuls')}") elif cta_focus == "waitlist": print(f" Sign up free (3 servers, weekly digest): https://mindsparkstack.com{cta.get('waitlist', '/patch#waitlist')}") print(f" Roadmap / V2: {cta.get('see_full_product', '')}") else: print(f" Start a 14-day trial (from $9/mo): {cta.get('lifetime', '')}") print(f" Live audit URL of our own VPS: {cta.get('free_audit_demo', '')}") print(f" Full product page: {cta.get('see_full_product', '')}") PY