Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
o1js-scan — Статический анализатор без зависимостей для поиска ошибок корректности zk-схем в o1js/Mina zkApps и схемах Noir | Kitploit
Инструменты/GitHubGitHub/auditinfra-io/o1js-scan
Оборонительные ИнструментыСтатический анализСканеры уязвимостейСтатический анализ кода (SAST)Анализ уязвимостейАнализ КодаКриптографияDevSecOps
GitHubauditinfra-io/o1js-scan

o1js-scan

Статический анализатор без зависимостей для поиска ошибок корректности zk-схем в o1js/Mina zkApps и схемах Noir

Репозиторий
2104 дней назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Сайт
Поделиться

o1js-scan

CI Python License PyPI npm

Пакет сообщества: o1js-scan указан в официальном каталоге o1js Community Packages.

Последняя версия: 0.20.0 — анализатор теперь читает контракты, которые extends TokenContract. До этого релиза проверка контрактов сопоставлялась только с SmartContract, поэтому каждый взаимозаменяемый токен, коллекция NFT и пул AMM в экосистеме сканировались как "no findings". Если вы сканировали контракт токена до 0.20.0, просканируйте его снова. См. .

CHANGELOG

Быстрый статический анализатор без зависимостей для ошибок корректности zk-схем в:

  • o1js / Mina zkApps (TypeScript .ts / .js) — схемы Kimchi из тел @method
  • Noir (.nr) — Rust-подобный ZK DSL от Aztec (включая шаблоны в стиле aztec-nr)

Критичные для безопасности ошибки обычно находятся не в системе доказательства — они в собственных ограничениях приложения: свидетели, которые контролирует доказывающий, но схема их никогда не связывает. o1js-scan — это сканер недостаточно ограниченных сигналов для родственников Circom в экосистемах Mina и Noir.```bash pip install o1js-scan

or: pipx install o1js-scan

or: npm install -D o1js-scan

o1js-scan path/to/zkapp # o1js + Noir (auto) noir-scan path/to/circuits # same binary — Noir-friendly alias noir-scan . --lang noir --fail-on high --sarif noir.sarif

root@kitploit:~
### Пример

Дан сейф, в котором сумма `withdraw` является управляемым доказывающим свидетелем, который
никогда не привязывается к состоянию on-chain:```console
$ o1js-scan examples/vulnerable_vault.ts --include-examples
LOW      O1JS_UNCONSTRAINED_RECIPIENT       vulnerable_vault.ts:23  fn=withdraw  Recipient `to` is prover-chosen in `withdraw`
HIGH     O1JS_UNCONSTRAINED_WITNESS         vulnerable_vault.ts:23  fn=withdraw  Unconstrained witness `amount` flows to send_amount in `withdraw`
o1js-scan: 2 finding(s) [1 high, 1 low] in 1 of 1 file(s) — fails (--fail-on high)
$ echo $?
1

--include-examples нужен здесь только потому, что демонстрационный файл находится в examples/, который классификатор путей по умолчанию понижает в приоритете, чтобы собственный пример кода репозитория не мог сломать его сборку. Тот же контракт в вашем src/ сообщает HIGH без флага.

Находка HIGH — это ошибка, позволяющая опустошить контракт. Исправленный контракт (examples/safe_vault.ts) отбрасывает её и завершается с 0, сохраняя только информационную LOW о получателе, выбранном доказывающим:```console $ o1js-scan examples/safe_vault.ts --include-examples LOW O1JS_UNCONSTRAINED_RECIPIENT safe_vault.ts:23 fn=withdraw Recipient to is prover-chosen in withdraw o1js-scan: 1 finding(s) [1 low] in 1 of 1 file(s) — passes (--fail-on high) $ echo $? 0

root@kitploit:~
См. [`examples/`](https://github.com/auditinfra-io/o1js-scan/blob/main/examples) для уязвимых/исправленных пар o1js и Noir.

## Содержание

- [Установка](#install)
- [Использование](#usage) · [Подавление находки](#suppressing-a-reviewed-finding)
- [GitHub Action](#github-action)
- [Что он обнаруживает — o1js](#what-it-detects-o1js) · [Noir](#what-it-detects-noir)
- [Известные ограничения](#known-limitations) · [Где этот инструмент останавливается](#where-this-tool-stops)
- [Конфиденциальность и приватный код](#privacy-and-private-code)
- [Постквантовая проверка](#post-quantum-review)
- [Совместимость](#compatibility) · [Как это работает](#how-it-works)
- [Участие в разработке](#roadmap--contributing)

## Установка```bash
pip install o1js-scan

Для изолированной глобальной установки CLI используйте pipx:```bash pipx install o1js-scan

root@kitploit:~
Для репозиториев приложений на Node/npm, использующих Noir, Aztec или o1js, установите npm-обёртку:```bash
npm install -D o1js-scan
npx noir-scan . --lang noir --fail-on high

Пакет npm — это тонкая обёртка вокруг того же Python-анализатора, и для его работы требуется Python 3.8+ в PATH (python3 или python). Задайте O1JS_SCAN_PYTHON, чтобы выбрать конкретный интерпретатор.

Или из исходного кода:```bash git clone https://github.com/auditinfra-io/o1js-scan cd o1js-scan pip install -e .

root@kitploit:~
Сторонних зависимостей Python нет. Требуется Python 3.8+. Консольный скрипт `noir-scan`
устанавливается вместе с `o1js-scan` (та же точка входа), в том числе через обёртку
npm.

## Использование```bash
# scan a directory (recursively; skips node_modules, target/, .git, …)
o1js-scan path/to/project

# Noir-only / o1js-only
noir-scan circuits --lang noir
o1js-scan src --lang o1js

