
Analisador estático sem dependências para bugs de solidez em circuitos zk em zkApps o1js/Mina e circuitos Noir
Pacote da comunidade:
o1js-scanestá listado no diretório oficial o1js Community Packages.
Mais recente: 0.20.0 — o analisador agora lê contratos que
extends TokenContract. Até esta versão, o filtro de contratos correspondia apenas aSmartContract, então todo token fungível, coleção NFT e pool AMM do ecossistema era escaneado como "sem achados". Se você escaneou um contrato de token antes da 0.20.0, escaneie novamente. Veja o .
Um analisador estático rápido e sem dependências para bugs de solidez de circuitos zk em:
.ts / .js) — circuitos Kimchi a partir de corpos de @method.nr) — a DSL ZK de estilo Rust da Aztec (incluindo padrões no formato aztec-nr)Os bugs críticos de segurança geralmente não estão no sistema de prova — eles estão nas
próprias restrições da aplicação: witnesses que o provador controla, mas que o circuito
nunca vincula. o1js-scan é o scanner de sinais sub-restritos para os primos do Circom
nos ecossistemas Mina e Noir.```bash
pip install 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
### Exemplo
Dado um cofre cujo valor de `withdraw` é uma testemunha controlada pelo provador que
nunca é vinculada ao estado 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 é necessário aqui apenas porque o arquivo de demonstração está em
examples/, que o classificador de caminhos rebaixa por padrão para que o próprio
código de exemplo de um repositório não possa falhar a sua build. O mesmo contrato no seu src/ reporta
HIGH sem nenhuma flag.
O achado HIGH é o bug drenável. O contrato corrigido
(examples/safe_vault.ts) elimina-o e sai com 0, mantendo apenas o LOW informativo
sobre o destinatário escolhido pelo prover:```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
Consulte [`examples/`](https://github.com/auditinfra-io/o1js-scan/blob/main/examples) para os pares vulneráveis/corrigidos de o1js e Noir.
## Conteúdo
- [Instalação](#install)
- [Uso](#usage) · [Suprimindo um achado](#suppressing-a-reviewed-finding)
- [GitHub Action](#github-action)
- [O que ele detecta — o1js](#what-it-detects-o1js) · [Noir](#what-it-detects-noir)
- [Limitações conhecidas](#known-limitations) · [Onde esta ferramenta para](#where-this-tool-stops)
- [Privacidade e código privado](#privacy-and-private-code)
- [Revisão pós-quântica](#post-quantum-review)
- [Compatibilidade](#compatibility) · [Como funciona](#how-it-works)
- [Contribuindo](#roadmap--contributing)
## Instalação```bash
pip install o1js-scan
Para uma instalação global isolada da CLI, use pipx:```bash
pipx install o1js-scan
Para repositórios de aplicações Noir, Aztec ou o1js baseados em Node/npm, instale o wrapper npm:```bash
npm install -D o1js-scan
npx noir-scan . --lang noir --fail-on high
O pacote npm é um wrapper fino em torno do mesmo analisador Python e requer
Python 3.8+ no PATH (python3 ou python). Defina O1JS_SCAN_PYTHON para escolher um
interpretador específico.
Ou a partir do código-fonte:```bash git clone https://github.com/auditinfra-io/o1js-scan cd o1js-scan pip install -e .
Sem dependências Python de terceiros. Python 3.8+. O script de console `noir-scan` é
instalado juntamente com `o1js-scan` (mesmo ponto de entrada), inclusive através do wrapper
npm.
## Uso```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
O código de saída é 1 quando um achado no nível --fail-on ou acima (padrão
high) está presente e 0 caso contrário — assim você pode inseri-lo diretamente no CI.
Com o padrão, um achado de baixa/média severidade (incluindo a regra informativa de destinatário
abaixo) não falha a build; use --fail-on none para apenas reportar,
ou --strict (um atalho para --fail-on medium) para restringir mais estritamente enquanto
ainda trata achados de baixa severidade como informativos. As duas opções são mutuamente
exclusivas para que a configuração de CI não possa ser ambígua. Um caminho de scan ausente sai com 2
com um erro em stderr, então um erro de digitação não pode passar silenciosamente no CI como uma execução limpa. Cada execução
imprime um resumo de uma linha (contagens por severidade e o veredito do gate) em stderr.
Código de teste é excluído por padrão — ambos os backends. Testes deliberadamente constroem valores inválidos e transações ruins para provar que os asserts os rejeitam, então um achado ali é o propósito do teste e não um bug de circuito. Um arquivo conta como código de teste quando:
*.test.ts / *.spec.ts (e as variantes .js/.jsx/.tsx/.mjs
/.cjs), ou *_test.nr / test_*.nr;test/, tests/, __tests__/, spec/ ou __mocks__/;#[test] / #[test(...)],
ou está dentro de um bloco mod test { … } / mod tests { … } —
com escopo de bloco, então um módulo de teste no final de um arquivo de produção não
silencia o resto dele.Passe --include-tests para reportá-los.
Código de exemplo é rebaixado, não descartado. Um achado em um diretório examples/ ou
example/, ou em um arquivo chamado *.eg.ts (.nr e as outras extensões JS/TS
também), é rebaixado para LOW com uma nota — ainda reportado, não mais
capaz de falhar uma build. Código de exemplo é deliberadamente simplificado, e sinalizar os
próprios exemplos de um framework como vulnerabilidades é ruído; mas ele é copiado para
produção com muito mais frequência do que código de teste, e é por isso que é rebaixado
em vez de ocultado. Passe --include-examples para manter a severidade original.
Sempre que qualquer uma das políticas se aplica, a execução imprime uma linha em stderr dizendo isso —
por exemplo, 6 file(s) skipped as test code, 1 finding(s) downgraded as examples — para que
um scan silencioso nunca seja silenciosamente silencioso. As contagens também aparecem no SARIF sob
invocation.properties. Note o trade-off: a detecção é apenas baseada em caminho
(sem parsing de describe(/it(), então um circuito de produção armazenado sob tests/
será ignorado — a linha de stderr é como você percebe.
Diretórios ignorados ao percorrer uma árvore: node_modules, target (nargo),
.git, dist, build, __pycache__, .venv, venv.
Silencie um achado que você triou sem afrouxar o gate, com um comentário inline na — ou na linha acima da — linha sinalizada:```ts this.send({ to, amount }); // o1js-scan-disable-line O1JS_UNCONSTRAINED_WITNESS
// o1js-scan-disable-next-line this.send({ to, amount });
| `--no-color` | Desativa a saída colorida |
| `--debug` | Ativa a saída de depuração |
| `--verbose` | Ativa a saída detalhada |
| `--version` | Exibe a versão do programa |
| `--help` | Exibe a mensagem de ajuda |
### Exemplos de Uso
```bash
# Escanear um único repositório
python3 GitDorker.py -tf tokens.txt -q "example" -d repos.txt -o results.txt
# Escanear uma organização
python3 GitDorker.py -tf tokens.txt -q "example" -org "myorg" -o results.txt
# Escanear um usuário
python3 GitDorker.py -tf tokens.txt -q "example" -u "myuser" -o results.txt
# Escanear um único repositório
python3 GitDorker.py -tf tokens.txt -q "example" -r "https://github.com/user/repo" -o results.txt
Nenhum token válido encontrado
Limite de taxa excedido
Nenhum resultado encontrado
Ative a saída de depuração para solucionar problemas:
python3 GitDorker.py -tf tokens.txt -q "example" -d repos.txt --debug
Esta ferramenta é apenas para fins educacionais e de pesquisa. Os usuários são responsáveis por garantir que suas ações estejam em conformidade com todas as leis e regulamentos aplicáveis. Os autores não se responsabilizam por qualquer uso indevido ou dano causado por esta ferramenta.
Este projeto está licenciado sob a Licença MIT - consulte o arquivo LICENSE para obter detalhes.
Este projeto está em desenvolvimento ativo. Os recursos e a API podem mudar sem aviso prévio.
Para dúvidas ou preocupações, por favor abra uma issue no repositório do GitHub.
Aviso: Esta ferramenta deve ser usada apenas para fins educacionais e de pesquisa. Sempre obtenha a devida autorização antes de realizar qualquer teste de segurança.```nr let inv = unsafe { hint(x) }; // o1js-scan-disable-line NOIR_UNCONSTRAINED_WITNESS
Liste um ou mais ids de regras para suprimir apenas essas; uma diretiva isolada (sem ids)
suprime todas as regras na linha de destino.
Como biblioteca:```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)
Adicione o scanner ao CI em poucas linhas. Os achados aparecem como anotações no diff do PR e como alertas na aba Security → Code scanning do repositório.```yaml
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
### Receita de CI somente para Noir
Recomendada para projetos Noir que desejam alertas de varredura de código e um
gate de alta severidade:```yaml
- uses: auditinfra-io/[email protected]
with:
path: .
lang: noir
fail-on: high
Ou sem a Action:```bash pip install o1js-scan noir-scan . --lang noir --fail-on high --sarif noir.sarif
### pre-commit (opcional)```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"]
Entradas: path (padrão .), lang (auto|o1js|noir, padrão auto),
version (versão do PyPI a instalar, padrão a mais recente), upload-sarif (padrão
true), fail-on (critical|high|medium|low|none, padrão none),
fail-on-findings (obsoleto, padrão false), include-tests (padrão
false), include-examples (padrão false). Saída: sarif-file. O upload do SARIF
requer security-events: write e a verificação de código (code scanning) ativada.
O relatório e o gate são construídos a partir de um único array de argumentos, pelo que include-tests
e include-examples se aplicam a ambos — o SARIF que lê e o código de saída sobre o qual
faz o gate descrevem sempre o mesmo conjunto de fontes. A passagem de relatório corre com
--fail-on none para que os achados nunca bloqueiem o upload do SARIF, mas uma falha
operacional (um caminho que não existe, um erro de utilização da CLI) continua a falhar o passo
em vez de ser reportada como uma análise limpa.
fail-on-findings: true é mantido por compatibilidade e mapeia para fail-on: high
quando fail-on é deixado em none; emite um aviso de depreciação. Prefira
fail-on, que pode fazer o gate em qualquer severidade.
| Backend | Regras | Capaz de High | Capaz de Medium | Capaz de Low |
|---|---|---|---|---|
| o1js | 18 | 11 | 12 | 2 |
| Noir | 11 | 4 | 9 | 1 |
| Total | 29 | 15 | 21 | 3 |
As contagens são IDs de regras distintos suportados por cada backend. Uma regra que atribui severidade de acordo com o contexto (por exemplo, high para uma transferência de valor e medium para uma escrita de estado) aparece em mais do que uma coluna de severidade, pelo que as colunas de severidade intencionalmente não somam o total de regras. Atualmente não existem regras de severidade critical ou info. As descrições completas e as proteções contra falsos positivos seguem-se abaixo.
| Regra | Severidade | O que significa |
|---|---|---|
O1JS_MISSING_STATE_PRECONDITION | high | Leitura de this.x.get() sem um requireEquals(...) / getAndRequireEquals() correspondente. Um get() simples não adiciona nenhuma pré-condição de conta, pelo que a prova não vincula x ao seu valor on-chain — um provador pode substituir qualquer valor. |
O1JS_UNCONSTRAINED_WITNESS | high / medium | Um argumento de @method (uma testemunha privada controlada pelo provador) flui para um montante de envio (this.send(...) ou um AccountUpdate.create*(...).send(...) do mesmo método) ou para um .set(...) de estado e nunca é asseverado. Análogo direto de um sinal Circom sub-restrito. High quando atinge uma transferência de valor. |
O1JS_UNCONSTRAINED_PROVABLE_WITNESS | high / medium / low | Uma variável local de Provable.witness(...) flui para um efeito de envio/estado com nenhuma asserção in-circuit. O callback da testemunha corre fora do circuito (é apenas uma dica para o provador), pelo que o resultado é um valor novo controlado pelo provador — a outra fonte de testemunhas além dos argumentos de @method. Tem de ser re-derivado e asseverado (x.assertEquals(<recomputed>)) ou vinculado ao estado. High num montante de envio (this.send(...) ou AccountUpdate.create* do mesmo método). |
O1JS_UNCONSTRAINED_RECIPIENT | low | Um argumento de @method é usado apenas como o destinatário to: de um envio. Isto é habitualmente intencional (um utilizador nomeia o seu próprio destino de levantamento) e é informativo — só importa se o destino se destinar a ser um tesouro fixo ou um endereço registado em estado. Não aciona o gate do código de saída da CI. |
O1JS_WITNESS_NOT_BOUND_TO_STATE | medium | Uma testemunha é apenas trivialmente restringida (por exemplo, > 0, ou comparada com uma constante) antes de um efeito — nunca ligada ao estado on-chain. Confirme que a orquestração off-chain torna isto seguro, ou o saldo é drenável até ao seu valor permanente. |
O analisador foi concebido para se manter silencioso em código correto:
@method que chama
this.requireSignature() (ou getAndRequireSignature, AccountUpdate.createSigned,
Signature.verify) tem gate de proprietário/admin — os seus argumentos são escolhidos pelo detentor
da chave, não por um provador arbitrário — pelo que as suas testemunhas não são sinalizadas. Este é o
equivalente o1js de onlyOwner.getAndRequireEquals()
é sólido e não será reportado. Isto cobre tanto a forma direta —
amount.assertLessThanOrEqual(bal) — como a forma encadeada
amount.lessThanOrEqual(bal).assertTrue(). A vinculação que reside num
helper não decorado da mesma classe (this.verifyX(arg)) também é reconhecida,
incluindo através de uma cadeia de tais helpers.Proof / SelfProof / DynamicProof /
*Proof sobre o qual .verify() é chamado é restringido pelo
circuito verificado — os achados de testemunha sobre ele (e o seu publicOutput /
publicInput) são suprimidos. Um .verifyIf(flag) só é creditado quando a
condição não é um argumento de método sem restrições, ou é ela própria asseverada. O
mesmo se aplica ao wrapper canónico OffchainState
this.offchainState.settle(proof) (o framework verifica dentro de settle).
Um .settle(proof) feito à mão não é
assumido como verificador. O caso inverso (argumento tipado como prova nunca verificado
e não liquidado via OffchainState) é reportado como O1JS_UNVERIFIED_PROOF..assertTrue() / .assertFalse(), aninhado em Provable.if(...), ou
atribuído a uma variável local que é posteriormente referenciada, não é reportado como
O1JS_UNASSERTED_BOOL.this.sender.getUnconstrained()
não dispara quando o mesmo @method também chama
this.sender.getAndRequireSignature(), ou quando esse valor testemunhado é
passado a AccountUpdate.createSigned(...) / autenticado via
.requireSignature() num AccountUpdate construído a partir dele (é exigida identidade
do argumento).assert
dentro de uma string não pode criar um resultado falso.A mesma ideia de solidez — testemunhas sub-restritas — aplica-se aos
circuitos Noir (.nr). Aponte o scanner a ficheiros .nr
(ou use --lang noir) e ele analisa-os com o conjunto de regras Noir.
Mesma abordagem lexical, sem dependências. Calibrado contra idiomas de oráculo /
unsafe do aztec-nr — ver docs/noir_calibration.md.
| Regra | Severidade | O que significa |
|---|---|---|
NOIR_UNCONSTRAINED_WITNESS | high | Um valor vinculado a partir de um bloco unsafe { ... } — o resultado de uma unconstrained fn (oráculo / dica Brillig) — que nunca é re-restringido por um assert / assert_eq (ou um helper de confirmação / verificação de merkle). A dica corre fora do circuito. Análogo de O1JS_UNCONSTRAINED_PROVABLE_WITNESS. |
NOIR_UNCONSTRAINED_INPUT | medium | Uma entrada privada (testemunha) de fn main que não flui para nenhum assert / assert_eq e não faz parte da saída pública. Análogo de O1JS_UNCONSTRAINED_WITNESS. |
NOIR_UNCONSTRAINED_PUBLIC_INPUT | medium | Uma entrada pública de fn main que não atinge nenhuma restrição nem nenhuma saída — o circuito nunca a lê. O dual da regra da testemunha privada: o verificador fornece o valor e acredita que a afirmação é sobre ele, enquanto o circuito o ignora (por exemplo, um merkle_root: pub Field que nunca é verificado, pelo que a pertença nunca foi realmente provada). MEDIUM porque uma entrada pública deliberadamente não usada é também um idioma legítimo para vincular uma prova a um contexto (nonce / chain id / destinatário), que é lexicalmente indistinguível — pelo que não faz o gate da CI no --fail-on high padrão. |
NOIR_UNCHECKED_CAST | medium | Um valor controlado pelo provador convertido para um tipo unsigned estreito (as u8/u16/u32) com nenhuma asserção de intervalo. Análogo de MissingRangeCheck do o1js. |
NOIR_UNCONSTRAINED_ARRAY_INDEX | medium | Um valor controlado pelo provador usado como índice de array (arr[i]) com nenhuma verificação de qualquer tipo sobre ele. A verificação implícita de limites do Noir estabelece apenas que o índice está dentro do intervalo — não que é o índice correto — pelo que o provador continua livre de selecionar qualquer elemento e ainda produzir uma prova que verifica. Este é o bug de liberdade de seletor por trás das posições de caminho de Merkle, seleção de notas e pertença a allow-list. Suprimido quando o índice tem limite de intervalo, é fixado por uma igualdade, é limitado antes de uma conversão (), ou quando o valor lido de volta é ele próprio fixado por um . |
unsafe.constrain_* / confirm_* / verify_* /
check_(non_)membership* / public_data_storage_read creditam argumentos (com
deteção de resultado não usado para verificações descartadas).// Safety: adjacente):
random(), avm::…, e formulação diferida de kernel/rollup/discovery.let de tuplo + flags asseveradas vinculam testemunhas de merkle passadas para verificações de pertença.Exemplo:```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)
Como no exemplo o1js acima, `--include-examples` só é necessário porque
esses arquivos de demonstração ficam em `examples/`.
## Limitações conhecidas
O analisador é um **frontend lexical sem dependências mais uma camada semântica leve** que faz rastreamento de alias e propagação interprocedural através de helpers da mesma classe. Ele não é um frontend do compilador TypeScript, um verificador de tipos ou um motor de fluxo de dados de programa inteiro, e não há camada de SMT ou prova formal neste scanner.
Tenha esses pontos cegos em mente ao fazer a triagem — eles são conhecidos e intencionais
para este design sem dependências, não bugs:
- **Apenas aliases simples são seguidos.** O rastreamento de testemunhas segue aliases
simples do mesmo método, como `const q = qty`, mas não expressões derivadas ou
desestruturação: ```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
A vinculação entre métodos cobre apenas cadeias de helpers da mesma classe. Um
helper da mesma classe sem decorador, chamado como this.verifyX(arg), pode vincular
o argumento de um chamador ao estado, e desde a 0.19.0 cadeias deles
(@method → helper A → helper B) são seguidas até um ponto fixo. O
passo helper→helper mapeia apenas uma referência de parâmetro simples, então
helperA(x.add(1)) não se propaga. Funções livres e importadas ainda não
são seguidas, e o aliasing de variável local do argumento do helper
permanece uma limitação documentada.
A detecção de Bool não asseverado tem forma de instrução. O Tier A apenas sinaliza
instruções de expressão simples cuja chamada mais externa é um predicado Bool sem nada
encadeado depois. Predicados aninhados dentro de Provable.if(...), ou atribuídos
e usados posteriormente, não são sinalizados. Usos complexos de fluxo de controle de um
local Bool ainda podem passar despercebidos se o nome nunca for referenciado (modo de falha: omissão,
não um falso positivo).
O gating por assinatura é em nível de método e baseado em substring.
_method_is_signature_gated trata um @method inteiro como gated pelo proprietário se ele
contiver um idioma de assinatura, e reconhece um verificador apenas quando o
nome do receptor contém literalmente signature — então sig.verify(admin, msg)
não é reconhecido como gating, enquanto uma verificação de assinatura não relacionada em outro lugar
de um método grande pode suprimir em excesso. É tudo ou nada por método.
A autenticação do remetente é baseada em nome e apenas no mesmo método.
O1JS_UNCONSTRAINED_SENDER suprime quando this.sender.getAndRequireSignature()
ou AccountUpdate.createSigned(<that sender>) aparece no mesmo corpo do @method.
Um requisito de assinatura que vive apenas em um helper
(this.requireSenderSig() → getAndRequireSignature dentro) não é
seguido — o modo de falha é um falso positivo em código correto que envolve o
idioma, não um bug real perdido.
Helpers cross-crate do Noir são reconhecidos apenas por convenção de nome (sem
resolução de Nargo.toml / import). Prefira omissão a falso positivo.
Essas são a razão pela qual os achados são um ponto de partida para revisão humana, não provas. Uma reescrita com reconhecimento de fluxo de dados está deliberadamente fora do escopo do analisador léxico.
o1js-scan é deliberadamente uma passagem léxica superficial de arquivo único — sem parser, sem fluxo de dados, sem solver. É isso que o torna livre de dependências e instantâneo em CI, e também é um teto rígido. As limitações acima não são um backlog; são consequências do design.
Então vale ser explícito sobre o que esta ferramenta pode e não pode lhe dizer:
Essa troca é a correta para um linter que você executa em cada commit. Se você está trabalhando em algo onde a diferença importa — um protocolo que detém valor real, um circuito que você não pode errar — trate isto como a primeira passagem e reserve orçamento para uma revisão de verdade.
Para análise mais profunda, o scanner completo separado é mantido no
repositório audit-engine-cli.
o1js-scan é o scanner aberto intencionalmente leve; o conhecimento de detecção proprietário
e os detalhes de implementação do scanner completo não são reproduzidos
aqui. Para acesso ou uma revisão de circuito mais completa, entre em contato:
[email protected].
A CLI instalada analisa arquivos localmente. Ela não tem telemetria, cliente de rede,
conta ou etapa de upload, e seu runtime Python não tem dependências de terceiros. Executar
o1js-scan path/to/private-repo não envia o código-fonte
ou os achados a lugar nenhum.
Como logs de compilador, a saída do scanner pode conter caminhos, identificadores e fragmentos de código-fonte. O SARIF também identifica localizações exatas do repositório, e a GitHub Action o envia para o code scanning do GitHub. Use os mesmos controles de acesso de repositório e CI que você já usa para o código-fonte sendo escaneado.
Quer contribuir com um relatório útil de falso positivo ou detecção perdida sem compartilhar uma aplicação? Reproduza a sintaxe com nomes e constantes inventados, remova a lógica de negócio uma instrução por vez, e verifique se o trecho sintético ainda dispara a mesma regra antes de publicá-lo. O guia de contribuição seguro para a privacidade tem uma checklist concreta e várias formas de ajudar a comunidade o1js sem divulgar um circuito privado.
Esse limite não impede que o scanner aberto melhore. Documentação e repositórios públicos de o1js podem apoiar novas regras e fixtures de compatibilidade; exemplos sintéticos podem testar falsos positivos e restrições perdidas; e resiliência do parser, diagnósticos, SARIF, desempenho, empacotamento e calibração podem todos melhorar sem publicar uma técnica de auditoria privada ou código de cliente. O scanner aberto deve fazer afirmações independentemente explicáveis; a pesquisa privada pode permanecer no motor de auditoria separado.
O risco quântico está relacionado à segurança de circuitos, mas não é uma regra de restrição
ausente. o1js-scan não determina se uma assinatura, hash, commitment, o
sistema de prova Kimchi, ou a própria Mina atende a um alvo de segurança pós-quântica. Essas
respostas dependem da primitiva e dos parâmetros concretos, das suposições de plataforma,
do tempo de vida exigido pela implantação e do seu plano de migração — não meramente de um
identificador TypeScript que um scanner léxico consegue ver.
Inspirado pelo Qubit or Not Qubit da O(1) Labs, o guia de revisão pós-quântica transforma esse limite em um inventário específico de o1js e uma checklist de agilidade criptográfica. Use-o junto com este scanner em vez de interpretar uma varredura limpa como uma avaliação pós-quântica.
Funciona em o1js 1.x, 2.x e 3.x, incluindo o hard fork Mesa que o o1js
3.0.0 tem como alvo. o1js-scan analisa código-fonte TypeScript como texto e não tem
dependência de runtime do o1js — nada é fixado por versão. Ele se baseia na moderna
API de pré-condição require* (getAndRequireEquals, requireEquals,
requireSignature, getAndRequireSignature), nos
decoradores @method / @method() / @method.returns(...), campos @state
anotados, this.send({...}), transferências de baixo nível AccountUpdate.balance.subInPlace(...),
e Permissions.*. As formas estabelecidas permanecem compatíveis através
das fronteiras 1.x → 2.x → 3.x, enquanto o scanner também aceita as variantes de decorador
e transferência de baixo nível recém-documentadas.
O idioma de autenticação de proprietário do 2.x this.sender.getAndRequireSignature() é reconhecido
como gating de assinatura. (Pré-condições legadas assertEquals ainda são aceitas,
então código mais antigo também não quebra.)
As mudanças incompatíveis do Mesa são todas em nível de runtime e protocolo — a remoção de
Transaction.setFeePerSnarkCost() e das constantes TransactionCost.*, a
nova forma de VerificationKey.toJSON(), chaves de verificação regeneradas,
MAX_ZKAPP_STATE_FIELDS elevado de 8 para 32, e o formato de transação
mina-signer v4. Nenhuma delas renomeia uma API que este scanner corresponde, então nenhuma
regra mudou para o Mesa, e isso é verificado em vez de afirmado.
scripts/o1js_release_matrix.sh escaneia duas releases fixadas do o1js que atravessam
a fronteira de protocolo — 2.15.0 (9620ef08, a última release 2.x) e
3.0.0 (cc18a919, Mesa) — e compara cada achado contra
tests/fixtures/o1js_release_matrix.json:
| Release | Achados | HIGH | MEDIUM | LOW | Arquivos |
|---|---|---|---|---|---|
| o1js 2.15.0 | 36 | 8 | 26 | 2 | 18 |
| o1js 3.0.0 (Mesa) | 39 | 8 | 29 | 2 | 19 |
33 achados são idênticos através da fronteira, nenhum foi perdido, e todos os três
novos estão em src/examples/zkapps/big-state-zkapp.ts — o exemplo de campo de estado 32
que existe apenas porque o Mesa elevou MAX_ZKAPP_STATE_FIELDS. Esse
delta é fixado por um teste, então não pode derivar silenciosamente. A matriz roda em cada
build de CI; o job semanal o1js-upstream-canary adicionalmente rastreia o o1js em
HEAD, à frente de qualquer release.
Grafias equivalentes de restrição são normalizadas para análise: instância
assertEquals(...), estático Provable.assertEqual(Type, ...), e
cadeias de igualdade equals(...).assertTrue() todas vinculam os mesmos operandos. A extração
de método é balanceada por chaves após mascaramento de comentários e strings que preserva comprimento,
e aceita decoradores multilinha, tipos de parâmetro aninhados em forma de callback,
modificadores de acesso TypeScript, e aliases de identidade multilinha (incluindo
formas parentetizadas e as Type).
A análise de Noir tem como alvo a sintaxe Noir usada por projetos Aztec / nargo (.nr); ela
não invoca nargo nem compila circuitos.
É um analisador léxico, não um parser completo de TypeScript ou Noir — fontes o1js e Noir são delimitadas por chaves e tratáveis por regex, e a saída destina-se a ser triada por um humano. Isso o mantém livre de dependências e instantâneo para rodar em CI. Os achados são um ponto de partida para revisão, não provas.
Contribuições são bem-vindas — novas famílias de regras, mais guardas de FP, e arquétipos
de calibração do mundo real são todos valiosos. Veja CONTRIBUTING.md.
Para um caminho proposto da listagem de Community Packages até uma verificação de advisory no repositório o1js, veja a proposta de integração upstream do o1js pronta para envio.
Execute os testes e o linter com:```bash pip install -e ".[dev]" pytest # unit tests + Noir/o1js corpus ruff check . # lint npm run format:check # prettier, npm wrapper only
## Licença
Apache-2.0. Consulte [`LICENSE`](https://github.com/auditinfra-io/o1js-scan/blob/main/LICENSE).
O1JS_STALE_MERKLE_ROOT | high | Um método recalcula uma raiz de Merkle a partir de uma testemunha fornecida pelo provador (computeRootAndKey / calculateRoot) mas não vincula nenhuma das raízes recalculadas à raiz on-chain atual. Sem um this.root.requireEquals(...) / assertEquals contra a raiz em vigor, um provador pode passar uma testemunha para uma árvore fabricada ou obsoleta — forjando a pertença ou repetindo estado antigo. A vinculação pode residir num helper não decorado da mesma classe (this.verifyX(witness)); a propagação de helpers cobre isso. |
O1JS_UNVERIFIED_PROOF | high | Um parâmetro de @method tipado como Proof<...> / SelfProof<...> / DynamicProof<...> nunca é .verify()'d antes de os seus campos públicos serem usados. Passar uma Proof não a verifica — sem uma verificação explícita o provador pode fornecer um objeto de prova arbitrário, e qualquer uso do seu publicOutput fica sem restrições. Também dispara quando .verifyIf(flag) é condicionado por um argumento de @method sem restrições e os campos públicos da prova são lidos, porque o provador pode tornar a condição falsa. |
O1JS_UNASSERTED_BOOL | high / medium | Um predicado o1js (equals / lessThanOrEqual / …) devolve um Bool e não adiciona nenhuma restrição a menos que o resultado seja asseverado ou usado. HIGH quando a chamada é uma instrução simples descartada; MEDIUM quando atribuída a uma variável local que nunca é referenciada novamente. |
O1JS_UNCONSTRAINED_SENDER | high / medium | this.sender.getUnconstrained() devolve o remetente da tx sem o provar. HIGH quando esse valor (ou uma variável local derivada dele) flui para um assert / .set de estado / send (verificação vazia); MEDIUM caso contrário. Prefira this.sender.getAndRequireSignature(), ou o idioma expandido AccountUpdate.createSigned(sender). Mantém-se silencioso quando (1) o mesmo @method também chama this.sender.getAndRequireSignature() em qualquer lugar (o requisito de assinatura é ao nível do método), ou (2) o valor testemunhado do remetente é o argumento de AccountUpdate.createSigned(...) / um AccountUpdate.create(...).requireSignature() sobre essa mesma chave (é exigida identidade do argumento — um createSigned sobre uma chave diferente não suprime). |
MissingRangeCheck | high | Um Field em bruto (não o UInt64/UInt32 com verificação de intervalo) é usado como montante de transferência. Um Field é um elemento mod p e não tem limite de intervalo. |
O1JS_WEAK_PERMISSIONS | high / medium | editState / send definidos como proofOrSignature() ou none(), permitindo que a chave da conta zkApp contorne o circuito ao assinar. Também sinaliza setVerificationKey / setPermissions deixados em signature / proofOrSignature / none (as rodinhas de treino de atualização documentadas da Mina); HIGH quando combinado com um editState/send fraco no mesmo permissions.set. |
O1JS_LOGIC_OUTSIDE_PROOF | high | Lógica de segurança (assert / approve / send / .set de estado) dentro de Provable.asProver(...) ou de um callback Provable.witness*. Esses callbacks correm fora do circuito — um provador malicioso pode eliminá-los e ainda produzir uma prova que verifica. |
O1JS_APPROVE_WITHOUT_BINDING | medium | Um @method chama approve / approveAccountUpdate / approveBase sem ler balanceChange / publicKey e sem assertCanMint / assertCanBurn / uma verificação de conservação forEachUpdate — o arquétipo Mina FlawedTokenContract. |
O1JS_VACUOUS_ASSERT | high / medium | Um assert que é satisfeito por construção: x.assertEquals(x), x.equals(x).assertTrue(), ou Bool(true).assertTrue(). HIGH para auto-comparações (quase sempre um erro de escrita); MEDIUM para asserts de Bool constante. |
O1JS_CONDITIONAL_ASSERT | medium | Um assert dentro de if <flag> { ... } onde <flag> é um Bool de @method controlado pelo provador (ou uma variável local derivada de .toBoolean()). Um condicional JS não restringe o circuito da forma como Provable.if o faz. As comparações inline permanecem não reportadas por precisão. |
O1JS_GUARDED_INVERSE | medium | Um .div() / .inv() / .sqrt() dentro de um ramo de Provable.if, guardado por uma condição sobre o próprio valor em que falha. Ambos os ramos são avaliados in-circuit e estas chamadas asseveram incondicionalmente que o inverso ou a raiz existe, pelo que a guarda não salta a asserção — o circuito é insatisfazível exatamente para a entrada que a guarda foi escrita para tratar, e o método nunca pode ser provado para ela. Reportado pela Veridise como V-O1J-VUL-060. Calcule primeiro um divisor seguro (Provable.if(isZero, Field(1), d)) e selecione o resultado depois. Mantém-se silencioso quando a guarda nada diz sobre o divisor, pelo que um Provable.if não relacionado em torno de uma divisão segura não é sinalizado. |
O1JS_PRECONDITION_OVERWRITTEN | medium | Duas ou mais chamadas requireEquals / requireBetween / requireNothing sobre a mesma propriedade num método, com argumentos diferentes. As pré-condições são definidas no AccountUpdate em vez de acumuladas, pelo que cada chamada substitui a anterior e apenas a última é imposta — ao contrário das asserções in-circuit, que se compõem. a.requireEquals(b) e depois a.requireEquals(c) implica a === c, não a === b. Reportado pela Veridise como V-O1J-VUL-012. Mantém-se silencioso quando os argumentos são idênticos (idempotente, nada se perde), em getAndRequireEquals() (um método diferente, pelo que leituras repetidas de estado são aceitáveis), e quando as chamadas se situam em ramos JS mutuamente exclusivos, que são resolvidos no momento de construção do circuito. Essa última isenção pode ocultar uma sobreposição real que atravessa um if/else não relacionado. |
O1JS_STATE_READ_AFTER_WRITE | medium | Um campo @state é lido (get() / getAndRequireEquals()) depois de um set(...) sobre o mesmo campo concluir, no mesmo método. set() regista a alteração no AccountUpdate mas não a escreve de volta para get(), pelo que a leitura ainda observa o valor anterior à escrita e qualquer aritmética construída sobre ele está silenciosamente desviada por essa escrita. Reportado pela Veridise como V-O1J-VUL-030. Mantenha o novo valor numa variável local em vez de ler o estado de volta. Mantém-se silencioso quando a leitura está aninhada nos próprios argumentos da escrita (o idioma read-modify-write this.x.set(this.x.getAndRequireEquals().add(1)), que está correto), e quando a escrita e a leitura se situam em ramos JS mutuamente exclusivos. Limitado a um único método — o caso de cache entre métodos que a Veridise também descreve requer conhecimento do grafo de chamadas que esta regra não tem. |
index.assert_max_bit_size::<8>(); let i = index as u32;assert_eqNOIR_UNASSERTED_BOOL | high / medium | Uma comparação cujo resultado bool é descartado. Análogo de O1JS_UNASSERTED_BOOL do o1js. |
NOIR_CONDITIONAL_ASSERT | medium | Um assert dentro de if <flag> { ... } onde <flag> é um bool simples controlado pelo provador ou uma variável local derivada de valores controlados pelo provador. Uma restrição dentro de um condicional só se aplica quando a condição é verdadeira, pelo que um ramo escolhido pelo provador pode saltar a verificação. As comparações inline (if x != 0) são deixadas em paz por precisão; atribuir a guarda a uma variável local (let gate = x != 0; if gate) é reportado a menos que gate seja ele próprio asseverado. |
NOIR_CONDITIONAL_CONSTRAIN | medium | Uma chamada constrain_* / confirm_* / verify_* apenas sob um if controlado pelo provador, enquanto uma dica unsafe ainda atinge a saída. |
NOIR_UNUSED_CHECK_RESULT | high / medium | Um resultado de check_* / confirm_* / verify_* / constrain_* é descartado (chamada simples) ou atribuído e nunca asseverado — a verificação não vincula o circuito. |
NOIR_VACUOUS_CONSTRAINT | high / medium | Uma restrição que é satisfeita por construção: uma auto-comparação (assert(x == x), assert_eq(x, x), x >= x) ou uma condição constante (assert(true)). Não adiciona nenhuma restrição, mas a linha lê-se como uma verificação — o que a torna mais perigosa do que uma restrição em falta, porque a revisão para aí. HIGH para uma auto-comparação (quase sempre um erro de escrita para uma verificação real: assert(computed == expected) mal escrito como assert(expected == expected)); MEDIUM para uma constante, que é mais frequentemente um placeholder. x != x não é sinalizado — isso é insatisfazível, um bug de liveness em vez de um buraco de solidez silencioso. |
NOIR_UNSAFE_MISSING_SAFETY | low | Um bloco unsafe { ... } sem um comentário // Safety: adjacente. Informativo; não falha a CI no --fail-on high padrão. |