Case study · 2026 · Systems analysis · security architecture · desktop tool
Кейс · 2026 · Системный анализ · архитектура безопасности · десктоп-инструмент
SpecShield
SpecShield
A local gateway that lets a commercial LLM read proprietary code and documents without reading the business behind them. It replaces organization, product, and partner names with opaque aliases, redacts secrets one way with no reverse path, leaves code structure untouched, and gates every export through a leak-detection scan before anything leaves the machine. The threat model and the residual risk statement came first. The number that matters most on this page is the one that admits what the tool does not yet catch.
Локальный обфускатор, который позволяет коммерческой LLM читать проприетарный код и документы, не видя бизнес за ними. Он заменяет имена организации, продукта и партнёров непрозрачными псевдонимами, затирает секреты необратимо, оставляет структуру кода нетронутой и перед выгрузкой любого материала проверяет его сканом на утечку. Сначала были модель угроз и заявление об остаточном риске. Самое важное число на этой странице то, которое честно называет, чего инструмент пока не ловит.
RustTauri v2ReactSQLiteTree-sitter
Task
Задача
What a commercial LLM is allowed to see
Sending proprietary code or an internal design document to a commercial LLM API leaks two different things at once: the business identity behind the material (which organization, which product, which partner, which environment) and the secrets embedded in it (keys, tokens, connection strings). Removing both by hand does not scale past a single file, and doing it wrong is worse than not doing it at all: a name that survives a sanitization pass has already left the machine.
The reduction this project makes: identity can be replaced with an opaque, reversible alias without touching the reasoning the model needs to do its job, because the model does not need to know that a table is called orgs for Acme Corp specifically, only that it is a table with these columns and these relationships. Secrets cannot be aliased at all; they are redacted one way, with no path back. Everything else, the shape of the system, stays exactly as written.
Scope: a local desktop app and a CLI, no server component, no telemetry, six named adversaries considered and two explicitly ruled out of scope, an export gate that runs before every copy leaves the machine, and a residual risk statement published alongside the product rather than buried in an appendix.
Что коммерческой LLM разрешено видеть
Отправка проприетарного кода или внутреннего проектного документа в API коммерческой LLM утекает сразу два разных типа информации: бизнес-принадлежность материала (какая организация, какой продукт, какой партнёр, какое окружение) и встроенные в него секреты (ключи, токены, строки подключения). Убрать и то и другое вручную не масштабируется дальше одного файла, а сделать это неправильно хуже, чем не делать вовсе: имя, пережившее обезличивание, уже покинуло машину.
Редукция проекта: принадлежность можно заменить непрозрачным обратимым псевдонимом, не затрагивая рассуждение, которое должна провести модель, потому что модели не нужно знать, что таблица называется orgs именно для Acme Corp, только то, что это таблица с такими-то колонками и связями. Секреты псевдонимизировать нельзя вообще; они затираются необратимо, без пути назад. Всё остальное, форма системы, остаётся ровно такой, как написана.
Рамки: локальное десктоп-приложение и CLI, без серверного компонента, без телеметрии, шесть поименованных противников рассмотрены и два явно выведены за рамки, шлюз проверки перед каждой выгрузкой с машины, заявление об остаточном риске опубликовано вместе с продуктом, а не спрятано в приложении.
Architecture
Архитектура
Two operations that must never be confused
Две операции, которые нельзя перепутать
--strict. Only the aliased twin crosses the dashed line to the external LLM. The response comes back through lexical restore, never the parser.--strict. Через пунктирную линию к внешней LLM уходит только псевдонимизированный двойник. Ответ возвращается через лексическое восстановление, а не через парсер.Structure survives by default, identity does not
Структура выживает по умолчанию, принадлежность нет
- Decision
- Nine identity categories are aliased on sight: organization, product, brand, partner, payment provider, person, tenant, environment, domain. Twelve structural types (services, DTOs, tables, columns, endpoints, filenames, and others) are left exactly as written unless the user explicitly promotes one with
specshield term <name> --entity-type <type>. - Why
- The model needs the shape of the system to reason about it; it does not need to know which company the shape belongs to. Structure that gets rewritten also has to be reversed at restore time, and every reversal is another place an alias can drift.
- Решение
- Девять категорий принадлежности псевдонимизируются сразу: организация, продукт, бренд, партнёр, платёжный провайдер, человек, тенант, окружение, домен. Двенадцать структурных типов (сервисы, DTO, таблицы, колонки, эндпоинты, имена файлов и другие) остаются как написаны, если пользователь явно не повысит один из них командой
specshield term <name> --entity-type <type>. - Почему
- Модели нужна форма системы, чтобы рассуждать о ней, но не нужно знать, какой компании эта форма принадлежит. Переписанную структуру пришлось бы восстанавливать обратно, а каждое восстановление это ещё одно место, где псевдоним может уплыть.
Secrets are redacted, never pseudonymized
Секреты затираются, а не псевдонимизируются
- Decision
- A secret becomes
<<REDACTED:type>>with no stored mapping back to the original value. Identity aliasing is reversible by design; secret redaction is one-way by design. The two are separate code paths and the distinction is load-bearing. - Why
- The design review that shaped v1.1 of the design document flagged the original draft for treating secrets the same way as names, reversibly. A reversible mapping for a secret is a second copy of the secret sitting in the vault.
- Решение
- Секрет становится
<<REDACTED:type>>без сохранённого соответствия обратно к исходному значению. Псевдонимизация принадлежности обратима по замыслу, затирание секретов необратимо по замыслу. Это два разных пути в коде, и разница здесь несущая. - Почему
- Design review, сформировавший версию 1.1 проектного документа, отметил, что в черновике секреты обрабатывались так же, как имена, обратимо. Обратимое соответствие для секрета это вторая копия секрета, лежащая в хранилище.
The export gate went from hard block to notify, secrets excepted
Шлюз выгрузки перешёл от жёсткой блокировки к уведомлению, кроме секретов
- Decision
- The gate scans the sanitized twin with an Aho-Corasick automaton built from every real name in the vault, including case variants, before export. By default a survived name is reported, not blocked.
--strictrestores a hard block, for CI. A detected secret hard-blocks in both modes, no exception. - Why
- The original design called for a hard block on any survived name. Run against a real repository it blocked 78 of 107 files on 124 ordinary words and wrote nothing at all. A gate nobody can get output from gets routed around, which is worse than a gate that reports and lets the user decide.
- Решение
- Перед выгрузкой шлюз сканирует обезличенного двойника автоматом Ахо-Корасик, построенным из всех реальных имён в хранилище, включая варианты регистра. По умолчанию пережившее имя сообщается, а не блокируется.
--strictвозвращает жёсткую блокировку, для CI. Обнаруженный секрет блокирует в обоих режимах, без исключений. - Почему
- Исходный замысел требовал жёстко блокировать любое пережившее имя. На реальном репозитории это заблокировало 78 из 107 файлов на 124 обычных словах и не выдало ничего. Шлюз, из которого никто не может получить результат, обходят стороной, а это хуже, чем шлюз, который сообщает и оставляет решение за пользователем.
Sequence-allocated aliases, chosen over a derived scheme
Псевдонимы по порядковому номеру вместо выводимой схемы
- Decision
- An alias like
ORG_001is assigned in order as new identities are found, stored in the vault. An earlier direction, deriving the alias from an HMAC of the real name so it would come out identical on two machines without syncing the vault, was tried and reversed back to sequence allocation. - Why
- A sequence number is short and readable in a diff; a derived alias is a long opaque string wherever it appears. The cost of reversing the decision is explicit: aliasing on a second machine without the shared vault produces a different, incompatible set of aliases for the same project.
- Решение
- Псевдоним вида
ORG_001назначается по порядку по мере обнаружения новых сущностей и хранится в хранилище. Более раннее направление, выводить псевдоним из HMAC реального имени, чтобы он совпадал на двух машинах без синхронизации хранилища, было опробовано и отменено обратно к порядковому назначению. - Почему
- Порядковый номер короткий и читаемый в diff; выводимый псевдоним это длинная непрозрачная строка везде, где встречается. Цена отмены решения названа прямо: псевдонимизация на второй машине без общего хранилища даёт другой, несовместимый набор псевдонимов для того же проекта.
Sanitize reads the AST, restore never does
Обезличивание читает AST, восстановление никогда
- Decision
- Sanitizing a file is AST-first, per format (Markdown, SQL, YAML, JSON, OpenAPI, TypeScript, PlantUML), so an identifier is only aliased where it is actually an identifier. Restoring the model's response back to real names is lexical and token-based, with canonical-form fuzzy matching to catch aliases the model itself introduced small variations of.
- Why
- The model's response is prose and code fragments, not a document that necessarily parses; a restore pass that required a clean AST would fail on exactly the responses that need restoring most. Tree-sitter has no symbol resolution for TypeScript either, which the design review flagged directly: 25 of 47 property-position identifiers in one real test file were unresolvable without a type checker, so the MVP scopes properties globally rather than pretending to resolve them, a stated and bounded leak.
- Решение
- Обезличивание файла идёт от AST, для каждого формата (Markdown, SQL, YAML, JSON, OpenAPI, TypeScript, PlantUML), так что идентификатор псевдонимизируется только там, где он действительно идентификатор. Восстановление ответа модели обратно к реальным именам лексическое и токенное, с нечётким сопоставлением по канонической форме, чтобы поймать псевдонимы, которые модель сама слегка исказила.
- Почему
- Ответ модели это проза и фрагменты кода, не обязательно парсящийся документ; проход восстановления, требующий чистый AST, падал бы именно на тех ответах, которые больше всего нуждаются в восстановлении. У Tree-sitter к тому же нет разрешения символов для TypeScript, на что design review указал прямо: в одном реальном тестовом файле 25 из 47 идентификаторов на позиции свойства были неразрешимы без проверки типов, поэтому MVP скоупит свойства глобально, а не притворяется, что разрешает их, это названная и ограниченная утечка.
One key wraps a random data key, not the vault itself
Один ключ оборачивает случайный ключ данных, а не само хранилище
- Decision
- Each sensitive column is encrypted with AES-256-GCM under a random data key; the data key is wrapped by a key-wrapping key derived from the user's passphrase with Argon2id (vault schema v3). The original design specced SQLCipher, whole-file encryption. It failed to build on Windows and was replaced.
- Why
- Wrapping a random data key instead of deriving the encryption key straight from the passphrase makes a passphrase change cheap (rewrap the data key, not every row), makes escrow possible (a second wrapped copy of the same data key), and makes revocation real (rotate the data key, the old wrapped copy stops working).
- Решение
- Каждая чувствительная колонка шифруется AES-256-GCM под случайным ключом данных; ключ данных оборачивается ключом-обёрткой, выведенным из пароля пользователя через Argon2id (схема хранилища v3). Исходный замысел специфицировал SQLCipher, шифрование всего файла целиком. Он не собрался на Windows и был заменён.
- Почему
- Оборачивание случайного ключа данных вместо вывода ключа шифрования прямо из пароля делает смену пароля дешёвой (переобернуть ключ данных, а не каждую строку), делает возможным escrow (вторая обёрнутая копия того же ключа данных) и делает отзыв реальным (сменить ключ данных, старая обёрнутая копия перестаёт работать).
Workflow
Процесс
What the user actually does
The desktop app walks through five steps in order (project, review, sanitize and verify, restore, diff and apply) plus three tools used as needed (whole project, audit log, vault). The CLI covers the same ground for scripting and CI, as two scenarios: sanitizing a single document, or working across a whole repository with init, term, scan, sanitize, restore, export, diff, and apply --dry-run before ever touching a branch other than the working one.
Что реально делает пользователь
Десктоп-приложение ведёт по пяти шагам по порядку (проект, обзор, обезличивание и проверка, восстановление, diff и применение) плюс три инструмента по мере надобности (весь проект, журнал аудита, хранилище). CLI покрывает то же самое для скриптов и CI, как два сценария: обезличивание одного документа или работа со всем репозиторием через init, term, scan, sanitize, restore, export, diff и apply --dry-run, прежде чем вообще коснуться другой ветки, кроме рабочей.
--strict), or a secret found, which always stops the export. The diff screen shows a three-way comparison before apply writes anything, and apply never targets main.--strict), или найден секрет, что всегда останавливает выгрузку. Экран diff показывает трёхстороннее сравнение до того, как apply что-либо запишет, и apply никогда не нацелен на main.The file picker opens in the Rust process; the webview holds zero filesystem permissions. On Windows, the clipboard gets three DWORD-0 opt-out formats set on copy, which stop cloud clipboard sync in apps that respect the convention. That last part is advisory, not enforcement, and is documented as such rather than presented as a guarantee.
Диалог выбора файла открывается в процессе Rust; у webview нет вообще никаких прав на файловую систему. В Windows при копировании выставляются три формата DWORD-0 для отказа от облачной синхронизации буфера обмена, что останавливает синхронизацию в приложениях, уважающих это соглашение. Последнее носит рекомендательный характер, а не принудительный, и задокументировано именно так, а не как гарантия.
Hard problems
Сложные проблемы
What went wrong and what it took to notice
None of these are dramatic. They are the kind of bug that a test suite catches only if the test suite was built to distrust its own tooling.
Что пошло не так и что понадобилось, чтобы заметить
Ничего из этого не драматично. Это тот тип бага, который тестовый набор ловит только если он построен так, чтобы не доверять собственным инструментам.
A file that failed to parse was reported as clean
Файл, который не распарсился, был отмечен как чистый
- Problem
- During a stress test against a real, production-shaped TypeScript repository, a TSX file that failed to parse was read by the verification pass as an empty result, and an empty result was treated the same as nothing found to flag, which surfaced as verified clean.
- Fix
- A parse failure is now its own outcome, distinct from a clean parse with no findings, threaded through as one of the five verification states rather than folded into a boolean.
- Проблема
- Во время стресс-теста на реальном, production-подобном TypeScript-репозитории TSX-файл, который не распарсился, был прочитан проходом проверки как пустой результат, а пустой результат обрабатывался так же, как «нечего отмечать», что проявилось как «проверено, чисто».
- Исправление
- Ошибка парсинга теперь отдельный исход, отличный от чистого парсинга без находок, проведённый как одно из пяти состояний проверки, а не свёрнутый в булево значение.
The corpus grader miscounted its own redactions
Грейдер корпуса неверно считал собственные затирания
- Problem
- Detection runs against text that has already had earlier redactions applied, and a redaction marker is not the same length as the text it replaced. The grader compared offsets computed before redaction against text measured after, and scored some correct detections as misses.
- Fix
- A dedicated offset mapper now tracks the shift introduced by each prior redaction, so the grader compares like against like.
- Проблема
- Детекция идёт по тексту, к которому уже применены более ранние затирания, а маркер затирания не той же длины, что заменённый текст. Грейдер сравнивал смещения, посчитанные до затирания, с текстом, измеренным после, и засчитывал часть верных находок как промахи.
- Исправление
- Отдельный маппер смещений теперь отслеживает сдвиг, вносимый каждым предыдущим затиранием, так что грейдер сравнивает сопоставимое с сопоставимым.
A mistyped passphrase could have bricked a pre-v3 vault
Опечатка в пароле могла безвозвратно испортить хранилище до v3
- Problem
- The migration to the wrapped-data-key scheme stamped the schema version before the passphrase used to unwrap the old key was actually verified. A mistyped passphrase during migration would have written the new version marker over a vault that was never successfully re-keyed.
- Fix
- The migration check now keys on the actual vault content, confirming the unwrap succeeded, rather than trusting the schema-version pragma alone.
- Проблема
- Миграция на схему с обёрнутым ключом данных проставляла версию схемы раньше, чем реально проверялся пароль, использованный для разворачивания старого ключа. Опечатка в пароле во время миграции записала бы новую метку версии поверх хранилища, которое так и не было перешифровано.
- Исправление
- Проверка миграции теперь опирается на реальное содержимое хранилища, подтверждая, что разворачивание удалось, а не доверяет одной только pragma версии схемы.
Still open
Всё ещё открыто
A real repository, even after every known detection bug was fixed, is still blocked by ordinary domain words like Node, Screen, or Status reaching the gate as false positives. This is tracked as an open policy question, not a bug: tightening detection to stop flagging common words risks missing the real ones, and the project has chosen to state that tension rather than paper over it with a longer stop-list that would need constant tuning per project.
Реальный репозиторий, даже после исправления всех известных багов детекции, всё ещё блокируется на шлюзе обычными доменными словами вроде Node, Screen или Status как ложными срабатываниями. Это ведётся как открытый вопрос политики, а не как баг: ужесточение детекции ради отсева частых слов рискует пропустить настоящие, и проект предпочёл назвать это напряжение прямо, а не замазать его удлинённым стоп-листом, который пришлось бы постоянно подстраивать под каждый проект.
439 / 439
Verification
Three properties are tested directly, not inferred: a round trip is byte-for-byte identical to the original, the sanitized twin parses with a structure that matches the original's, and nothing reaches the network during sanitize, restore, or verify. The test strategy layers property tests, a golden corpus, snapshot tests, and adversarial fixtures, plus a separate LLM round-trip suite that is explicitly non-gating because it depends on a live external model.
Верификация
Три свойства проверяются напрямую, а не выводятся косвенно: круговой путь побайтово идентичен оригиналу, обезличенный двойник парсится со структурой, совпадающей с оригиналом, и ничего не уходит в сеть во время обезличивания, восстановления или проверки. Стратегия тестирования складывается из property-тестов, золотого корпуса, snapshot-тестов и состязательных фикстур, плюс отдельный набор кругового пути через LLM, который явно не блокирующий, потому что зависит от живой внешней модели.
| Metric | Rules only | With dictionary |
|---|---|---|
| Recall | 49.0% | 100% |
| Precision | 98.0% | |
| Secrets caught | 6 of 6 | |
| Test count | 439 | |
| Метрика | Только правила | Со словарём |
|---|---|---|
| Recall | 49.0% | 100% |
| Precision | 98.0% | |
| Секретов поймано | 6 из 6 | |
| Тестов | 439 | |
The 49.0% figure is the one that matters most: it is rules-only recall on a synthetic seven-project corpus authored by the same person who wrote the detector, and it is stated in the project's own documentation as the number describing a first run, not a ceiling. The 100% column depends on the project's own vault dictionary already knowing the names in question, which is the ordinary case in practice but not a property of the detector by itself.
Цифра 49.0% самая важная: это recall только на правилах на синтетическом корпусе из семи проектов, написанном тем же человеком, что писал детектор, и в документации проекта она прямо названа числом, описывающим первый прогон, а не потолком. Колонка 100% зависит от того, что словарь хранилища проекта уже знает эти имена, что на практике обычный случай, но не свойство самого детектора.
A real project, measured
Реальный проект, замер
| Op | Target | Measured | Corpus |
|---|---|---|---|
| Index | < 30s | 0.03s | 1,000 files, 151k LOC |
| Full export | n/a | 11.5s | 1,000 files, 151k LOC |
| Rescan | < 2s | 0.44s | 1,000 files, 151k LOC |
| Операция | Цель | Замер | Корпус |
|---|---|---|---|
| Индексация | < 30 с | 0.03 с | 1000 файлов, 151k LOC |
| Полная выгрузка | н/д | 11.5 с | 1000 файлов, 151k LOC |
| Повторный скан | < 2 с | 0.44 с | 1000 файлов, 151k LOC |
Residual risk
Остаточный риск
What this project does not claim
The project ships its own residual risk statement, described in its own documentation as the most unflattering document in the repository by design. This section restates it rather than softening it.
- Business semantics fully reach the model. Only names and secrets are removed; what the system does, and why, is visible to whichever provider processes the request. This is a scope decision, not an oversight, and it is stated as such.
- Structural re-identification got easier, not harder, in v2.0: because structure now leaves verbatim by default, a determined reader with outside knowledge of the codebase has more to work with than before.
- The external provider retains the sanitized twin on its own terms once the request is sent; nothing in this design constrains what happens to it after that point.
- Prose-level name detection is heuristic and is only as good as its dictionary; a name the vault has never seen can slip through in free text even when the same name is correctly aliased in code.
- Clipboard hardening on Windows is advisory, three opt-out formats an app can choose to respect, not a guarantee that cloud clipboard sync is actually blocked.
- There is no OS credential store integration; a passphrase is the only key path. Key escrow is a bearer credential until
rotate-keyis run. Installers are unsigned. The desktop UI has never been driven end-to-end, only the CLI has full test coverage of that depth. Alias durability against a real, paid model API has never been measured; the 100%-with-dictionary figure is a lab result.
Чего проект не заявляет
Проект публикует собственное заявление об остаточном риске, названное в его же документации самым нелестным документом в репозитории намеренно. Этот раздел пересказывает его, не смягчая.
- Бизнес-семантика полностью доходит до модели. Убираются только имена и секреты; что делает система и зачем, видно любому провайдеру, обрабатывающему запрос. Это решение о рамках, а не недосмотр, и названо так прямо.
- Структурная переидентификация в v2.0 стала не сложнее, а легче: поскольку структура теперь по умолчанию уходит дословно, у настойчивого читателя с внешним знанием кодовой базы больше материала, чем раньше.
- Внешний провайдер удерживает обезличенного двойника на своих условиях после отправки запроса; ничто в этом дизайне не ограничивает, что происходит с ним дальше.
- Детекция имён в прозе эвристическая и настолько хороша, насколько хорош её словарь; имя, которого хранилище никогда не видело, может проскользнуть в свободном тексте, даже если то же имя корректно псевдонимизировано в коде.
- Защита буфера обмена в Windows рекомендательная, три формата отказа, которые приложение может уважать или нет, а не гарантия, что облачная синхронизация буфера действительно заблокирована.
- Интеграции с хранилищем учётных данных ОС нет; пароль единственный путь к ключу. Escrow ключа это предъявительский credential до запуска
rotate-key. Установщики не подписаны. Десктоп-интерфейс никогда не был прогнан end-to-end, только у CLI есть полное покрытие тестами на такую глубину. Устойчивость псевдонимов против реального платного API модели никогда не измерялась; цифра 100% со словарём это лабораторный результат.
Resources
Материалы
Code and the complete reference
Код и полный справочник
Built from the full internal documentation set: the threat model, the security review one-pager, the residual risk statement, the software design document (v1.1) and its preceding design review, the CLI and desktop user guides, the complete reference, and the working TODO tracking real milestones and bugs.
Построено на полном комплекте внутренней документации: модель угроз, security review one-pager, заявление об остаточном риске, проектный документ (v1.1) и предшествующий ему design review, руководства пользователя CLI и десктопа, полный справочник и рабочий TODO с реальными вехами и багами.