Skip to content

v0.10.0

Latest

Choose a tag to compare

@hexsecs hexsecs released this 01 Oct 02:47
· 7 commits to main since this release
4468245

[0.10.0] - 2026-09-29

Security

  • Running the tests no longer touches the operator's own configuration, state, or CAN bus (#531). The suite depended on the machine it ran on. A single full run left two fuzz campaign directories in the real ~/.canarchy/findings (thirty-eight had accumulated on the machine where this was found), and with a ~/.canarchy/config.toml selecting udp_multicast and a default interface, nine tests failed and python-can opened a real multicast socket. The default transport backend is python-can on socketcan, so on a machine with a live interface an ordinary pytest run could reach real hardware. Two autouse fixtures now apply by default: state is redirected to a per-test temporary directory with every CANARCHY_* variable cleared, and any attempt to open a real CAN interface fails immediately, naming the test and the interface. python-can's in-process virtual bus is still allowed, as is the localhost server the web tests bind. Tests that genuinely need the real environment are marked integration, excluded from the default run, and selected with -m integration. Configuration-sensitivity is deliberately not hidden: the uds services regressions from #530 still set a configured default interface through their own fixtures, verified by reintroducing that defect and confirming they still fail under isolation. The suite now reports the same result with and without an operator config present.

  • A configured default interface no longer turns the UDS service catalog into active bus probing (#530). uds services is dual-mode: without an interface it lists the static UDS service catalog, with one it probes an ECU. The default-interface fallback was applied before that distinction was evaluated, so on any machine with [transport].default_interface or CANARCHY_DEFAULT_INTERFACE set, a bare canarchy uds services — and the MCP uds_services tool, which has no interface and no acknowledgement parameter at all — transmitted 17 diagnostic requests to the configured bus instead of answering from the catalog. An agent asking for a reference lookup could not have authorised that and had no way to see it coming. uds services now opts out of the default-interface fallback entirely: active probing requires an interface on the command line, the catalog is returned before any transport client is constructed, and the answer is the same under either CANARCHY_REQUIRE_ACTIVE_ACK setting (previously the "safe" setting turned the wrong behaviour into an outright error). The MCP server additionally pins the exact argv that reference-only tools may produce and fails closed with REFERENCE_ONLY_TOOL_VIOLATION rather than running anything else. Explicit CLI probing is unchanged. One visible consequence: uds services --dry-run with no interface now plans against interface: null rather than the configured default, which is what the corresponding real invocation would do. Sibling dual-mode commands were audited: xcp commands, j1587 pids, and j2497 mids are reference-only and outside the fallback set, and j1939 pgn / j1939 spn switch on a capture file rather than an interface, so none shared the defect.

Added

  • Traffic investigation starts from a linked identifier explorer (#547). The TUI groups frames by bus and standard/extended CAN ID, showing count, recent rate, age, last payload, changed-byte offsets, and activity rather than making operators chase repeated frames through a scrolling log. v opens the chronological log; selecting history freezes the complete raw hex/bit and decoded-evidence inspector without stopping capture, with visible incoming/loss counts and F4 to return live. Filters for ID, PGN, and J1939 source address use the same frame grammar as CLI filter (which now accepts sa==); invalid typed filters and unknown sort fields report an error instead of silently changing the view. History and memory are bounded by the backlog cap.

  • DBC decode results appear in the Traffic explorer (#547). Decoded-message frames and correlated signals now populate the identifier summary and event log, so an offline decode --dbc run can be inspected without a separate raw-frame command. Live follow respects active traffic filters, returns to the newest visible event after historical inspection, and honors backlog increases as well as decreases.

  • The TUI now uses responsive, task-focused workspaces (#546). Traffic, Signals, J1939, UDS, and Findings get the full body height one at a time instead of sharing a fixed four-pane grid. Alt+1–Alt+5 switch views, F3 expands the compact activity band, and the selected row has a contextual inspector on wide terminals or an Enter drill-down on narrow ones. First-run guidance points to live capture and the offline demo; workspace switches and resizes keep the selection, retained rows, and running capture. Leaving drill-down restores table keyboard focus, and widening the terminal returns to the side-by-side layout automatically.

  • Synthetic datasets that work with no network access (#460). Every dataset in the catalog provider downloads from a host (zenodo.org, figshare.com, huggingface.co, ocslab.hksecurity.net) that a typical sandboxed agent environment blocks, so the documented dataset workflow could not run there at all — and failed late, as a transport error deep inside a replay. A second offline provider now ships four synthetic datasets generated locally from protocol rules: can-basic (classic CAN with a discoverable counter and checksum), j1939-basic (J1939 with a BAM/TP sequence and a DM1 active fault), can-intrusion (attack-labelled intrusion-detection rows), and signal-decoded (pre-decoded per-ID signal columns). datasets fetch offline:<name> writes real data into the cache and returns its path, after which datasets convert, datasets stream, stats, j1939 summary, and replay --file work against a local file with no egress. Generation is deterministic, so a given dataset always produces the same bytes and provenance hashes stay stable across machines; nothing is downloaded and no third-party dataset content is redistributed. The data is synthetic and says so in its licence string, a SYNTHETIC: description prefix, and a synthetic: true machine field — it is for exercising tooling, not for research findings.

  • Review fixes for the offline provider (#460): the generated BAM sequence is addressed to the global destination 0xFF rather than node 0x00, matching the 0x20 BAM control byte in its announcement, so protocol-aware consumers no longer see a self-contradictory session (generator version bumped to v2, which changes the cache path so no stale data is reused). Generated datasets are published atomically, so a write interrupted by a full disk cannot leave a truncated file that the next fetch serves as cached. A cache-write failure now exits 2 rather than 1, separating a storage problem from bad input. And datasets fetch on a provider that materialises data reports data_is_local: true with next steps naming the local path — previously it said "Provenance only — no data was downloaded" and pointed at datasets download / datasets replay, both of which fail for a generated dataset while the data sat in the cache.

  • Sessions are reproducible research records (#504). session save now records input provenance — a SHA-256 hash, size, and modification time for every capture and DBC it names, the provider repo/commit/ref when a DBC came from the provider cache, the effective configuration, an environment snapshot, and the CANarchy version — alongside the context it always kept. Five new subcommands build on that: session verify re-hashes every recorded input and artifact offline and reports what changed, what is missing, and what is needed to reproduce the analysis (exit 1 when degraded, --root for relocated inputs); session attach records an analysis output with its hash, the command that produced it, and the inputs it derives from (--embed keeps the result inside the record); session annotate appends timestamped operator notes that can target recorded entries; and session bundle / session import move a whole session — manifest plus copies of its files — to another directory or machine without depending on the original absolute paths. Each is also exposed as an MCP tool. Sessions saved earlier keep loading unchanged and report provenance_available: false with an explicit warning naming the command that upgrades them; nothing in the session surface executes a recorded command, opens a transport, or serializes credentials.

Documentation

  • Added a maintainer launch packet with ecosystem listing copy, a released-package demo without CAN hardware, MCP registry prerequisites, and search-engine submission instructions (#484).

Changed

  • Docs pages carry their own search and social metadata (#482). All 41 user-facing pages — getting started, the user guide, every tutorial, and every cookbook recipe — now set a unique description: in front matter instead of inheriting one site-wide sentence, and Material's social plugin renders a per-page card so a shared docs URL unfurls with that page's own title and summary. Internal design specs, test specs, and benchmark records stay published and searchable on the site but are marked noindex and dropped from the sitemap, which now lists 41 user-facing pages rather than ~140 mostly internal ones. The docs workflow also builds (without deploying) on pull requests, so a broken docs build is caught before it reaches the deploy path.

  • Homepage is now static HTML and CSS (#478). The landing page previously shipped an empty <div id="root"> filled in by React development builds and an in-browser Babel compiler, so crawlers that do not execute JavaScript — and every social scraper — saw no page content, and visitors paid for a compile on every load. Headings, copy, comparison table, and links are now in the initial payload, client-side script is limited to the mobile menu and the install copy button, and the Pages build fails if that content or the no-framework guarantee regresses. The hero and shell release tags now read 0.9.2 instead of the stale 0.7.0.

Fixed

  • Decoded signals no longer appear twice in the TUI (#550). Parent decoded_message and child signal events now share a source-frame index, so the Signals pane displays one row per observation while preserving units and repeated equal-valued frames. The fold keeps the best available numeric timestamp for future trend views and conservatively retains legacy events without a correlation index instead of guessing from values. Standalone signal-event timestamp propagation remains tracked in #525.

  • TUI commands no longer lose non-event answers (#521). Stats, DBC inspection, doctor, config, J1939 reference lookups, errors, help, and version text now open a scrollable, selectable result view instead of updating invisible fold state or printing behind the full-screen app. F2 switches between results and live panes, Esc returns to panes, and retained event rows and capture continue unchanged. Explicit JSON/JSONL commands remain available in the result view. The active-command guard still refuses transmit operations when --help or --version is used as a positional value after --.

  • /stop in the TUI no longer acts on arguments it cannot parse (#542). _run_slash discarded whatever followed /stop, so /stop " ended a running capture instead of reporting the unmatched quote — a destructive action taken on exactly the input the documented contract says to refuse. /stop now takes no arguments: an unmatched quote, a dangling backslash, or an ordinary extra token is reported in the alerts log and the capture keeps running, while a bare /stop behaves as before. REQ-TUI-15 has been narrowed to the commands that actually tokenise (/capture, /stop); /filter and /sort intentionally take raw text so a filter needle can contain a quote character, and they validate their pane argument rather than tokenising.

  • datasets provider list explains an empty provider set instead of printing a bare heading (#543). With every provider disabled the command that exists to show the effective provider configuration rendered only its title — no providers, no search order, no reason. Text mode now reports (none registered), Search order: (none), and the [datasets.providers.<name>] key to re-enable one. JSON output is unchanged: providers and search_order were already empty lists and stay that way, with no human decoration added.

  • MCP schema descriptions no longer tell agents that dataset refs and session names must be filesystem paths (#544). The - restriction note added with the stdin guard opened with "Must be a real filesystem path", and it is appended to every restricted parameter — including ones that take a dataset ref, a remote URL, a manifest file id or a session name. datasets_replay_plan.source published as "Dataset ref (e.g. catalog:candid) or remote candump URL. Must be a real filesystem path", which would steer an agent away from the supported value. The note now states only that - is unavailable and why, leaving each parameter's own description to say what it accepts. Runtime behaviour is unchanged — - is still refused with STDIN_MCP_EXCLUDED.

  • Argument errors from the canarchy command now honour --json and --jsonl (#519). Every argparse failure from the installed console script printed the human-readable command: / error: / hint: block even when the caller explicitly asked for machine-readable output, so an agent needed one parser for runtime errors and a second for usage errors — the failures automation hits most often. The console script enters main() with no argument list, and the requested output format was detected from that empty list rather than from the real command line, so the format was resolved as text before argparse ever looked at sys.argv; every in-process test passed an explicit argument list and could not see it. The effective argument vector is now resolved first and handed to both the format detection and the parser, so unrecognised options, unknown commands and subcommands, missing required arguments, and values that fail type conversion all return the canonical INVALID_ARGUMENTS envelope in the requested format, still exiting 1. Text output is unchanged for callers that do not ask for JSON, and successful command output is untouched. When several output flags appear together — itself a usage error, since they are mutually exclusive — the envelope reporting it is rendered by the fixed precedence --json > --jsonl > --text > --table, matching what successfully parsed commands already do. Regression coverage runs the installed console script as a real subprocess, because that is the only route that reproduces the defect.

  • Opening /help in the TUI no longer erases the captured history (#517). The shared hotkey layer reported /help and /clear with the same "handled locally" disposition, and the full-screen app reset every pane on it — so asking for the hotkey table emptied the traffic, decoded, J1939, and UDS tables, dropped the retained row stores behind them, and posted a misleading panes cleared alert. Because the stores were discarded too, removing a filter could not bring the history back. The disposition now distinguishes a read-only handler from one that clears pane data, and only the clearing signal authorises a front end to discard its view state. /help leaves rows, row stores, counters, filters, sort state, and the live capture untouched, during capture and while the feed is paused; /clear and the c key still clear everything.

  • A typo in a TUI slash command no longer kills the application (#518). Typing /capture " raised ValueError: No closing quotation out of an unguarded shlex.split and tore the full-screen app down with a traceback, losing the analysis view — the ordinary command path already caught this, the slash path did not. Slash arguments are now tokenised behind the same guard, so an unmatched quote or a dangling backslash is reported in the alerts log and the session keeps running. /capture also validates before it touches the capture session: it takes exactly one interface, and no argument, an empty or whitespace-only token (/capture "", which previously started a capture on nothing and announced capture started on ), or extra arguments (/capture can0 --candump, previously silently ignored) are each refused with their own diagnostic. Nothing rejected reaches the capture path, so a running capture is neither stopped nor replaced and the displayed history is preserved.

  • [datasets].search_order now actually decides which provider a dataset ref resolves against (#514). The key was parsed out of ~/.canarchy/config.toml and then thrown away: the registry built its order from hard-coded registration order, so an operator who set search_order got no reordering, no warning, and no error. That was harmless while catalog was the only provider; with the offline provider shipped there are two, and provider order is what a bare ref (road, no provider: prefix) is resolved by — so the setting that was meant to control it silently did nothing. Providers are now registered in the configured order, and the effective order is reported by datasets provider list as a search_order list, a per-provider order index, and a Search order: line in text mode. The default is unchanged and now pinned by a test that uses two providers sharing a dataset name: catalog comes first, so a bare ref still prefers the real dataset over synthetic data. A provider that is enabled but not listed is appended after the listed ones rather than dropped — search_order is a preference, not an allow-list, so a partial list never quietly makes offline: refs unresolvable; [datasets.providers.<name>].enabled = false remains the way to exclude one and still wins over listing it. An unknown provider name in the list now fails every datasets command with a structured DATASET_PROVIDER_NOT_FOUND (exit 1) naming the entry and the known providers, and a search_order that is not a list of names fails with DATASET_SEARCH_ORDER_INVALID, both as JSON errors rather than tracebacks.

  • An MCP file parameter set to - no longer wedges the whole agent session (#516). - is a documented CLI sentinel meaning "read the capture from stdin", and the MCP server forwarded it straight through to the CLI. But the MCP server owns stdin as its JSON-RPC transport, so the reader consumed protocol bytes instead of capture text: capture_info with file: "-" never returned, and every later tool call in that session was starved behind the stolen stream. One schema-valid argument value silently killed the connection, with no error and no way for an agent to tell what had happened. The server now refuses - at the tool boundary with error code STDIN_MCP_EXCLUDED, on every parameter it can reach — capture and DBC paths, path arrays such as compare.files, session artifacts and bundles, dataset replay inputs, and the fuzz/replay file inputs. The refusal is reported as an MCP tool failure and carries the canonical envelope with a hint naming what to pass instead, and because it happens before the CLI argv is built, no reader and no worker thread is ever created against the protocol stream. Each affected parameter's schema description now states the restriction, so an agent learns the rule from list_tools rather than by hanging. The CLI pipelines are untouched: capture-info --file -, stats --file -, filter --file -, and the --stdin JSONL variants work exactly as before.

  • SynCAN is no longer catalogued as MIT-licensed (#513). datasets inspect syncan reported a permissive MIT licence, but the dataset ships bespoke ETAS GmbH / Robert Bosch GmbH terms that allow non-commercial use only, forbid redistribution of the dataset or modified versions, and require a citation in any work that uses it. Operators and agents acting on the old label could have re-hosted or commercialised data they have no licence to. The catalog entry now states the real terms and links the licence text, and a new test fails whenever a catalog entry advertises a permissive licence identifier that has not been checked against the publisher's own terms.

  • TUI capture drain no longer crashes during teardown (#509). The 0.1s drain timer started at mount kept firing while the app exited, after the widget tree was gone, so a status refresh could raise NoMatches from an unhandled timer callback. The drain now stops once the app is shutting down, and the status, J1939 ribbon, and alert helpers tolerate an absent node.

  • Guided-fuzz review fixes (#503). Raw adapter I/O failures retain transport diagnostics rather than being reported as disk errors. Non-finite rate/duration values are rejected as usage errors before archive creation in active and dry-run modes.

  • Guided fuzz findings survive corpus pruning and interrupted runs (#503). Active campaigns now save exact payloads, responses, lineage, and campaign settings incrementally in unique evidence directories. --findings-dir / MCP findings_dir selects the root; failures report archive location and stop further sends. Existing corpus files remain compatible, and dry-run creates no archive.

  • RE signal-name suggestions use exact Intel/Motorola bit overlap (#502). Unaligned Motorola signals retain valid matches across byte boundaries, and disjoint fields sharing a byte no longer produce false DBC matches. Confidence uses the actual overlapping-bit fraction; output fields are unchanged.

  • Capture comparisons preserve standard/extended identifier types (#500). Messages sharing a numeric CAN ID now retain independent metrics and new/dropped flags. Rows expose is_extended_id, and category summaries add typed *_identifiers lists while retaining numeric *_ids lists for compatibility.

  • TUI live capture now stops promptly on idle buses, preserves completed and paused capture buffers until they are drained, blocks replacement of a worker that has not stopped, and exposes queue-loss telemetry instead of silently dropping events (#498).