# scan a single file
o1js-scan src/MyContract.ts
noir-scan src/main.nr

# machine-readable output for CI
o1js-scan src --json

# SARIF 2.1.0 for GitHub code scanning (writes o1js-scan.sarif by default)
o1js-scan src --sarif
noir-scan . --lang noir --sarif noir.sarif

# choose which severity fails CI (critical|high|medium|low|none; default high)
o1js-scan src --fail-on medium

# progressive/power-user gate (equivalent to --fail-on medium)
o1js-scan src --strict

# test code is excluded by default (both backends); opt back in
o1js-scan src --include-tests

# example code is downgraded to LOW by default; keep original severity
o1js-scan src --include-examples

o1js-scan --version

Код выхода равен 1, когда присутствует находка уровня --fail-on (по умолчанию high) или выше, и 0 в противном случае — так что его можно напрямую встроить в CI. При значении по умолчанию находка уровня low/medium (включая информационное правило о получателе ниже) не приводит к сбою сборки; используйте --fail-on none, чтобы только сообщать, или --strict (сокращение для --fail-on medium), чтобы задать более строгий порог, по-прежнему считая находки низкой серьёзности рекомендательными. Эти две опции взаимоисключающие, поэтому конфигурация CI не может быть неоднозначной. Отсутствующий путь сканирования завершается с кодом 2 и ошибкой в stderr, так что опечатка не может незаметно пройти CI как чистое выполнение. Каждый запуск выводит однострочную сводку (количество по уровням серьёзности и вердикт гейта) в stderr.

Тестовый код исключается по умолчанию — в обоих бэкендах. Тесты намеренно создают недопустимые значения и плохие транзакции, чтобы доказать, что проверки их отклоняют, поэтому находка там — это суть теста, а не ошибка схемы. Файл считается тестовым кодом, когда:

  • его имя соответствует *.test.ts / *.spec.ts (а также вариантам .js/.jsx/.tsx/.mjs /.cjs), или *_test.nr / test_*.nr;
  • он находится в каталоге test/, tests/, __tests__/, spec/ или __mocks__/;
  • (только Noir, на основе содержимого) функция несёт атрибут #[test] / #[test(...)], или находится внутри блока mod test { … } / mod tests { … } — с областью действия блока, поэтому тестовый модуль в конце production-файла не заглушает остальную его часть.

Передайте --include-tests, чтобы сообщать о них.

Пример кода понижается в серьёзности, а не отбрасывается. Находка в каталоге examples/ или example/, либо в файле с именем *.eg.ts (а также .nr и другие расширения JS/TS), понижается до LOW с примечанием — всё ещё сообщается, но больше не может привести к сбою сборки. Пример кода намеренно упрощён, и помечать собственные примеры фреймворка как уязвимости — это шум; но он копируется в production гораздо чаще, чем тестовый код, поэтому его понижают, а не скрывают. Передайте --include-examples, чтобы сохранить исходную серьёзность.

