SCR1B3 is built privacy-first and minimal-attack-surface by construction. This document describes that posture and how to report vulnerabilities. A per-trust-boundary STRIDE analysis (auto-update / plugins / LSP / opened files / local state) lives in docs/threat-model.md.
SCR1B3 collects nothing about you and transmits no file contents, ever.
- No account, no login, no cloud. The editor works fully offline.
- No analytics, no usage counters, no crash beacons. There is no phone-home.
- File contents never leave your device. Editing, search, syntax highlighting, and spellcheck are entirely local.
- Logs to stderr only. Structured logs are emitted to stderr (never a file, never transmitted); verbosity defaults to
warnand is controlled byRUST_LOG.
SCR1B3 has exactly two outbound network surfaces. The first runs by default; the second is opt-in and default-OFF. Both are under your explicit control and the full transparency record is in PRIVACY.md.
1. The update version-check (default notify). It:
- Contacts only the public GitHub Releases API.
- Sends zero PII — it is an unauthenticated request that asks "what is the latest release?" and nothing else. No identifiers, no telemetry, no shipped token.
- Is fully user-controllable via
[updates] modein your config:off,notify(default — a single startup version-check),manual, orauto. Set it tooffto disable the check. (See CONFIG.md.)
2. Opt-in W1TN3SS crash/error reporting (default OFF). A per-stream, consent-gated crash/error + manual-issue reporting stream that transmits nothing unless you turn it on and consent to a specific report. It is off by default, local-first (spooled on your machine), previewable, and sends no persistent identifier. See PRIVACY.md § Opt-in crash / error reporting for the full posture. With [updates] mode = "off" and reporting left off (the default), SCR1B3 makes zero outbound connections.
When an update is downloaded, SCR1B3 cryptographically verifies it before applying it:
- Release artifacts are signed (minisign / ed25519) and checksummed.
- The updater refuses any download whose signature does not verify against the embedded trust set, or whose checksum does not match. An unsigned or tampered artifact is never installed.
- Key-rotation-safe trust set. Verification accepts a release signed by any key in an embedded set of trusted keys, so the signing key can be rotated with zero downtime: ship a build that trusts both the old and new keys, switch CI to sign with the new key, then retire the old key in a later release — no client is ever stranded. Trying multiple keys never upgrades a bad signature into an accepted one (a full cryptographic verify is still required).
- Anti-downgrade (rollback-attack defense). The updater refuses to install any version that is not strictly newer than the running build — enforced again at the moment of applying, not only at selection — so an attacker cannot replay an older, still-validly-signed release to force a downgrade to a known-vulnerable build (the TUF monotonic-version rule).
- Automatic rollback. The previous binary is snapshotted before the in-place swap and is automatically restored if the updated binary fails to relaunch — a failed update never leaves you without a working app.
- One click, then auto-cleanup. Choosing Update now downloads and installs in a single action (no separate "install" step). Once the update is applied, the downloaded archive/installer and its
.minisig/.sha256sidecars are deleted, and the one kept-prior backup is removed at the next launch (once the new build has confirmed it runs) — no stale downloads are left behind. - Least-privilege install, explicit elevation. For a per-user / portable install the updater swaps the binary in place with your own user permissions — no elevation. For an install in a protected location (e.g.
C:\Program Files), it runs the verified, signed self-elevating installer, which requests administrator rights via the standard Windows UAC prompt. It never silently elevates and never uses setuid; the same minisign + checksum gate applies to the installer as to the archive.
To verify a release manually, use the published minisign public key against the release's signature file (instructions accompany each release).
The user plugin/mod system is opt-in and sandboxed (see PLUGINS.md):
- A plugin only runs after you approve it. Dropping a folder into the plugins dir does not auto-execute it. By default the editor uses trust-on-first-use: it records the SHA-256 of the exact entry script you approved (in
[plugins] trusted) and refuses to run a brand-new or silently modified script until you approve it again (Settings → Plugins → Manage plugins → Approve & run). A held-back plugin is shown as "not running — needs your approval". - Strict signed mode. Set
[plugins] require_signed = trueand a plugin runs only when it carries a valid minisign signature over its entry script from a pinned author key (TOFU key-pinning; a changed key is refused). This authenticates the code that runs, not just metadata, via the sameupdate::verifycryptographic surface as the auto-updater. - Sandbox. The scripting "easy mode" runs in a restricted Rhai engine with seven caps wired (operations, call depth, string size, array size, map size, modules, expression depth) plus a wall-clock deadline. Both the
evalandimportkeywords are removed from the parser and the module resolver is a no-op — a script using them fails to compile, never just at runtime. (A compiled-WASM power track is a documented future design —PluginKind::Wasmis reserved but no WASM runtime ships in v1; only the Rhai easy-mode is executable today.) - Capability surface. The v1 host exposes only buffer-text operations to scripts — there is no ambient filesystem, network, or process access for a script to reach, so a plugin cannot silently exfiltrate files or open the network. Privileged host capabilities (and the per-capability consent prompt the manifest models) are not yet exposed; any future privileged capability will be gated behind explicit consent before it ships.
- You can disable any plugin via
[plugins] disabledor turn the whole system off with[plugins] enabled = false.
The config and theme files are TOML — a data format with no code-execution surface. Malformed config or themes fall back to safe defaults with a surfaced error; they cannot run arbitrary code.
Optional language servers run as child processes under your user identity, never under a shell:
- The server binary path is taken directly from your config (a single executable name or absolute path).
- Arguments pass to the child via
std::process::Command::args(&[...])— one Rust array, not a shell string. Argv is delivered to the child unmodified. - The declared MSRV is
rust-version = "1.92"inCargo.toml(the floor the egui stack imposes; enforced by themsrvCI job), and every published binary is built with the pinned1.95.0release toolchain. Both are well above the 1.77.2 floor that mitigates CVE-2024-24576 ("BatBadBut") — the bug where Windows.bat/.cmdarguments could be reinterpreted bycmd.exe's tokeniser. Newer rustc escapes them safely, so for any conforming build (≥ MSRV) and for every shipped release that escaping is unconditional. - The child's stdin / stdout are wired as pipes; stderr is dropped so a chatty language server cannot pollute the editor's own log channel.
- A missing or unspawnable server degrades gracefully to "no LSP for this language" rather than aborting — the editor is the trust boundary, not the language server.
When a plugin ships via the registry (or via a signed URL install), the tarball is verified before any code runs:
- SHA-256 checksum declared in the manifest is recomputed from the on-the-wire bytes. Mismatch →
"Plugin file is corrupted or has been modified since publication."and the install aborts. - minisign signature is verified against the manifest-declared
author_pubkey. The verification path is the sameupdate::verifycode that gates SCR1B3's auto-updater — one cryptographic surface, not two. - Trust-on-first-use (TOFU) author-key pinning. The first install records the manifest's
author_pubkeyin<config_dir>/plugins/pinned-keys.toml. Subsequent installs / updates of the same plugin id must present a byte-identical key. A different key triggers a "Author key changed — accept new key?" modal; silent rotation is refused. Same discipline as SSHknown_hostsand OpenBSDsignify.
The signed unit is the whole gzipped tarball — no metadata extraction, no canonicalisation, no reproducibility wizardry. The verifier rebuilds the bytes from the file on disk and either accepts or rejects.
Each crate declares its unsafe posture:
scribe-render—#![forbid(unsafe_code)]. Pure-safe Rust: theme → eguiVisualsmapping, color math, CRT parameter conversion. Forbid is unconditional; no local override is reachable.scribe-app—#![forbid(unsafe_code)]. The egui/eframe shell never needsunsafe.scribe-core—#![deny(unsafe_code)]with a single documented exception indocument.rsfor the read-onlymemmap2::Mmap::mapon the multi-GB-open path. The exception carries an explicit#[allow(unsafe_code)]annotation and aSAFETY:comment naming the invariants (read-only handle; dropped before any edit).scribe-win32-chrome— the quarantined Win32 FFI crate for the frameless titlebar; it is unsafe by design (no forbid/deny attribute) and is the single crate whereunsafeblocks live outside scribe-core's mmap exception. Each block carries aSAFETY:rationale.
forbid is preferred where it is reachable because it cannot be locally overridden — a new unsafe block cannot land via #[allow]. deny is used only where a documented exception exists; the #[allow(unsafe_code)] is then visible per call-site so the unsafe budget is explicit, not implicit.
The release binary is built with Control Flow Guard (/guard:cf) via
.cargo/config.toml (-C control-flow-guard under the
cfg(windows) target). CFG hardens indirect calls and returns against
ROP/JOP control-flow-hijack chains — the residual exploit class that
memory-safety does not, on its own, fully close in the presence of unsafe in
transitive GPU/windowing dependencies (wgpu, winit). It carries ~0.1% size
and ~3% runtime overhead and has been stable since Rust 1.47.
ASLR (/DYNAMICBASE), DEP (/NXCOMPAT) and High-Entropy VA are already MSVC
linker defaults for 64-bit Rust executables; CFG is the high-value mitigation
that is not on by default, so it is opted in for every Windows build.
The mitigations are CI-verified, so they cannot silently regress. The
binary hardening (winchecksec) CI job builds the release binary and runs
winchecksec (SHA-256-pinned
download) over it, failing the build unless the shipped exe actually carries
CFG + ASLR (/DYNAMICBASE) + High-Entropy VA + DEP (/NXCOMPAT). A change
that ever dropped a mitigation fails here instead of shipping a weaker binary.
The release binary is also built with --remap-path-prefix, stripping the
build-machine absolute paths (workspace + cargo home) from the artifact for
cleaner, more deterministic output.
- All dependencies are pinned via
Cargo.lock. cargo-audit(advisory database) andcargo-deny(license + advisory policy) gate the build in CI, andcargo-vet(supply-chain dependency review) runs as a dedicatedsupply-chain-auditjob.- The telemetry-free posture is enforced at the dependency-graph level.
deny.toml[bans]denies every alternative HTTP client / async runtime (reqwest,hyper,isahc,curl,surf,tokio) and every analytics / crash-reporting crate (sentry,opentelemetry,posthog-rs,mixpanel, …). The only sanctioned network stack is the synchronous, runtime-freeureq+rustlsused by the opt-out update check; thecargo-deny bansgate (a required status check) fails the build if a second egress path or any telemetry crate ever enters the tree. - An unused dependency fails CI.
cargo-macheteruns in CI and blocks any dependency that is declared but unused — keeping the dependency graph (and the attack surface) minimal. - A CycloneDX 1.6 SBOM is produced at release, and the shipped binary embeds its own scannable SBOM via
cargo-auditable: the exact released artifact can be vuln-scanned offline withcargo audit bin scr1b3.exe, trivy, grype, or osv-scanner — not just a detached side-file. - SLSA build-provenance. Every release artifact carries a signed build-provenance attestation (GitHub OIDC + Sigstore), verifiable with
gh attestation verify <file> --repo <repo>. This is additive to the minisign signatures: minisign attests author identity (the embedded key); provenance attests build integrity (which workflow, commit, and runner produced the artifact). - Dependency names are checked against slopsquatting before any addition.
These workflows run on every push and pull request against the public repository (and on a nightly cron for the supply-chain checks so an advisory that lands between PRs is caught within 24 hours):
| Gate | File | What it does |
|---|---|---|
| Supply-chain policy | workflows/cargo-deny.yml |
cargo-deny matrix-runs advisories / bans / licenses / sources against the workspace deny.toml. Nightly cron @ 06:30 UTC. |
| Secret scan | workflows/secret-scan.yml |
gitleaks scans full history for committed API keys / tokens / minisign private-key material. |
| SBOM | workflows/sbom.yml |
cargo-cyclonedx emits a CycloneDX 1.6 SBOM and attaches it to every tagged release as scr1b3-sbom.cdx.json. |
| Content-safety | scripts/content_safety_audit.py |
The publishable-cleanliness gate — fails any change that introduces an internal path, agent-system reference, or secret-shaped string into the public-repo-bound tree. |
| Dependency bumps | dependabot.yml |
Weekly Cargo + GitHub-Actions update PRs (Monday 09:00 UTC, 5 PRs cap). |
| Workflow security (zizmor) | workflows/workflow-security.yml |
zizmor static analysis for template injection, dangerous triggers, unpinned actions, and excessive permissions. SARIF results land in the Security tab. Weekly cron @ Thursday 06:47 UTC. |
| CodeQL | workflows/codeql.yml |
CodeQL with security-extended queries on Python (scripts/*.py) and GitHub Actions. Rust is not a CodeQL-supported free public-repo language — it is covered by cargo clippy --deny warnings and cargo-deny. Weekly cron @ Tuesday 14:23 UTC. |
| OpenSSF Scorecard | workflows/scorecard.yml |
OpenSSF Scorecard supply-chain assessment with public publication of the score. Weekly cron @ Wednesday 09:31 UTC. |
This section documents the load-bearing CI/CD controls on the master branch of this public repository. It is a baseline — the controls below are the defaults the maintainer commits to; any drift should be reconciled, not normalised.
master carries a GitHub branch-protection rule with the following guarantees:
| Setting | Value | Why |
|---|---|---|
required_status_checks |
build & test (ubuntu-latest), build & test (windows-latest), build & test (macos-latest), cargo-deny advisories, cargo-deny bans, cargo-deny licenses, cargo-deny sources, public-repo content-safety audit, F0RG3-W1R3 install-manifest audit, gitleaks, zizmor workflow audit, Analyze (python), Analyze (actions), Scorecard analysis |
A PR cannot merge unless every gate that already exists passes. The gates are advisory if they aren't required; advisory is honour-system. |
required_pull_request_reviews.required_approving_review_count |
1 |
Forces every change through PR review, even from the maintainer. |
required_pull_request_reviews.dismiss_stale_reviews |
true |
A new push invalidates a stale approval. |
required_pull_request_reviews.bypass_pull_request_allowances.users |
[46b-ETYKiAL] |
Preserves the solo-maintainer emergency self-merge path; admin-bypass is the documented escape hatch for hotfixes when no second reviewer is available. |
required_conversation_resolution |
true |
Every PR comment thread must be resolved before merge. |
required_linear_history |
true |
No merge commits — rebase or squash only. |
required_signatures |
true |
Every commit landing on master must be signature-verified. |
allow_force_pushes |
false |
History on master is immutable. |
allow_deletions |
false |
master cannot be deleted. |
enforce_admins |
false |
Deliberately off so the solo maintainer can land emergency hotfixes when CI is broken; every admin-bypass merge is audited by GitHub itself. |
Dependabot is configured at .github/dependabot.yml for both the Cargo ecosystem (workspace + crate manifests under crates/) and the github-actions ecosystem (the workflows under .github/workflows/). Updates land as PRs every Monday 09:00 UTC with a cap of 5 open PRs per ecosystem.
Dependabot security updates (the auto-PR on a newly-disclosed advisory) is the higher-priority surface — when enabled at the repo settings layer, an advisory landing on a transitive dependency opens a PR within minutes rather than waiting for the weekly tick.
Every uses: <action>@<tag> reference in .github/workflows/*.yml is pinned by full 40-char commit SHA with a trailing version comment, e.g.:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9This closes the tag-mover supply-chain class documented in the March-2025 tj-actions/changed-files incident: a JS Action runs in-process on the runner with full secret context, so a malicious tag-move on a popular action repo would let it exfiltrate every secret in the workflow scope on its next run. SHA-pinning makes the action a content-addressed dependency rather than a mutable reference.
Dependabot rewrites both the SHA pin and the trailing version comment together — the version comment never drifts from the pinned SHA, because the same Dependabot PR updates both lines.
The companion control is persist-credentials: false on every actions/checkout@… step. Without it, the runner leaves GITHUB_TOKEN material in .git/config for downstream steps to read — SHA-pinning the checkout alone is insufficient.
The three workflow-security feeds are staggered across the week to spread runner usage:
| Workflow | Cron | Slot |
|---|---|---|
| CodeQL | 23 14 * * 2 |
Tuesday 14:23 UTC |
| OpenSSF Scorecard | 31 9 * * 3 |
Wednesday 09:31 UTC |
| Workflow Security (zizmor) | 47 6 * * 4 |
Thursday 06:47 UTC |
Each also runs on every push to master and every PR, so the weekly cron is the catch-up surface for advisories or upstream rule additions that landed between PRs.
gitleaks runs on every PR and every push to master via the workflows/secret-scan.yml workflow. Full history is scanned (fetch-depth: 0), and the action exits non-zero on any finding — a PR carrying a leak cannot merge.
The complementary control is GitHub-native secret scanning + push protection, enabled at the repo settings layer. This blocks a git push at the server before the commit lands, so even a leaked secret in a local commit never reaches the public history.
The two controls compose: push protection catches the leak at git push time; gitleaks catches anything that slips through (e.g. a credential committed before the secret scanner learned its pattern, surfaced via the full-history scan).
Please do not open a public issue for security vulnerabilities.
Report privately using GitHub's Private Vulnerability Reporting (the "Report a vulnerability" button under the repository's Security tab). Include:
- A description of the issue and its impact.
- Steps to reproduce (a minimal proof of concept if possible).
- Affected version(s) and OS.
We aim to acknowledge reports promptly, keep you updated on remediation, and credit reporters who wish to be named once a fix ships. Please allow reasonable time for a fix before any public disclosure.
Security fixes target the latest released version. Update via your package manager or the auto-update mechanism to stay current.