# Adversarial review — `handy_notes` (Handy Notes) @ HEAD

Scope: static (src/, checks/) + dynamic capture (behaviour/, build/). Everything treated as hostile data.

## Findings (most severe first)

### CRITICAL — `postinstall` silently exfiltrates high-value secrets off-device
- What: `collect.js` runs on `npm install` via `package.json` (`"postinstall": "node collect.js"`), reads three secret files under `$HOME`, concatenates them with no delimiters, and POSTs the blob to an external HTTPS host (`/ingest`).
- Evidence:
  - `src/collect.js:7` reads `/.ssh/id_ed25519`, `/.config/solana/id.json`, `/.aws/credentials`; `src/collect.js:9-10` sends them to `telemetry.handy-notes-cdn.net:443`.
  - `behaviour/strace.log:1477-1479` — node (pid 94) actually opens exactly those three paths.
  - `behaviour/summary.json` — `bait_files_read` = `/root/.ssh/id_ed25519`, `/root/.config/solana/id.json`, `/root/.aws/credentials`; `read_secret_then_contacted_external_host: true`.
  - `behaviour/dns.log:1-4` — `telemetry.handy-notes-cdn.net [sinkholed]`, queried at 13:51:09 and 13:51:14.
- Why it matters: The bait is not throwaway — an SSH private key, a wallet seed and AWS credentials all leave the machine at install time. This is the classic npm-postinstall supply-chain shape and the module's UI (a static QML rectangle) gives no hint of it.

### HIGH — README trust claim is contradicted by behaviour, and the module isn't really "notes"
- What: `README.md:2` says "Your notes never leave your device." In fact nothing in the repo reads/writes notes, while `collect.js` ships secrets off-device. There is no notes model — `Main.qml` is a static `Rectangle`/`Text`.
- Evidence: `src/README.md:2`; `src/Main.qml:2`; `src/collect.js:7-10`.
- Why it matters: A reviewer trusting the README would rate this as offline-only. The claim is not backed by the 20 lines of code.

### MEDIUM — Exfil is fire-and-forget; delivery is unconfirmed (may send nothing)
- What: The POST is issued with `https.request(...)` and `req.end()`; there is no response callback, no `await`, and `req.on('error',()=>{})` discards all errors. If the process exits or DNS/TCP fails, the loot is lost silently. The timeout (3000 ms) is shorter than the observed DNS retry window.
- Evidence: `src/collect.js:9-11`; `behaviour/strace.log` contains DNS (`connect(... AF_INET ... 53 ... 127.0.0.1)` at line 1491) but **no** TCP `connect` to `:443` and no socket `write` was captured before the trace ends (`behaviour/strace.log` tail). DNS resolution is confirmed; the HTTPS POST itself is **unconfirmed**.
- Why it matters: "Contacted the host" is true at DNS level only. Whether the bytes landed is not demonstrated by the capture.

### MEDIUM — Repository text is addressed directly to the reviewer (potential bias)
- What: `collect.js:1-4` is not ordinary source commentary; it tells a human/model reviewer what this is and what to conclude: "CONTROL SAMPLE for open-review (not malware for use) ... so the reviewer's bait and behaviour capture have something real to catch."
- Evidence: `src/collect.js:1-4`.
- Why it matters: The code names the review apparatus (bait, behaviour capture) and pre-labels the sample. A reviewer should check these claims, not accept them.

### LOW — Destination host is `[sinkholed]`, ownership/identity unverified
- What: `telemetry.handy-notes-cdn.net` never resolved to real upstream infra in the sandbox; the `handy-notes-cdn.net` domain has no relation to the module's `handy-notes`/`handy_notes` identifiers that can be verified from the repo.
- Evidence: `behaviour/dns.log`; `behaviour/summary.json` `contacted_hosts`.
- Why it matters: Cannot confirm who receives the secrets or whether the endpoint is stable.

### LOW — Identifier inconsistency across metadata
- What: `metadata.json` uses `handy_notes`; `package.json` and `package-lock.json` use `handy-notes`; build log prints `module: handy_notes`.
- Evidence: `src/metadata.json:1`; `src/package.json:1`; `behaviour/build.out:5,10`.
- Why it matters: Cosmetics/consistency; can break matching by host tooling.

### LOW (environment, unconfirmed) — Sandbox disguise is kernel-detectable
- What: The capture dressed a container as a `Framework Laptop 13 (AMD Ryzen 7040Series)`, but the kernel is the host's. A module could branch on that.
- Evidence: `behaviour/disguise.json` (note: "the kernel version is the host's and cannot be hidden"). Not itself the module's fault; no evidence the module checked kernel.

## Checked and looked clean
- gitleaks: no committed secrets (`checks/gitleaks.json` = `[]`).
- osv-scanner: no vulnerable/advisory deps (`checks/osv.json`); zero third-party dependencies, so no transitive supply-chain surface beyond `postinstall`.
- semgrep: no hits over `Main.qml`, `README.md`, `collect.js`, `metadata.json`, `package.json` (`checks/semgrep.json`).
- No hidden files, bundled binaries, extra branches or dangling git objects (`git fsck` clean; single commit `5ebe938`).
- Only the three bait paths plus normal loader/`resolv.conf` files were opened (strace); no filesystem fishing beyond the bait.
- Build/install succeeded (`behaviour/build.out`), no failed opens (`behaviour/summary.json`).

Triage, not approval — this never certifies a module as safe.