Всякий раз, когда применяется любая из этих политик, запуск выводит строку в stderr с указанием этого — например, 6 file(s) skipped as test code, 1 finding(s) downgraded as examples — так что тихое сканирование никогда не бывает молча тихим. Эти счётчики также появляются в SARIF в разделе invocation.properties. Обратите внимание на компромисс: обнаружение основано только на пути (без разбора describe(/it(), поэтому production-схема, хранящаяся в tests/, будет пропущена — строка в stderr — это то, как вы это заметите.

Каталоги, пропускаемые при обходе дерева: node_modules, target (nargo), .git, dist, build, __pycache__, .venv, venv.

Подавление проверенной находки

Заглушите находку, которую вы уже разобрали, не ослабляя гейт, с помощью встроенного комментария на — или на строке выше — отмеченной строке:```ts this.send({ to, amount }); // o1js-scan-disable-line O1JS_UNCONSTRAINED_WITNESS

// o1js-scan-disable-next-line this.send({ to, amount });

root@kitploit:~
| `--no-color` | Отключить цветной вывод |
| `--debug` | Включить режим отладки |
| `--verbose` | Включить подробный вывод |
| `--silent` | Подавить весь вывод |
| `--json` | Выводить результаты в формате JSON |
| `--csv` | Выводить результаты в формате CSV |
| `--html` | Выводить результаты в формате HTML |
| `--markdown` | Выводить результаты в формате Markdown |
| `--output <file>` | Записать вывод в файл |
| `--config <file>` | Использовать указанный файл конфигурации |
| `--threads <n>` | Количество потоков (по умолчанию: 10) |
| `--timeout <n>` | Тайм-аут в секундах (по умолчанию: 30) |
| `--retries <n>` | Количество повторных попыток (по умолчанию: 3) |
| `--proxy <url>` | Использовать прокси |
| `--user-agent <string>` | Установить User-Agent |
| `--cookie <string>` | Установить Cookie |
| `--header <string>` | Установить пользовательский заголовок |
| `--rate-limit <n>` | Ограничить запросы в секунду |
| `--random-agent` | Использовать случайный User-Agent |
| `--follow-redirects` | Следовать перенаправлениям |
| `--no-verify-ssl` | Отключить проверку SSL |
| `--insecure` | Разрешить небезопасные соединения |
| `--force` | Принудительное выполнение |
| `--yes` | Автоматически отвечать «да» на все запросы |
| `--no-interactive` | Отключить интерактивный режим |
| `--version` | Показать версию |
| `--help` | Показать справку |```nr
let inv = unsafe { hint(x) };  // o1js-scan-disable-line NOIR_UNCONSTRAINED_WITNESS

Укажите один или несколько идентификаторов правил, чтобы подавить только их; директива без идентификаторов подавляет все правила в целевой строке.

Как библиотека:```python from o1js_scan import analyze_file, analyze_project

for path, finding in analyze_project("src", lang="auto"): print(path, finding.rule_id, finding.severity.value, finding.title)

root@kitploit:~
## GitHub Action

Добавьте сканер в CI в несколько строк. Результаты отображаются как аннотации в diff'е PR
и как оповещения во вкладке **Security → Code scanning** репозитория.```yaml
# .github/workflows/o1js-scan.yml
name: o1js-scan
on: [push, pull_request]

permissions:
  contents: read
  security-events: write   # required to upload SARIF to code scanning

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: auditinfra-io/[email protected]
        with:
          path: src              # optional, defaults to the repo root
          lang: auto             # auto | o1js | noir
          # version: 0.20.0       # optional, pin the scanner version
          # fail-on: high         # optional, fail the job on high/critical

CI-рецепт только для Noir

Рекомендуется для проектов Noir, которым нужны оповещения сканирования кода и порог высокой критичности:```yaml

  • uses: auditinfra-io/[email protected] with: path: . lang: noir fail-on: high
root@kitploit:~
Или без Action:```bash
pip install o1js-scan
noir-scan . --lang noir --fail-on high --sarif noir.sarif

pre-commit (опционально)```yaml

.pre-commit-config.yaml

  • repo: local hooks:
    • id: noir-scan name: noir-scan entry: noir-scan language: system pass_filenames: false args: [".", "--lang", "noir", "--fail-on", "high"]
root@kitploit:~
Входные данные: `path` (по умолчанию `.`), `lang` (`auto`|`o1js`|`noir`, по умолчанию `auto`),
`version` (версия PyPI для установки, по умолчанию последняя), `upload-sarif` (по умолчанию
`true`), `fail-on` (`critical`|`high`|`medium`|`low`|`none`, по умолчанию `none`),
`fail-on-findings` (устарело, по умолчанию `false`), `include-tests` (по умолчанию
`false`), `include-examples` (по умолчанию `false`). Выходные данные: `sarif-file`. Для загрузки SARIF
требуются `security-events: write` и включённое сканирование кода.

Отчёт и гейт строятся из одного массива аргументов, поэтому `include-tests`
и `include-examples` применяются к обоим — SARIF, который вы читаете, и код возврата, по которому вы
принимаете решение, всегда описывают один и тот же набор исходников. Проход отчётности выполняется с
`--fail-on none`, поэтому находки никогда не блокируют загрузку SARIF, но операционный
сбой (несуществующий путь, ошибка использования CLI) всё равно приводит к сбою шага,
а не сообщается как чистое сканирование.

`fail-on-findings: true` сохранён для совместимости и отображается на `fail-on: high`,
когда `fail-on` оставлен на `none`; он выдаёт предупреждение об устаревании. Предпочитайте
`fail-on`, который может ограничивать на любом уровне серьёзности.

## Что он обнаруживает (o1js)

### Поддерживаемые правила кратко

<!-- BEGIN GENERATED RULE SUMMARY -->
| Backend | Rules | High-capable | Medium-capable | Low-capable |
|---------|------:|-------------:|---------------:|------------:|
| o1js | 18 | 11 | 12 | 2 |
| Noir | 11 | 4 | 9 | 1 |
| **Total** | **29** | **15** | **21** | **3** |
<!-- END GENERATED RULE SUMMARY -->

Счётчики — это уникальные ID правил, поддерживаемых каждым бэкендом. Правило, которое назначает
серьёзность в зависимости от контекста (например, high для перевода стоимости и
medium для записи состояния), появляется более чем в одной колонке серьёзности, поэтому колонки
серьёзности намеренно не суммируются в общее число правил. В настоящее время нет правил
уровня critical или info. Полные описания и
защиты от ложных срабатываний приведены ниже.

<!-- BEGIN GENERATED O1JS RULE TABLE -->
| Rule | Severity | What it means |
|------|----------|---------------|
| `O1JS_MISSING_STATE_PRECONDITION` | high | `this.x.get()` читается без соответствующего `requireEquals(...)` / `getAndRequireEquals()`. Простой `get()` не добавляет **никакого** предусловия аккаунта, поэтому доказательство не привязывает `x` к его on-chain значению — доказывающий может подставить любое значение. |
| `O1JS_UNCONSTRAINED_WITNESS` | high / medium | Аргумент `@method` (приватный свидетель, контролируемый доказывающим) попадает в **сумму** отправки (`this.send(...)` или `AccountUpdate.create*(...).send(...)` в том же методе) или в `.set(...)` состояния и **никогда** не проверяется утверждением. Прямой аналог недостаточно ограниченного сигнала Circom. High, когда он достигает перевода стоимости. |
| `O1JS_UNCONSTRAINED_PROVABLE_WITNESS` | high / medium / low | Локальная переменная `Provable.witness(...)` попадает в эффект отправки/состояния **без** внутрисхемного утверждения. Callback свидетеля выполняется *вне* схемы (это лишь подсказка доказывающему), поэтому результат — свежее значение, контролируемое доказывающим — другой источник свидетелей помимо аргументов `@method`. Его нужно заново вывести и утвердить (`x.assertEquals(<recomputed>)`) или привязать к состоянию. High для суммы отправки (`this.send(...)` или `AccountUpdate.create*` в том же методе). |
| `O1JS_UNCONSTRAINED_RECIPIENT` | low | Аргумент `@method` используется **только** как получатель `to:` при отправке. Обычно это намеренно (пользователь указывает собственный адрес вывода) и носит информационный характер — это важно только если получатель должен быть фиксированной казной или адресом, записанным в состоянии. **Не** приводит к срабатыванию гейта кода возврата CI. |
| `O1JS_WITNESS_NOT_BOUND_TO_STATE` | medium | Свидетель ограничен лишь *тривиально* (например, `> 0` или сравнением с константой) перед эффектом — никогда не привязан к on-chain состоянию. Убедитесь, что внецепочечная оркестрация делает это безопасным, иначе баланс можно опустошить вплоть до его текущего значения. |
| `O1JS_STALE_MERKLE_ROOT` | high | Метод пересчитывает корень Меркла из свидетеля, предоставленного доказывающим (`computeRootAndKey` / `calculateRoot`), но не привязывает **ни один** из пересчитанных корней к текущему on-chain корню. Без `this.root.requireEquals(...)` / `assertEquals` относительно живого корня доказывающий может передать свидетеля для сфабрикованного или устаревшего дерева — подделав членство или воспроизведя старое состояние. Привязка может находиться в недекорированном вспомогательном методе того же класса (`this.verifyX(witness)`); распространение через вспомогательные методы это покрывает. |
| `O1JS_UNVERIFIED_PROOF` | high | Параметр `@method` с типом `Proof<...>` / `SelfProof<...>` / `DynamicProof<...>` никогда не проходит `.verify()` до использования его публичных полей. Передача Proof не верифицирует его — без явной проверки доказывающий может предоставить произвольный объект доказательства, и любое использование его `publicOutput` не ограничено. Также срабатывает, когда `.verifyIf(flag)` управляется неограниченным аргументом `@method` и читаются публичные поля доказательства, поскольку доказывающий может сделать условие ложным. |
| `O1JS_UNASSERTED_BOOL` | high / medium | Предикат o1js (`equals` / `lessThanOrEqual` / …) возвращает `Bool` и не добавляет **никакого** ограничения, если результат не утверждён или не использован. HIGH, когда вызов — это просто отброшенный оператор; MEDIUM, когда он присвоен локальной переменной, на которую больше нигде не ссылаются. |
| `O1JS_UNCONSTRAINED_SENDER` | high / medium | `this.sender.getUnconstrained()` возвращает отправителя транзакции без его доказательства. HIGH, когда это значение (или локальная переменная из него) попадает в assert / `.set` состояния / `send` (пустая проверка); MEDIUM в остальных случаях. Предпочитайте `this.sender.getAndRequireSignature()` или развёрнутую идиому `AccountUpdate.createSigned(sender)`. **Молчит, когда** (1) тот же `@method` также вызывает `this.sender.getAndRequireSignature()` где-либо (требование подписи действует в пределах метода), или (2) значение свидетеля-отправителя является аргументом `AccountUpdate.createSigned(...)` / `AccountUpdate.create(...).requireSignature()` на том же ключе (требуется идентичность аргумента — `createSigned` на другом ключе не подавляет). |
| `MissingRangeCheck` | high | Сырое `Field` (а не проверенное на диапазон `UInt64`/`UInt32`) используется как сумма перевода. `Field` — это элемент по модулю p и не ограничен по диапазону. |
| `O1JS_WEAK_PERMISSIONS` | high / medium | `editState` / `send` установлены в `proofOrSignature()` или `none()`, позволяя ключу аккаунта zkApp обойти схему путём подписи. Также отмечает `setVerificationKey` / `setPermissions`, оставленные на `signature` / `proofOrSignature` / `none` (документированные Mina «training wheels» для обновления); HIGH в сочетании со слабыми `editState`/`send` в том же `permissions.set`. |
| `O1JS_LOGIC_OUTSIDE_PROOF` | high | Логика безопасности (assert / approve / send / `.set` состояния) внутри `Provable.asProver(...)` или callback `Provable.witness*`. Эти callback выполняются *вне* схемы — злонамеренный доказывающий может удалить их и всё равно получить верифицирующее доказательство. |
| `O1JS_APPROVE_WITHOUT_BINDING` | medium | `@method` вызывает `approve` / `approveAccountUpdate` / `approveBase` без чтения `balanceChange` / `publicKey` и без `assertCanMint` / `assertCanBurn` / проверки сохранения через `forEachUpdate` — архетип Mina FlawedTokenContract. |
| `O1JS_VACUOUS_ASSERT` | high / medium | Утверждение, которое выполняется по построению: `x.assertEquals(x)`, `x.equals(x).assertTrue()` или `Bool(true).assertTrue()`. HIGH для самосравнений (почти всегда опечатка); MEDIUM для утверждений константного Bool. |
| `O1JS_CONDITIONAL_ASSERT` | medium | Утверждение внутри `if <flag> { ... }`, где `<flag>` — это контролируемый доказывающим `Bool` из `@method` (или локальная переменная из `.toBoolean()`). Условный оператор JS не ограничивает схему так, как это делает `Provable.if`. Встроенные сравнения не сообщаются ради точности. |
| `O1JS_GUARDED_INVERSE` | medium | `.div()` / `.inv()` / `.sqrt()` внутри ветви `Provable.if`, защищённой условием на само значение, на котором она терпит неудачу. Обе ветви вычисляются в схеме, и эти вызовы безусловно утверждают, что обратное значение или корень существует, поэтому защита не пропускает утверждение — схема невыполнима ровно для того входа, для которого защита была написана, и метод никогда не может быть доказан для него. Сообщается Veridise как `V-O1J-VUL-060`. Сначала вычислите безопасный делитель (`Provable.if(isZero, Field(1), d)`) и выберите результат после. **Молчит, когда** защита ничего не говорит о делителе, поэтому несвязанный `Provable.if` вокруг безопасного деления не отмечается. |
| `O1JS_PRECONDITION_OVERWRITTEN` | medium | Два или более вызовов `requireEquals` / `requireBetween` / `requireNothing` на **одном и том же** свойстве в одном методе с различающимися аргументами. Предусловия *устанавливаются* на AccountUpdate, а не накапливаются, поэтому каждый вызов перезаписывает предыдущий и применяется только последний — в отличие от внутрисхемных утверждений, которые компонуются. `a.requireEquals(b)`, затем `a.requireEquals(c)` подразумевает `a === c`, а не `a === b`. Сообщается Veridise как `V-O1J-VUL-012`. **Молчит, когда** аргументы идентичны (идемпотентно, ничего не теряется), при `getAndRequireEquals()` (другой метод, поэтому повторные чтения состояния допустимы), и когда вызовы находятся во взаимоисключающих ветвях JS, которые разрешаются во время построения схемы. Это последнее исключение может скрыть реальную перезапись, охватывающую несвязанный `if`/`else`. |
| `O1JS_STATE_READ_AFTER_WRITE` | medium | Поле `@state` читается (`get()` / `getAndRequireEquals()`) после завершения `set(...)` на том же поле в том же методе. `set()` записывает изменение в AccountUpdate, но не записывает сквозь в `get()`, поэтому чтение всё ещё наблюдает значение до записи, и любая арифметика на его основе молча смещена на эту запись. Сообщается Veridise как `V-O1J-VUL-030`. Храните новое значение в локальной переменной вместо повторного чтения состояния. **Молчит, когда** чтение вложено в собственные аргументы записи (идиома read-modify-write `this.x.set(this.x.getAndRequireEquals().add(1))`, которая корректна), и когда запись и чтение находятся во взаимоисключающих ветвях JS. Ограничено одним методом — случай кэширования между методами, который также описывает Veridise, требует знания графа вызовов, которого у этого правила нет. |
<!-- END GENERATED O1JS RULE TABLE -->

### Защиты от ложных срабатываний (o1js)

Анализатор спроектирован так, чтобы молчать на корректном коде:

- **Методы, защищённые подписью, пропускаются.** `@method`, который вызывает
  `this.requireSignature()` (или `getAndRequireSignature`, `AccountUpdate.createSigned`,
  `Signature.verify`), защищён владельцем/администратором — его аргументы выбираются держателем
  ключа, а не произвольным доказывающим — поэтому его свидетели не отмечаются. Это
  эквивалент `onlyOwner` в o1js.
- **Свидетели, привязанные к состоянию, пропускаются.** Аргумент, утверждённый равным (или
  ограниченный сравнением порядка относительно) значению, полученному из `getAndRequireEquals()`,
  корректен и не будет сообщён. Это покрывает как прямую форму —
  `amount.assertLessThanOrEqual(bal)` — так и цепочечную форму
  `amount.lessThanOrEqual(bal).assertTrue()`. Привязка, находящаяся в
  недекорированном вспомогательном методе того же класса (`this.verifyX(arg)`), также распознаётся,
  в том числе через цепочку таких вспомогательных методов.
- **Верифицированные доказательства пропускаются.** Аргумент типа `Proof` / `SelfProof` / `DynamicProof` /
  `*Proof`, для которого вызывается `.verify()`, ограничен
  верифицированной схемой — находки о свидетелях на нём (и его `publicOutput` /
  `publicInput`) подавляются. `.verifyIf(flag)` засчитывается только когда
  условие не является неограниченным аргументом метода или само утверждено. То же
  относится к канонической обёртке OffchainState
  `this.offchainState.settle(proof)` (фреймворк верифицирует внутри `settle`).
  Самодельный `.settle(proof)` **не** предполагается верифицирующим. Обратный случай (аргумент типа proof никогда не верифицирован
  и не урегулирован через OffchainState) сообщается как `O1JS_UNVERIFIED_PROOF`.
- **Утверждённые / использованные Bool пропускаются.** Предикат, сцепленный с
  `.assertTrue()` / `.assertFalse()`, вложенный в `Provable.if(...)` или
  присвоенный локальной переменной, на которую позже ссылаются, не сообщается как
  `O1JS_UNASSERTED_BOOL`.
- **Аутентифицированные отправители пропускаются.** `this.sender.getUnconstrained()`
  не срабатывает, когда тот же `@method` также вызывает
  `this.sender.getAndRequireSignature()`, или когда это значение свидетеля
  передаётся в `AccountUpdate.createSigned(...)` / аутентифицируется через
  `.requireSignature()` на AccountUpdate, построенном из него (требуется идентичность
  аргумента).
- Комментарии и строковые литералы удаляются перед анализом, поэтому `assert`
  внутри строки не может создать ложный результат.

## Что он обнаруживает (Noir)

Та же идея корректности — недостаточно ограниченные свидетели — применяется к
[Noir](https://noir-lang.org) схемам (`.nr`). Направьте сканер на файлы `.nr`
(или используйте `--lang noir`), и он проанализирует их с набором правил Noir.
Тот же лексический подход без зависимостей. Откалиброван по идиомам оракулов aztec-nr /
`unsafe` — см. [`docs/noir_calibration.md`](https://github.com/auditinfra-io/o1js-scan/blob/main/docs/noir_calibration.md).

<!-- BEGIN GENERATED NOIR RULE TABLE -->
| Rule | Severity | What it means |
|------|----------|---------------|
| `NOIR_UNCONSTRAINED_WITNESS` | high | Значение, полученное из блока `unsafe { ... }` — результат `unconstrained fn` (оракул / подсказка Brillig) — которое никогда не переограничивается через `assert` / `assert_eq` (или подтверждающий вспомогательный метод / проверку Меркла). Подсказка выполняется **вне** схемы. Аналог `O1JS_UNCONSTRAINED_PROVABLE_WITNESS`. |
| `NOIR_UNCONSTRAINED_INPUT` | medium | Приватный (свидетель) вход `fn main`, который не попадает **ни в один** `assert` / `assert_eq` и **не** является частью публичного вывода. Аналог `O1JS_UNCONSTRAINED_WITNESS`. |
| `NOIR_UNCONSTRAINED_PUBLIC_INPUT` | medium | **Публичный** вход `fn main`, который не достигает ни ограничения, ни вывода — схема никогда его не читает. *Двойник* правила о приватном свидетеле: верификатор предоставляет значение и верит, что утверждение о нём, тогда как схема его игнорирует (например, `merkle_root: pub Field`, который никогда не проверяется, поэтому членство фактически никогда не было доказано). MEDIUM, потому что намеренно неиспользуемый публичный вход также является законной идиомой для привязки доказательства к контексту (nonce / chain id / получатель), что лексически неотличимо — поэтому он не ограничивает CI при значении по умолчанию `--fail-on high`. |
| `NOIR_UNCHECKED_CAST` | medium | Контролируемое доказывающим значение, приведённое к узкому беззнаковому типу (`as u8`/`u16`/`u32`) **без** утверждения о диапазоне. Аналог o1js `MissingRangeCheck`. |
| `NOIR_UNCONSTRAINED_ARRAY_INDEX` | medium | Контролируемое доказывающим значение, используемое как индекс массива (`arr[i]`) **без** какой-либо проверки. Неявная проверка границ Noir устанавливает лишь то, что индекс *в пределах диапазона* — а не то, что он *правильный* — поэтому доказывающий остаётся свободным выбрать любой элемент и всё равно получить верифицирующее доказательство. Это баг свободы выбора селектора, стоящий за позициями путей Меркла, выбором нот и членством в allow-list. Подавляется, когда индекс ограничен по диапазону, закреплён равенством, ограничен перед приведением (`index.assert_max_bit_size::<8>(); let i = index as u32;`) или когда считанное значение само закреплено через `assert_eq`. |
| `NOIR_UNASSERTED_BOOL` | high / medium | Сравнение, чей результат `bool` **отброшен**. Аналог o1js `O1JS_UNASSERTED_BOOL`. |
| `NOIR_CONDITIONAL_ASSERT` | medium | `assert` внутри `if <flag> { ... }`, где `<flag>` — контролируемый доказывающим простой `bool` или локальная переменная, производная от контролируемых доказывающим значений. Ограничение внутри условия применяется только когда условие истинно, поэтому выбранная доказывающим ветвь может пропустить проверку. Встроенные сравнения (`if x != 0`) не трогаются ради точности; присвоение защиты локальной переменной (`let gate = x != 0; if gate`) сообщается, если сам `gate` не утверждён. |
| `NOIR_CONDITIONAL_CONSTRAIN` | medium | Вызов `constrain_*` / `confirm_*` / `verify_*` только под контролируемым доказывающим `if`, тогда как подсказка `unsafe` всё ещё достигает вывода. |
| `NOIR_UNUSED_CHECK_RESULT` | high / medium | Результат `check_*` / `confirm_*` / `verify_*` / `constrain_*` отброшен (простой вызов) или присвоен и никогда не утверждён — проверка не привязывает схему. |
| `NOIR_VACUOUS_CONSTRAINT` | high / medium | Ограничение, которое выполняется по построению: самосравнение (`assert(x == x)`, `assert_eq(x, x)`, `x >= x`) или константное условие (`assert(true)`). Оно не добавляет никакого ограничения, но строка *читается* как проверка — что делает её более опасной, чем отсутствующее ограничение, потому что ревью на ней останавливается. HIGH для самосравнения (почти всегда опечатка вместо реальной проверки: `assert(computed == expected)`, набранное как `assert(expected == expected)`); MEDIUM для константы, которая чаще является заполнителем. `x != x` **не** отмечается — это невыполнимо, баг живости, а не молчаливая дыра в корректности. |
| `NOIR_UNSAFE_MISSING_SAFETY` | low | Блок `unsafe { ... }` без соседнего комментария `// Safety:`. Информационно; не приводит к сбою CI при значении по умолчанию `--fail-on high`. |
<!-- END GENERATED NOIR RULE TABLE -->

### Защиты от ложных срабатываний (Noir)

- **Assert / переход через let / подтверждающие вспомогательные методы в том же файле** привязывают подсказки `unsafe`.
- **Имена на месте вызова** `constrain_*` / `confirm_*` / `verify_*` /
  `check_(non_)membership*` / `public_data_storage_read` засчитывают аргументы (с обнаружением неиспользованного результата для отброшенных проверок).
- **Документированные намеренно неограниченные** (требуется соседний `// Safety:`):
  `random()`, `avm::…` и отложенные формулировки kernel/rollup/discovery.
- **Кортежный `let` + утверждённые флаги** привязывают свидетелей Меркла, переданных в проверки членства.

Пример:```console
$ noir-scan examples/noir_unconstrained.nr --include-examples
HIGH     NOIR_UNCONSTRAINED_WITNESS         noir_unconstrained.nr:16  fn=main  Unconstrained `unsafe` result `inv` in `main`
LOW      NOIR_UNSAFE_MISSING_SAFETY         noir_unconstrained.nr:16  fn=  `unsafe` block without a `// Safety:` comment
noir-scan: 2 finding(s) [1 high, 1 low] in 1 of 1 file(s) — fails (--fail-on high)

$ noir-scan examples/noir_constrained.nr --include-examples
noir-scan: no findings in 1 o1js or Noir file(s) — passes (--fail-on high)

Как и в примере с o1js выше, --include-examples нужен только потому, что эти демонстрационные файлы находятся в examples/.

Известные ограничения

Анализатор — это лексический фронтенд без зависимостей плюс облегчённый семантический слой, который выполняет отслеживание псевдонимов и межпроцедурное распространение через вспомогательные методы того же класса. Это не фронтенд компилятора TypeScript, не проверяющий типы модуль и не движок потоков данных для всей программы, и в этом сканере нет слоя SMT или формальных доказательств. Учитывайте эти слепые зоны при триаже — они известны и намеренны для этой архитектуры без зависимостей, а не являются ошибками:

  • Отслеживаются только простые псевдонимы. Отслеживание свидетелей следует за простыми псевдонимами в пределах одного метода, такими как const q = qty, но не за производными выражениями или деструктуризацией: ```ts const q = qty; this.send({ to: dest, amount: q }); // followed const q = qty.add(1); this.send({ to: dest, amount: q }); // not followed const slot = this.root; slot.get(); // missing precondition missed

    root@kitploit:~
  • Кросс-методная привязка охватывает только цепочки помощников внутри одного класса. Недекорированный помощник того же класса, вызываемый как this.verifyX(arg), может привязать аргумент вызывающего, и начиная с 0.19.0 цепочки таких помощников (@method → помощник A → помощник B) прослеживаются до неподвижной точки. Шаг помощник→помощник отображает только голую ссылку на параметр, поэтому helperA(x.add(1)) не распространяется. Свободные и импортированные функции по-прежнему не прослеживаются, а псевдонимизация аргумента помощника через локальную переменную остаётся задокументированным ограничением.

  • Обнаружение неутверждённых Bool имеет форму инструкции. Уровень A помечает только голые выражения-инструкции, у которых самый внешний вызов — это Bool-предикат, и после него ничего не сцеплено. Предикаты, вложенные в Provable.if(...), или присвоенные и позже используемые, не помечаются. Сложные варианты использования Bool-локальной переменной в управляющем потоке всё ещё могут быть пропущены, если имя никогда не упоминается (режим отказа: пропуск, а не ложное срабатывание).

  • Гейтирование по сигнатуре — на уровне метода и на основе подстрок. _method_is_signature_gated рассматривает весь @method как гейтированный владельцем, если он содержит идиому сигнатуры, и распознаёт верификатор только тогда, когда имя получателя буквально содержит signature — поэтому sig.verify(admin, msg) не распознаётся как гейтирование, тогда как не связанная с этим проверка сигнатуры в другом месте большого метода может привести к избыточному подавлению. Это всё или ничего для каждого метода.

  • Аутентификация отправителя основана на имени и только в пределах одного метода. O1JS_UNCONSTRAINED_SENDER подавляется, когда this.sender.getAndRequireSignature() или AccountUpdate.createSigned(<that sender>) появляется в том же теле @method. Требование подписи, которое находится только в помощнике (this.requireSenderSig() → getAndRequireSignature внутри), не прослеживается — режим отказа — ложное срабатывание на корректном коде, оборачивающем идиому, а не пропуск реальной ошибки.

  • Кросс-крейтовые помощники Noir распознаются только по соглашению об именовании (без Nargo.toml / разрешения импортов). Предпочтение отдаётся пропуску, а не ложному срабатыванию.

Именно поэтому находки — это отправная точка для ручной проверки, а не доказательства. Переписывание с учётом потоков данных намеренно вне области применения лексического анализатора.

Где этот инструмент останавливается

o1js-scan намеренно представляет собой поверхностный лексический проход по одному файлу — без парсера, без потоков данных, без решателя. Именно это делает его свободным от зависимостей и мгновенным в CI, и это же является жёстким потолком. Ограничения выше — не список задач на будущее; это следствия дизайна.

Поэтому стоит явно сказать, что этот инструмент может и не может вам сообщить:

  • Чистый прогон — это не аудит. Он означает, что ни одна форма, которую распознаёт этот сканер, не совпала — а не то, что схема корректна. Классы ошибок, требующие потоков данных, чувствительности к путям или решения ограничений, недостижимы для инструмента такой формы, на любом языке.
  • Находка — это зацепка, а не вердикт. Каждое правило здесь — эвристика с задокументированным классом ложных срабатываний.

Этот компромисс правильный для линтера, который вы запускаете на каждом коммите. Если вы работаете над чем-то, где разница имеет значение — протокол, содержащий реальную ценность, схема, в которой вы не можете позволить себе ошибиться — относитесь к этому как к первому проходу и закладывайте бюджет на настоящую проверку.

Для более глубокого анализа отдельный полный сканер поддерживается в репозитории audit-engine-cli. o1js-scan — это намеренно лёгкий, открытый сканер; проприетарные знания о детектировании и детали реализации полного сканера здесь не воспроизводятся. Для доступа или более полной проверки схемы обращайтесь: [email protected].

Приватность и приватный код

Установленный CLI анализирует файлы локально. У него нет телеметрии, сетевого клиента, учётной записи или шага загрузки, а его среда выполнения Python не имеет сторонних зависимостей. Запуск o1js-scan path/to/private-repo никуда не отправляет исходный код или находки.

Как и логи компилятора, вывод сканера может содержать пути, идентификаторы и фрагменты исходного кода. SARIF также указывает точные местоположения в репозитории, а GitHub Action загружает его в GitHub code scanning. Используйте те же средства контроля доступа к репозиторию и CI, которые вы уже используете для сканируемого исходного кода.

Хотите внести полезный отчёт о ложном срабатывании или пропущенном обнаружении, не делясь приложением? Воспроизведите синтаксис с вымышленными именами и константами, удаляйте бизнес-логику по одной инструкции за раз и убедитесь, что синтетический фрагмент всё ещё срабатывает по тому же правилу, прежде чем публиковать его. Руководство по безопасному для приватности вкладу содержит конкретный чек-лист и несколько способов помочь сообществу o1js, не раскрывая приватную схему.

Эта граница не мешает открытому сканеру становиться лучше. Публичная документация и репозитории o1js могут поддерживать новые правила и фикстуры совместимости; синтетические примеры могут тестировать ложные срабатывания и пропущенные ограничения; а устойчивость парсера, диагностика, SARIF, производительность, упаковка и калибровка — всё это может улучшаться без публикации приватной техники аудита или клиентского кода. Открытый сканер должен делать независимо объяснимые утверждения; приватные исследования могут оставаться в отдельном движке аудита.

Постквантовая проверка

Квантовый риск связан с безопасностью схем, но это не правило о пропущенном ограничении. o1js-scan не определяет, соответствует ли подпись, хеш, обязательство, система доказательств Kimchi или сама Mina постквантовой цели безопасности. Эти ответы зависят от конкретного примитива и параметров, предположений о платформе, требуемого срока службы развёртывания и его плана миграции — а не просто от идентификатора TypeScript, который может увидеть лексический сканер.

Вдохновлённое Qubit or Not Qubit от O(1) Labs, руководство по постквантовой проверке превращает эту границу в специфичный для o1js инвентарь и чек-лист крипто-гибкости. Используйте его вместе с этим сканером, а не интерпретируйте чистый прогон как постквантовую оценку.

Совместимость

Работает на o1js 1.x, 2.x и 3.x, включая хардфорк Mesa, на который нацелен o1js 3.0.0. o1js-scan анализирует исходный код TypeScript как текст и не имеет зависимости времени выполнения от o1js — ничего не привязано к версии. Он опирается на современный API предусловий require* (getAndRequireEquals, requireEquals, requireSignature, getAndRequireSignature), декораторы @method / @method() / @method.returns(...), аннотированные поля @state, this.send({...}), низкоуровневые переводы AccountUpdate.balance.subInPlace(...) и Permissions.*. Устоявшиеся формы остаются совместимыми на границах 1.x → 2.x → 3.x, при этом сканер также принимает недавно задокументированные варианты декораторов и низкоуровневых переводов. Идиома аутентификации владельца 2.x this.sender.getAndRequireSignature() распознаётся как гейтирование по сигнатуре. (Устаревшие предусловия assertEquals по-прежнему принимаются, так что старый код тоже не ломается.)

Ломающие изменения Mesa — все на уровне времени выполнения и протокола: удаление Transaction.setFeePerSnarkCost() и констант TransactionCost.*, новая форма VerificationKey.toJSON(), перегенерированные ключи верификации, MAX_ZKAPP_STATE_FIELDS, поднятый с 8 до 32, и формат транзакций mina-signer v4. Ни одно из них не переименовывает API, по которому сопоставляет этот сканер, поэтому ни одно правило не изменилось для Mesa, и это проверено, а не заявлено. scripts/o1js_release_matrix.sh сканирует два закреплённых релиза o1js, straddling границу протокола — 2.15.0 (9620ef08, последний релиз 2.x) и 3.0.0 (cc18a919, Mesa) — и сравнивает каждую находку с tests/fixtures/o1js_release_matrix.json:

РелизНаходкиHIGHMEDIUMLOWФайлы
o1js 2.15.036826218
o1js 3.0.0 (Mesa)39829219

33 находки идентичны по обе стороны границы, ни одна не потеряна, и все три новые находятся в src/examples/zkapps/big-state-zkapp.ts — пример с 32 полями состояния, который существует только потому, что Mesa поднял MAX_ZKAPP_STATE_FIELDS. Эта дельта закреплена тестом, поэтому она не может незаметно дрейфовать. Матрица запускается при каждой сборке CI; еженедельная задача o1js-upstream-canary дополнительно отслеживает o1js на HEAD, опережая любой релиз.

Эквивалентные написания ограничений нормализуются для анализа: экземплярный assertEquals(...), статический Provable.assertEqual(Type, ...) и цепочки равенства equals(...).assertTrue() привязывают одни и те же операнды. Извлечение методов сбалансировано по скобкам после маскирования комментариев и строк с сохранением длины и принимает многострочные декораторы, вложенные типы параметров в форме колбэков, модификаторы доступа TypeScript и многострочные псевдонимы идентичности (включая формы в скобках и as Type).

Анализ Noir нацелен на синтаксис Noir, используемый проектами Aztec / nargo (.nr); он не вызывает nargo и не компилирует схемы.

Как это работает

Это лексический анализатор, а не полноценный парсер TypeScript или Noir — исходники o1js и Noir разделены скобками и поддаются обработке регулярными выражениями, а вывод предназначен для сортировки человеком. Это делает его свободным от зависимостей и мгновенным для запуска в CI. Находки — это отправная точка для проверки, а не доказательства.

Дорожная карта / участие

Вклад приветствуется — новые семейства правил, больше защит от ложных срабатываний и архетипы калибровки из реального мира — всё это ценно. См. CONTRIBUTING.md.

О предлагаемом пути от листинга в Community Packages до проверки в виде advisory в репозитории o1js см. готовое к отправке предложение по интеграции в upstream o1js.

Запустите тесты и линтер с помощью:```bash pip install -e ".[dev]" pytest # unit tests + Noir/o1js corpus ruff check . # lint npm run format:check # prettier, npm wrapper only

root@kitploit:~
## Лицензия

Apache-2.0. См. [`LICENSE`](https://github.com/auditinfra-io/o1js-scan/blob/main/LICENSE).
Скачать инструмент