Repository navigation
Releases: maximilianfeix/hexvero
Releases · maximilianfeix/hexvero
Release list
v0.11.1
Fixed
- IPC: a channel-forwarding helper's parameter is not bound where a nearer declaration of the name hides it (#227).
function reg(o) { { const o = "t:inner"; ipcMain.on(o, h); } } reg("t:p");gave a falset:prow (via wrapper); the helper is now a forwarder only where the IPC call means its parameter by scope, the rule #225 applies to loop variables: a block'slet/const/class/function, a pattern orfor … ofhead, acatch (o)or a parameter of a function in between hides it, avarof the helper itself is the same binding (var o = …writes it), and a declaration in another block, before or after the call, does not hide it. In plain code the call then readst:innerfrom the const, as bundled and minified builds did. The same holds for wrapper methods of classes and objects (where any declaration of the name in the method made it no wrapper, avar oor a declaration in another block no longer does) and for parameters the resolver binds from call sites (acatch (o)gavet:pvia resolved). Where it cannot be decided (a nesting too deep to read) the helper is kept and the new limitationipc_wrapper_param_undecidednames it, file-scoped. JS_SCAN_SCHEMA 35.
Install: npm install -g hexvero@0.11.1 · Run: npx hexvero@0.11.1 ui
v0.11.0
Added
switches: process-wide security settings, read from tokens, literal-first. One row perapp.commandLine.appendSwitch(name[, value])andappendArgument(arg)(name and value as a literal or constant, elsenullwithvalueRead: "unknown"), percertificate-errorlistener that accepts (callback(true)or!0:acceptsalwaysas a statement of the handler's own body,conditionalbehind a condition, a return or in a nested function,unknownwhen not read;preventsDefaultrecorded;callback(false)only gives no row), persetCertificateVerifyProc(callback(0)accepts;-2,-3and constants of them reject) and per write ofELECTRON_DISABLE_SECURITY_WARNINGS(process.env,window).targetKind: "app"only where the receiver is Electron's app.commandLine by scope (require,import, interop helpers, webpack's module ofrequire("electron"),const cl = app.commandLine,const { commandLine } = app); any other receiver (a parameter, a commandLine not read,["appendSwitch"]on one) gives a row withtargetKind: "unknown", a dangerous name theredangerous: null; only a local object literal gives none. Dangerous:ignore-certificate-errors,ignore-certificate-errors-spki-list,allow-insecure-localhost,disable-web-security,allow-running-insecure-content,no-sandbox,disable-gpu-sandbox,disable-site-isolation-trials,remote-debugging-port,remote-debugging-pipe,unsafely-treat-insecure-origin-as-secure,disable-setuid-sandbox,allow-file-access-from-files, anddisable-featureslistingIsolateOrigins/SitePerProcess(also*X,X<Trial,X:param); each justified from Electron's or Chromium's switch documentation. Names are trimmed, stripped of leading dashes, split at=and lowercased for the check; a name with other characters thana-z0-9-is undecided, never safe, anddisable-featuresvalues are compared case-insensitively. The callback is read by position: a rest parameter at or before it, a pattern at it, or a short parameter list withargumentsin the body isunknown; a non-zero number, a non-empty string or a constant holdingtrueaccepts; a BigInt, a separated number (1_0), an unparsable literal or an argument whose end is not found is not read (unknown). A commandLine reached through an object literal of the file gives anunknownrow unless the path ends at the literal's own function. Rows past the cap cost only their reading, so padding cannot hide a later dangerous call. Listeners:on,once,addListener,prependListener,prependOnceListener. Findingelectron.unsafe-switch; limitationswitches_truncated(200 per file, 2,000 in all, 2,000,000 steps per file; the cap drops only rows known to be harmless, and a truncated side makes the rule undecidable). Discord 0.0.414: 12 switch rows, none dangerous,disable-featureswith an unread value and two loops with unread names; Claude: 7 switch rows (none dangerous; three with unread names, and two on the commandLine handed to a helper,e.appendSwitch(t, …)ande.appendSwitch("blink-settings", …), withtargetKind: "unknown") and the local-pairing verify proc withaccepts: "unknown"(it passes only the constantsFEr/PEr, -2 and -3, which are not resolved in that 13 MB chunk; the same shape in a small file readsnever); Electron Fiddle 0.38/0.39: none. JS_SCAN_SCHEMA 34. See docs/electron-findings.md ("Command-line switches and certificate checks").- Policy rule
new-unsafe-switch(warn when a policy does not list it): a dangerous switch, accepted certificate errors or an accepting verify proc that the before build does not have; a row whose danger is not read is an unknown finding. Policy schema,examples/policy.json, SARIF ruleNewUnsafeSwitch, docs/cli.md (seven rules).
Fixed
- Comparing builds: the limitations that say bridge values or page calls of generic forwarders were not read now apply only in the files they name (#219), not to every file of the app.
context_bridge_module_unreadnames the exposing entry and the module of the value (at preload.js:3, at api.js:1; when a module was not located or lies deeper than is read, the other build may hold the members in any file, and it applies anywhere),bridge_calls_cappedandbridge_calls_unreadthe page scripts not read (at page.js:1), andbridge_forwarder_ambiguousthe page scripts read that name the api. One of these that names no file applies anywhere, as before, unless its message ends with "No page calls are missing.", which is written only where that is established: an ambiguous api no page script names, and files not read that are only workers that were not located (their calls are read as a page's). A one-sided item in those files is unknown, an item elsewhere (a main-process handler) is added or removed again; a message that names only some files (… and N more) still applies anywhere, andipc_wrappers_cappedstays app-wide.bridge_global_writtenstays out of the map. A test walks every limitation code the core can emit and fails when one is neither mapped nor in a commented exemption list. - IPC: a loop variable is not bound where a nearer declaration of the name hides it (#225).
function reg(o) { for (o of Object.keys(T)) { const o = "t:inner"; ipcMain.on(o, h); } }gaveT's keys as channels (via resolved); the loop variable is now bound only where the binding lookup gives that loop's variable (for a head without a declaration, the binding it assigns), so the row has no channel in plain code, andt:innerwhere a bundler or minifier renames the inner name; hidden at some places of the read range and not at others, the loop is not read. The same holds for loops over lists (for (const o of ["a", "b"]) { const o = …; }). For avarhead, everyvarof the name in the same function is that binding (twofor (var o of …)loops in one function or file, avar o = 1after the loop): only a parameter, acatch, or a block'slet/const/class/functionhides it. Scan schema 33.
Install: npm install -g hexvero@0.11.0 · Run: npx hexvero@0.11.0 ui
v0.10.1
Fixed
- Comparing generic bridge forwarders (#221): a forwarder's allow-list (
allowedon the contextBridge row) is compared as a set, so the same channels written in another order or twice are no change; a channel added to or taken out of the list stays a change of every member that checks it (and of a page call it now blocks,ipcallowedyes → no). The minified form of an allow-list check that returns the call,i.includes(e)?n.invoke(e,r):Promise.reject(…)(esbuild's form ofif (LIST.includes(ch)) return ipcRenderer.invoke(…); return …), is read as the check, so a minifiedrequestmember hasallowedlike the plain one instead of none. A page call of a channel the allow-list refuses isallowed: "no"only when nothing else in the member can pass the channel on: with another use of it (guard: "unknown") only the exit-first form (if (!LIST.includes(ch)) return;before every call) keepsno; behind a check around the call (if (check) return call; return fwd(ch),check && call; fwd(ch),check ? call : require("electron").ipcRenderer.invoke(ch)oripcRenderer["invoke"](ch)) the other path may still reach the call, so the call isunknown(it wasno). Lines added around the forwarders give no contextBridge or IPC change, plain, bundled and minified. On holdout8 the three changes~shopping.send,~shopping.request,~shopping.receivebetween Gristmill v1 and v2 remain: v2 renamesshopping:cleartoshopping:clear-checkedin that allow-list, which these rules count as a change, while the set's expectation does not. Electron Fiddle 0.38.0 → 0.39.0, Discord and Claude.app: contextBridge and IPC rows unchanged. JS_SCAN_SCHEMA 31. - IPC: a table's keys are channels (
via: "table") only where the name means that table's loop variable (#209).for (let o of Object.keys(t)) f(o);followed by anothero— a callback parameter (Object.values(t).forEach((o) => ipcMain.on(o, …))), a function's parameter, an outer binding, or a read after the loop (also of avaror of a loop variable declared elsewhere) — gave the keys oftas channels; esbuild's minifier writes such reused names. The loop variable is now tied to its loop by scope: the use must be in that loop's body, and the resolver's binding lookup must give that variable there, not a parameter, a nested loop's variable or another declaration of the name. The same holds forfor (const [name, …] of LIST)over a list of pairs and forfor (const k in T);if (k in T)is no loop. A loop's body is read as one statement: a block, a bracelessif … else …, a nested loop,do … while,try,switch, a labeled statement, or expressions joined with commas (for (…) f(o), ipcMain.on(o, h);, as esbuild minifies) up to;or, in code without semicolons, the line break before the next statement; so the values ofObject.entriesandObject.valuesloops in a comma body are found too, and a call on the line after a braceless body (for (const ch of ["a"]) h(ch)⏎ipcMain.on(ch, h)) no longer reads the loop's values.for await (…)is read asfor (…)is. A braceless loop as the last statement of a file is read to the end of the file; a line that starts with a prefix++,--,!or~after a complete value, a line break afterreturnoryield, and a line break after an arrow's block body end the loop's body. No change on the corpus, Discord, Claude or Electron Fiddle 0.38/0.39. Scan schema 32.
Install: npm install -g hexvero@0.10.1 · Run: npx hexvero@0.10.1 ui
v0.10.0
Added
- The Electron runtime of a packaged build (
runtimelist,data.runtime, aRuntimeblock inanalyze): for a macOS.app, a Windows or Linux install directory and a release ZIP, the Electron, Chromium and Node versions read from the bytes of the Electron binary (Chrome/… Electron/…and Node's release URL; known only when every occurrence agrees, so a universal binary's two slices must match), the fuses decoded from the wire after Electron's sentinel and named by Electron's schema version 1 (runAsNode…wasmTrapHandlers; a position past the schema, another schema version or a disagreeing wire isunknown), the ASAR integrity record (macOSElectronAsarIntegrityinInfo.plist, Windows the.exeresourceIntegrity/ElectronAsar; whether it covers the archive Electron loads, and whether each entry's hash matches that archive's header; each archive hashed once, at most 64 MiB of headers per build), and whether the Electron line is supported or past its end of support by an offline snapshot of Electron's release data (dated,packages/core/src/data/electronReleases.ts; refreshed by hand withscripts/electron-releases.mjs, never in CI; never a claim about vulnerabilities). Each row names the file and offset it was read from. On Windows and Linux the binary is the executable the packaging names after the app'spackage.json(or, on Windows, the.exewith the integrity resource); the 3 largest unnamed PE or ELF files are scanned too, and only when no named file holds markers is one taken by its markers (up to 8 scanned; it needs both a user agent and a fuse wire); any scanned files whose markers disagree make the runtimeunknown. At most 4,096 strings that start like a marker are examined per kind (a file of repeats stays one pass). Support issupported,past end of support(from the end-of-life day itself) orprerelease, with the day it was judged on. In a ZIP a candidate is inflated only when its first bytes show a PE or ELF file, and all candidates inflated count against the target's size limit. A fuse wire of an unknown schema version makes thefusesrowunknown. Bounded: a binary over 1 GiB is not scanned, at most 64 occurrences per marker are kept (more make the valueunknown), metadata files have their own caps; a missing binary givesunknownrows and the limitationelectron_runtime_not_read. An.asaror app directory alone has noruntimelist (shipped: false, "not shipped in this input"). Measured on Claude 2.42304.0 (Electron 44.7.2, Chromium 152.0.7977.149, Node 24.21.0, fuses010011011, integrity coversapp.asar), Discord 0.0.414 (42.11.8,001000011, the record covers onlydefault_app.asar) and LM Studio 0.3.31 (37.7.1,10110001, past end of support since 2026-01-15). docs/cli.md, "The Electron runtime"; docs/electron-findings.md, "Runtime of a packaged build". - Policy rule
fuse-weakened(warn when a policy does not list it): between two packaged builds, a fuse that moved to its less safe state (runAsNode,nodeOptions,nodeCliInspect,grantFileProtocolExtraPrivilegeson;embeddedAsarIntegrityValidation,onlyLoadAppFromAsar,cookieEncryptionoff), integrity coverage of the loaded archive lost, or the runtime gone past end of support; a state not read on one side, or a watched fuse the after build's wire does not hold, is an unknown finding, one packaged build only iscollection_missing. Policy schema,examples/policy.json, SARIF ruleFuseWeakened, docs/cli.md. remoteEnabled:@electron/remoteturned on, read from tokens. One row perenable(…)andinitialize()call on the main module (require("@electron/remote/main"),import, the bundler interop forms, destructured and renamed imports,require(…).enable(…)as Joplin writes it,(0, R.enable)(…)), with the target named as navigation handlers name it (window,appinsideweb-contents-created,unknownfor a helper's parameter, else the argument as written), and oneuserow per file that loads@electron/remoteitself. A parameter or another local of the same name, and a name written after its declaration, give none. The findingelectron.remote-enablednames the targets, or says "enable not found in the files read" when only loads are seen. Policy ruleremote-enabled: a target newly enabled; a policy that does not list it runs it atwarn. See docs/electron-findings.md ("Remote module turned on").- contextBridge: a bridge object built in another module is read there, member by member (R3): an imported export (
exposeInMainWorld("SignalContext", MinimalSignalContext)), members named after objects or functions of the file or of other modules ({ auth, platform }givesauth.loginRequest, three levels deep), spreads of namespaces ({ ...Misc, ...Library }, also esbuild's__exportgetters), and an object a function of the file returns (createApi(new IpcClient("padwright"))). An object some module may change, an export that is not defined once, and a module that is not among the files leave the entry as it was and are named incontext_bridge_module_unread. holdout7 bridge members 11 of 52 → 52 of 52. See docs/electron-findings.md ("Bridge objects of other modules and functions"). - contextBridge and IPC: generic forwarders (R1). A member that passes its parameter on as the channel (
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), electron-react-boilerplate's nestedipcRenderer.sendMessage) saysforwards, withallowed/prefixonly for a check that is a statement of the function's own body and decides every IPC call (if (!LIST.includes(ch)) return;,if (LIST.includes(ch)) { … },LIST.includes(ch) && call, and what esbuild makes of them; a check in a nested block, atry, a loop or aswitch, an inverted one, or a list some call may change isguard: "unknown", as is any other use of the channel); the page scripts' calls of it (window.electron.ipcRenderer.sendMessage("x"),const { ipcRenderer } = window, aliases, destructured members) areipcrowsvia: "bridge"withmemberandallowed. A page script that may replace the api as a global (window.api = …,window["api"] = …,Object.assign(window, …),Object.defineProperty(window, "api", …), a classic script'svar api = …) is named inbridge_global_written, the page rows listed as read; worker scripts, files that take ipcMain from electron and files with awithstatement are not read (bridge_calls_unread); no rows when another exposure of the api was not read. New limitationsbridge_forwarder_ambiguous,bridge_calls_capped. See docs/electron-findings.md ("Generic forwarders and the page's calls of them"). - IPC: channel-forwarding functions of another module give rows at their calls (R4), as Heroic (
addHandler), Insomnia (ipcMainHandle), Rocket.Chat (handle as handleOnMain) and a curriedmakeHandlerInvoker("x")write them, withvia: "wrapper"and the module and export inwrapper.
Fixed
- The app inside a packaged build follows Electron's order since Electron 15.0.0-beta.6 (electron#30667, backport #30900):
app.asar, thenapp/, thendefault_app.asar(shell/common/node_bindings.cc;app/came first before). Before, hexvero readapp/when both were there, which Electron 15 and later do not run whenapp.asarloads. When the candidates would give different apps under the two orders or with the fuseonlyLoadAppFromAsaron, hexvero reads the fuse wire and the version from the runtime binary and picks by them (a schema 1 wire of 6 or more positions:app.asarfirst; fewer:app/first and no such fuse; without a wire the version, with 15.0.0 prereleases before beta.6 and 15/16 nightlies left open; Electron 14 and earlier have no such fuse; a wire and a version that disagree leave the order open) (with the fuse on and no loadableapp.asar, Electron runs no app:electron_app_not_read);electron_app_pick_unverifiedis left for a version or fuse that could not be read, and says what Electron would run otherwise. The pick in a ZIP passes on its warning totarget.info.container.warningtoo. Apackage.jsonthat is JSON but not an object is judged by the version (#216): Electron 45 and later pass over it; earlier versions run a list, string or number, andnullmakes them run no app; with the version unread and the pick depending on it, the pick stays open.fuse-weakenedtreats a fuse marked removed (r) before as not known to move (an unknown finding), also when it is not read afterwards. - Caps that dropped data without a word now say so. A helper called at more than 64 places (the call sites the resolver reads) gave the values of the first 64 as if they were all, e.g. 64 of 70 IPC channels with no limitation; the call now also stays open (a row without a channel,
ipc_channel_unresolved), a storage wrapper called that often, also through another helper called that often, gets its unknown row at the wrapper's key, a window options parameter is not followed, and afetch,new WebSocketor other request whose address is read through such call sites keeps an unknown row next to the addresses read. A function that takesipcMainoripcRendereras a parameter and is called at more than 64 places (by name or through an export key) gives no row from the first 64 alone, and says so withipc_wrappers_capped, which a comparison now reads as IPC values not all listed (an IPC item missing on that side is unknown, not removed); so does the search for such calls when it runs out of its budget, which left the rows out without a word. The per-file limitations list keeps 200 entries and always the first of each code, so 200 of one code (webpreferences_unattached) no longer push outscan_truncated,ipc_wrappers_cappedand the others the blind spots and the comparison read; the rest are counted in one entry per code.coverage.partialcounts every file read only up to the token budget, als...
v0.9.9
Security
- MCP
hexvero_exportandhexvero_recover: the write policy judges every component of a path by where it leads, also a symbolic link whose target does not exist yet (a link in a sticky directory only when you or root own it), and is checked again when an export is opened (the opened file must be the one it resolved) and before every directory and file a recovery creates, each recovered file also opened and compared before its content is written; directories on the way that other users can change (owned by another user than you or root, world-writable, or group-writable unless the group is provably your user-private group on Linux, orHEXVERO_TRUST_GROUP_WRITABLE=1is set) are refused unless sticky, including the ancestors of the root; paths are compared in their canonical form (native realpath: stored case, Windows mount points and junctions), so a path written in another case inside a root is no longer refused; before, such a link was not resolved, andhexvero_exportwithoverwrite: truecould write through it outside the allowed roots. Exports (MCP and CLI) never write through a symbolic link at the path itself: a new file is created exclusively and an existing one is opened without following links (O_NOFOLLOW where the platform has it).compare --report,check --outandcheck --update-baselinerefuse a symbolic link at their path the same way, also with--force; so doesinit, which now writes through the same code (init --out -writes the policy to standard output).-writes to standard output, and/dev/stdout,/dev/stderr,/dev/nulland/dev/fd/N(process substitution) are written to as the shell connected them (a terminal, a pipe, or the file of a redirection such as--out /dev/stdout > out.json), opened once. For MCP exports, a file with other hard links is not replaced. - Release workflow:
verifyalso runs the end-to-end flows, the site demo check and the corpus gates, without a dependency cache, and a Windows and macOS test job must pass before anything is published, so a tag no longer publishes without the checks CI runs. The Review workflow builds and runs the pull request's code in a job with a read-only token; a separate job without that code posts the report, cut at 60,000 characters, and updates only a comment it wrote itself. The MCP registry publish follows only a Release run started by a tag push, not a rehearsal started by hand. Checkouts that do not push keep no credentials (persist-credentials: false).
Fixed
- Output to standard output stays parseable: with
--out -,--out /dev/stdout,export <path> -,check --format sarif --out -orcompare --report -, the summary and the confirmation line go to stderr; exports end with a newline; a closed pipe (| head) ends the output quietly. A FIFO or device at a path that is not a stream name is refused instead of blocking, and an MCP export refuses a stream name outright. compare <a> <b>without a collection prints the window, setting and values of changed windows, the lists only one side has, partial gaps, and therecoversummary through the same escaping as the other output, so a line break or escape sequence in an archive entry name cannot add lines of its own.- Suggested commands (
analyzenext steps in text and in--jsonnext[].command, thedemosummary, the baseline hint ofcheck) quote paths for a POSIX shell in single quotes, as the reproduce lines of reports already did; before, they used JSON string quoting, which a shell does not read the same way ($(…), backticks and$VARinside it are expanded). A path is no longer read as a replacement pattern when it is put into a suggestion. On Windows they use double quotes, and only for paths of plain ASCII characters that neither cmd.exe nor PowerShell expands; any other path, and on every platform a path with control characters or bidi or zero-width marks, is shown as<path>rather than altered. A path starting with-gets./. Reproduce lines in reports stay POSIX on every platform, so a report is the same bytes everywhere. check(CLI, MCPhexvero_check): a build with no JavaScript to read, an empty directory, a directory without JavaScript, or one whose JavaScript files are only symbolic links out of it, is refused with exit1instead of passing with exit0; the message names an.asarfound at the top or inresources/. An app withoutpackage.jsonat the top is still checked and gets a warning in the newwarningslist (text, JSON and Markdown output). With a baseline, app directories get a content digest ina.sha256/b.sha256(it was empty), over the files the analysis lists and within its limits, with paths in NFC, so a baseline written against other builds warns for them too; a partial digest is named in the baseline warnings.recoverandhexvero_recover: module and source names from the analyzed bundle no longer create hidden files or directories in the output directory; a leading dot in a path segment becomes_(./.ssh/authorized_keys→_ssh/authorized_keys.js), as docs/mcp.md already said. A segment named after a Windows device (con,nul,aux,prn,com1–com9,lpt1–lpt9, also with an extension) gets a leading_on every platform, each recovered file lists the name it was written for (written[].name), and the source-map manifest maps every source to its file (files). An output directory that is a symbolic link to a missing location is refused asinvalid_inputinstead of failing withinternal.- After the second review of #207: an exposing function called more than once (
if (EARLY) expose(); api.c = …; if (!EARLY) expose();) counted only the writes before its first call and droppedapi.cwithout a word; a write between the first and the last call is now one unreadable entry, and one after the last call is not exposed. In the wrapper's own file (one bundle), a replacement through a computed key that is a string constant (const k = "handle"; a[k] = f,Ipc.prototype[k] = f) or a loop over a list of strings (for (const m of ["handle", "on"]) Ipc.prototype[m] = f) blocks those methods withwrapper_method_replaced, as the plain build read across modules does; a key that cannot be read (a[process.env.M]) leaves the file unchecked. - contextBridge members, after review of #207: an exposure inside a function that runs from the top of the file (
function expose() {…}withexpose()there,setTimeout(later), aDOMContentLoadedorprocess.once("loaded")listener, an IIFE) lost the members written at the top before it (an unreadable entry instead); they are read again, and a write after a listener's registration is one unreadable entry. A write in asetup()called before the exposure, or in an IIFE before it, is a member again. A write in a loop that also holds the exposure (for (…) { if (i === 1) expose(…); api.x = …; },while/break) was dropped without a word; it is one unreadable entry now. A whole new object assigned in a comma sequence or a declaration list as minifiers write them (var { … } = require("electron"), n = {…}; n = {…}; n.more = …) is read in the minified build as in the source. - webPreferences, after review of #207:
[SB]: trueinsidefunction make(SB)besideconst SB = "sandbox"was read assandbox: truein the plain source when the file callsmake(process.env.X)(the bundles said computed): a computed key is now read only where its name is bound to that constant by scope, and the resolver no longer lets a parameter whose argument is not known, afor … ofvariable or acatchbinding fall through to a constant of the same name (alsoipcMain[M](…)insidefor (const M of ["on"])besideconst M = "handle", which this release read asipcMain.handle).get [k]() {…},async [k]() {},*[k]() {}(already before this release) and an unread computed key in a later argument ofwebPreferences: Object.assign({ nodeIntegration: false }, { [k]: true })left the earliernodeIntegration: falsestanding; they make it computed now, andget [NI]()/get ["nodeIntegration"]()make that key computed."preload": "p.js"and["preload"]: "p.js"are read as the preload script, as the minified build already did. A getter or quoted key in an object nested in the settings (inner: { get nodeIntegration() {…} }) is no longer taken for a setting. - Real apps with these fixes (#199–#202): Discord, Claude.app and Electron Fiddle 0.38.0 and 0.39.0 give the same rows in every column. The in-file check reads only files where a call of a wrapper method gives a row, and there only the writes that can replace a wrapper method (its name, a computed member,
Object/Reflectcalls), within 200,000 steps; it finishes on every file of these apps (the largest needs 86,000 steps, about 0.1–0.6 s per app). A computed member write in the wrapper's own file (this[t]=iin the setter of Fiddle'sAppState,this[M]=0in got'sRequestclass,this[t]=nin Claude.app'sindex.chunk-muI2IV6k.js) may replace a method but proves nothing: Fiddle's main and renderer bundles and that file of Claude.app are named bywrapper_replacements_unchecked, and their rows are read (counting it as a replacement dropped Fiddle's 62AppState.save/retrievestorage rows). Corpus: no row changed in any set. JS_SCAN_SCHEMA 26. - A block's own
const ipcMain = new EventEmitter()passed toregister(ipc) { ipc.handle("r2:x", f) }was taken for Electron's ipcMain (the name alone decided), and gave a falser2:xrow via parameter in the plain source (probe I; the bundles rename the local and were right). The name now counts only where its declaration there is not provably another object (new X(), a literal, an object literal, a function);const ipcMain = getIpc()is read as before. ipcMain["handle"]("x", f)andipcMain[M]("x", f)withconst M = "handle"outside a loop gaveipcMain.[unknown]without a channel in ...
v0.9.8
Electron analysis
- Storage keys passed through a wrapper class or object that another module defines are read at each call in the importing module:
import { local } from "./web-store.js"; local.get(LAST_PROJECT)withlocal = { get: (name) => localStorage.getItem(key(name)) }there giveslocalStorage read tidemark.lastProjectat the call,via: "local.get". The wrapper's own key expression is evaluated in its module with the argument read at the call (a literal, a constant, another module's constant such as a compiled string enum member). The receiver must be proven an instance (import,require+new,module.exports = new Store(),new (require(…).C)(), re-exports, one esbuild bundle plain or minified); a shadowing parameter, local, pattern,for … oforcatch, an assignment to the binding, or a member assignment of the method name gives no row; a call whose key is not read whole is an unknown row at the call. Uses the cross-module mechanism of the IPC wrapper classes. Corpus: holdout5 (development set) storage keys 7 → 16 found and 17 → 8 missed (tidemark plain, minified and v2), holdout6 22 → 33 found and 19 → 8 missed (snapdeck plain and v2, and the v2 change+localStorage snapdeck:ocrLanguage), no new false rows; Electron Fiddle 0.38.0 → 0.39.0 storage, endpoints and IPC unchanged. Not followed: plain functions as cross-module wrappers, and endpoints through a wrapper of another module. Stored scans are read again (JS_SCAN_SCHEMA 21). - Wrapper look-alikes that 0.9.6 read as calls (IPC and storage): a
staticmethod called on an instance (also by the in-file storage rules), a method that a module importing the wrapper (also throughmodule.exports = require(…)in any form) replaces or removes on the class, its prototype (alsoconst P = Store.prototype,Object.getPrototypeOf(s),s.constructor.prototype,s.__proto__) or an instance (s.get = f,s.get ||= f,[s.get] = …,delete s.get,Object.assign,Object.defineProperty,Reflect.set,Object.setPrototypeOf), or a write to the export itself (m.store = new Proxy(…)); limitationwrapper_method_replaced, while a write to another object of the same name changes nothing. Not seen: replacements through a global,require.cache,require(n)and an instance handed to a module that does not import the wrapper. For every other reading,module.exports = require("./x")now passes./xon likeexport *only when it is the file's one write of the exports, a top-level statement of its own that ends there (not conditional, not in a function, not followed by a tagged template or another continuation on the next line, not replaced or wrapped later, also not throughmodule["exports"]or an alias ofmodule); the replacement scan has one budget per application, read in an order that a file growing between builds does not change (defining modules first, then the rest, by path); a module past it makes the storage wrapper methods it imports count as replaced (flagged), while IPC wrapper calls are read as before with the limitationwrapper_replacements_unchecked, a name that a module's own export shadows overexport *or that twoexport *sources give, and a parameter the callee assigns or that another call passes something else for. These give no row now.
Install: npm install -g hexvero@0.9.8 · Run: npx hexvero@0.9.8 ui
v0.9.7
Release policies
- New rule
new-bridge-exposure: a privileged object a preload newly hands to pages as it is
(ipcRendereror one of its methods,shell, theelectronmodule, a Node module such asfs,
process,require), read frombridgeExposures. The same exposure in another preload file of
the before build is not new; an exposure hexvero could not decide is an unknown finding. A
policy that does not list the rule runs it atwarn, so upgrading never turns a passing check
red;examples/policy.jsonlists it atfail. SARIF ruleNewBridgeExposure, schema updated.
hexvero 0.9.6 and older reject a policy that lists the rule (exit 2, unknown rule); keep it
out of a policy that an older pinned version reads. A narrowed exposure (ipcRenderer→
ipcRenderer.send) is a different object and is reported as new.
CLI
- Commands hexvero suggests (
Nextafterdemoandanalyze, the hints under lists and
comparisons) are written asnpx hexvero …when npx started it, so they can be pasted as they
are;--jsonoutput is unchanged. Found by a first-time-user run of 0.9.4.
Web UI
- The compare view explains the
ipcgroup (the calls whose payload changed on an existing
channel), like every other list; before, the group had no line saying what it lists.
Performance
- Analysis of a large single-bundle app uses less CPU than 0.9.6, with identical results: the
window-options evaluation shares the scan's constants of the file instead of reading them again,
the bracket depth and parent tables are built in one pass, and the reassigned-client and
privileged-binding checks visit only the names they ask about. Discordanalyze --freshCPU time
1,990 → 1,760 ms (0.8.0: 1,560 ms); Claude.app unchanged (median, interleaved, on a shared machine
at load 28 to 71; docs/benchmarks.md has the method and the raw runs).
Electron analysis
- IPC calls with optional chaining give the rows of the plain call (channel, api, sender or receiver,
args):ipcRenderer?.send(…),ipcRenderer.send?.(…),ipcMain?.handle(…),event.sender?.send(…),event?.reply(…),win?.webContents?.send(…),contextBridge?.exposeInMainWorld(…), and the forms esbuild, TypeScript and Babel lower them to for older targets, plain and minified ((_a = ipcRenderer) == null ? void 0 : _a.send(…),(a = ipcRenderer.send) == null || a.call(ipcRenderer, …),null == e || e.send(…)). The lowered forms only where the temporary is assigned from the receiver in the same expression; another name after the null test, another receiver of.call, and.applygive no row. holdout (time-tracker, plain, bundled and minified):widget?.webContents?.send("theme:changed", …)is now read, IPC channels 66 → 69 of 72 and senders and receivers 107 → 110, 0 false; holdout5 and holdout6 unchanged (no such call there). Stored scans are read again (JS_SCAN_SCHEMA 20, the bump below). - A window whose options spread something unreadable before a whole
webPreferencesis no longer read as computed: innew BrowserWindow({ ...display.bounds, frame: false, webPreferences })withconst webPreferences = { contextIsolation: true, sandbox: true }the later key replaces anything the spread brought, so every setting is known (an unwritten one takes Electron's default) and the window is no longer named inwindow_options_unresolved. Only for awebPreferenceswritten in the window's own options whose object (a literal, a followed name, the objects every caller passes) nothing unreadable reaches; a spread inside it or in the declaration it names, a condition, and a spread after it ({ webPreferences: {…}, ...opts }) stay computed. A member of a window's options that may writewebPreferenceswithout the reading naming it — a quoted"webPreferences": …, a computed key[k]: …, a method or accessor — now counts like an unknown spread at its place: the keys written before it read as computed. Up to 0.9.6 such a later member was ignored, and the settings before it read as written ({ ...o, webPreferences: { sandbox: true }, ["webPreferences"]: o.p }gavesandbox: true). This also holds for a window without any spread:new BrowserWindow({ webPreferences: { sandbox: true, … }, [k]: 1 })read safe up to 0.9.6 thoughkmay be"webPreferences"; it now reads computed. The evaluation of an object reads a computed key whose value is one known string as that key ([KEY]: { sandbox: false }withKEY = "webPreferences"replaces the earlier object; 0.9.6 read the window as conflicting) and any other computed key,get [k]()and*gen()as possibly replacing every key before it;get NAME()/set NAME()/async NAME()name NAME.__proto__: …never replaces an own key, but a key the object does not write is taken from the prototype: in a window's options without an ownwebPreferences(new BrowserWindow({ __proto__: evil })) every setting reads as computed, and inwebPreferences: { __proto__: x, sandbox: true }the keys other thansandboxread as computed instead of not written (both read safe before). An accessor or async member that cannot be named (async *gen(),get [k](),get 1()) may replace any earlier key. - IPC channel tables (
const CH = { … }; ipcRenderer.send(CH.x)) read a later member that may replace an entry:[K]: "v"with K a constant is the entry K (it gave the earlier value, which is not the one sent), any other computed key, a spread,*gen()or an unnamed accessor make the earlier entries unknown, andget x()/x() {}makexunknown; the call then has no channel and is named inipc_channel_unresolved. A handler table with a prototype ({ __proto__: base, own: fn }read throughObject.entries) no longer lists__proto__as a channel. holdout6 overlay window:nodeIntegration/contextIsolation/sandboxcomputed → false/true/true in the plain, bundled and minified builds (the rows were already listed; the corpus score is unchanged). Alsonew n(t)under a minifier's short name forBrowserWindowcounts as the window takingt, asnew BrowserWindow(t)does, instead of handing the object on (which made every followed key computed). Stored scans are read again (JS_SCAN_SCHEMA 20).
Install: npm install -g hexvero@0.9.7 · Run: npx hexvero@0.9.7 ui
v0.9.6
Performance
- Analysis of large apps is faster than in 0.9.5, with identical results: a per-file text test, the
URLconstructor table, class-field owners and scoped enums are read once per file instead of per
call or per resolver. Discordanalyze --fresh995 → 953 ms, Claude.app 8.4 → 7.5 s (median,
interleaved, registry 0.9.5 against this build; docs/benchmarks.md has the method).
Install: npm install -g hexvero@0.9.6 · Run: npx hexvero@0.9.6 ui
v0.9.5
Electron analysis
- IPC payload shapes (case-study miss M1): the
ipcrow of a call that sends (ipcRenderer.send/invoke/sendSync/postMessage,webContents.send,event.reply,event.sender.send) carriesargs, the arguments after the channel as written:({dir, packageManager}, ...)names the sorted property names of an object literal argument,…ends a list whose names a...spreador a computed key leaves unknown,...is a spread argument and_any other value;not readwhen the call's brackets do not close.comparereports a changedargsas a changed call (the identity staysmodule+api+channel; repeats of one channel in a file that differ only there are still paired, not removed and added), and the release report lists those calls. Electron Fiddle 0.38.0 → 0.39.0:compare … ipcgoes from 157 unchanged to1 changed—preload.js::ipcRenderer.invoke::NPM_ADD_MODULES,args: "({dir, packageManager}, ...)" → "({dir, packageManager, useSocketFirewall}, ...)", the one change ofsrc/preload/preload.ts. Not read: calls through wrapper methods, helpers and names of other files (noargs). Keys are compared as written:{1.5: x}/{"1.5": x},{0x10: x}/{16: x},{["a"]: 1}/{a: 1}and a transpiler's spread helper (_objectSpread({}, o, {a}),(_)) against{...o, a}differ, and__proto__: pcounts as an unknown name. An export written by 0.9.4 compared with a new scan showsargson every sending call as a change (the 0.9.4 rows have noargs). Brackets of different kinds that meet (a cut-off file) make the callnot read, and a template literal is one argument whatever commas its${…}gaps hold. Stored scans are read again (JS_SCAN_SCHEMA 19).
Wording
- One minified bundle or native addon is named in the singular ("1 minified bundle", not "1 minified
bundles"); found by a first-time-user run of 0.9.4.
Install: npm install -g hexvero@0.9.5 · Run: npx hexvero@0.9.5 ui
v0.9.4
Electron analysis
- Two false client rows that 0.9.2 also reported are gone (#157). A reassigned client binding (
let h = require("https"); h = fake; h.get("https://…"), alsoh ??= fake, and calls before the assignment) no longer falls back to the name rule: where the name means that declaration the call is an unknown row, not the address; an assignment of another client module (if (x) https = require("http")) keeps it a client. A webpack localconst r = n(7)is the client only when module 7 of the same file passes a client module on (7(e) { e.exports = require("https") }, now also a proven binding under any name); when module 7 is not in the file, the call is an unknown row instead of the address, and when it passes another module (fs) there is no row. A local bound byrequireto another module is that module, except under a client's own name (const axios = require("./http")stays a client). Probes against 0.9.3: every other row unchanged;const r = n(5); r.get("https://wp-real.example/")with module 5 outside the file is now an unknown row (it cannot be decided there). Electron Fiddle, holdout5 and holdout6 unchanged.
Install: npm install -g hexvero@0.9.4 · Run: npx hexvero@0.9.4 ui