
Analizador estático sin dependencias para errores de solidez en circuitos zk en o1js/Mina zkApps y circuitos Noir
Paquete de la comunidad:
o1js-scanestá listado en el directorio oficial de Paquetes de la Comunidad de o1js.
Última versión: 0.20.0 — el analizador ahora lee contratos que
extends TokenContract. Hasta esta versión, la puerta de contratos coincidía únicamente conSmartContract, por lo que cada token fungible, colección NFT y pool AMM del ecosistema se escaneaba como "sin hallazgos". Si escaneaste un contrato de token antes de la 0.20.0, vuelve a escanearlo. Consulta el .
Un analizador estático rápido y sin dependencias para bugs de solidez en circuitos zk en:
.ts / .js) — circuitos Kimchi a partir de cuerpos @method.nr) — el DSL ZK de Aztec con sintaxis similar a Rust (incluyendo patrones con forma de aztec-nr)Los bugs críticos para la seguridad normalmente no están en el sistema de pruebas — están en las
restricciones propias de la aplicación: testigos que el probador controla pero que el circuito
nunca vincula. o1js-scan es el escáner de señales sub-restringidas para los primos de Circom en los ecosistemas de Mina y 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
### Ejemplo
Dado un vault cuyo monto de `withdraw` es un testigo controlado por el probador que
nunca está vinculado al 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 es necesario aquí solo porque el archivo de demostración se encuentra bajo
examples/, que el clasificador de rutas degrada por defecto para que el
código de ejemplo de un repositorio no pueda hacer fallar su compilación. El mismo contrato en tu src/ reporta
HIGH sin ninguna bandera.
El hallazgo HIGH es el bug que permite drenar fondos. El contrato corregido
(examples/safe_vault.ts) lo elimina y sale con 0, conservando solo el
LOW informativo sobre el destinatario elegido por el probador:```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
Consulta [`examples/`](https://github.com/auditinfra-io/o1js-scan/blob/main/examples) para los pares vulnerable/corregido de o1js y Noir.
## Contenido
- [Instalación](#install)
- [Uso](#usage) · [Suprimir un hallazgo](#suppressing-a-reviewed-finding)
- [GitHub Action](#github-action)
- [Qué detecta — o1js](#what-it-detects-o1js) · [Noir](#what-it-detects-noir)
- [Limitaciones conocidas](#known-limitations) · [Dónde se detiene esta herramienta](#where-this-tool-stops)
- [Privacidad y código privado](#privacy-and-private-code)
- [Revisión post-cuántica](#post-quantum-review)
- [Compatibilidad](#compatibility) · [Cómo funciona](#how-it-works)
- [Contribuir](#roadmap--contributing)
## Instalación```bash
pip install o1js-scan
Para una instalación global aislada de la CLI, use pipx:```bash
pipx install o1js-scan
Para repositorios de aplicaciones basados en Node/npm como Noir, Aztec u o1js, instala el wrapper de npm:```bash
npm install -D o1js-scan
npx noir-scan . --lang noir --fail-on high
El paquete npm es un envoltorio ligero alrededor del mismo analizador de Python y requiere
Python 3.8+ en el PATH (python3 o python). Establece O1JS_SCAN_PYTHON para elegir un
intérprete específico.
O desde el código fuente:```bash git clone https://github.com/auditinfra-io/o1js-scan cd o1js-scan pip install -e .
Sin dependencias de Python de terceros. Python 3.8+. El script de consola `noir-scan` se
instala junto con `o1js-scan` (mismo punto de entrada), incluso a través del wrapper
de 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
El código de salida es 1 cuando hay un hallazgo igual o superior al nivel de --fail-on (por defecto high) y 0 en caso contrario, por lo que puedes integrarlo directamente en CI. Con el valor predeterminado, un hallazgo de severidad baja/media (incluida la regla informativa de destinatarios que se describe a continuación) no hace fallar la compilación; usa --fail-on none para solo informar, o --strict (una forma abreviada de --fail-on medium) para aplicar un filtro más estricto mientras sigues tratando los hallazgos de severidad baja como informativos. Las dos opciones son mutuamente excluyentes, por lo que la configuración de CI no puede ser ambigua. Una ruta de escaneo inexistente sale con 2 y un error en stderr, de modo que un error tipográfico no puede pasar silenciosamente por CI como una ejecución limpia. Cada ejecución imprime un resumen de una línea (recuentos por severidad y el veredicto del filtro) en stderr.
El código de prueba se excluye por defecto — en ambos backends. Las pruebas construyen deliberadamente valores inválidos y transacciones incorrectas para demostrar que las aserciones las rechazan, por lo que un hallazgo allí es el objetivo de la prueba y no un error del circuito. Un archivo cuenta como código de prueba cuando:
*.test.ts / *.spec.ts (y las variantes .js/.jsx/.tsx/.mjs/.cjs), o *_test.nr / test_*.nr;test/, tests/, __tests__/, spec/ o __mocks__/;#[test] / #[test(...)], o se encuentra dentro de un bloque mod test { … } / mod tests { … } — con alcance de bloque, por lo que un módulo de prueba al final de un archivo de producción no silencia el resto de este.Pasa --include-tests para informarlos.
El código de ejemplo se degrada, no se descarta. Un hallazgo en un directorio examples/ o example/, o en un archivo llamado *.eg.ts (también .nr y las demás extensiones de JS/TS), se reduce a LOW con una nota — sigue informándose, pero ya no puede hacer fallar una compilación. El código de ejemplo se simplifica deliberadamente, y señalar los propios ejemplos de un framework como vulnerabilidades es ruido; pero se copia a producción con mucha más frecuencia que el código de prueba, por lo que se degrada en lugar de ocultarse. Pasa --include-examples para mantener la severidad original.
Siempre que se aplique cualquiera de las dos políticas, la ejecución imprime una línea en stderr indicándolo — p. ej. 6 file(s) skipped as test code, 1 finding(s) downgraded as examples — para que un escaneo silencioso nunca sea silenciosamente silencioso. Los recuentos también aparecen en SARIF bajo invocation.properties. Ten en cuenta la contrapartida: la detección es solo basada en rutas (sin análisis de describe(/it(), por lo que un circuito de producción almacenado bajo tests/ sí se omitirá — la línea de stderr es cómo te das cuenta.
Directorios omitidos al recorrer un árbol: node_modules, target (nargo), .git, dist, build, __pycache__, .venv, venv.
Silencia un hallazgo que hayas triado sin relajar el filtro, con un comentario en línea sobre — o en la línea anterior a — la línea señalada:```ts this.send({ to, amount }); // o1js-scan-disable-line O1JS_UNCONSTRAINED_WITNESS
// o1js-scan-disable-next-line this.send({ to, amount });
| `-l` | `--list` | List all available modules |
| `-m` | `--module` | Specify the module to use |
| `-o` | `--output` | Specify the output file |
| `-p` | `--proxy` | Specify the proxy to use |
| `-r` | `--report` | Generate a report |
| `-s` | `--silent` | Silent mode |
| `-t` | `--threads` | Specify the number of threads |
| `-u` | `--url` | Specify the target URL |
| `-v` | `--verbose` | Verbose mode |
| `-w` | `--wordlist` | Specify the wordlist to use |
| `-x` | `--exclude` | Exclude specific modules |
### Examples
```bash
# Scan a single target
python3 darkus.py -u https://example.com
# Scan multiple targets from a file
python3 darkus.py -f targets.txt
# Use a specific module
python3 darkus.py -u https://example.com -m sqli
# Generate a report
python3 darkus.py -u https://example.com -r report.html
| Module | Description |
|---|---|
sqli | SQL Injection scanner |
xss | Cross-Site Scripting scanner |
lfi | Local File Inclusion scanner |
rfi | Remote File Inclusion scanner |
ssrf | Server-Side Request Forgery scanner |
csrf | Cross-Site Request Forgery scanner |
open-redirect | Open Redirect scanner |
crlf | CRLF Injection scanner |
xxe | XML External Entity scanner |
ssti | Server-Side Template Injection scanner |
The configuration file is located at config/config.yaml. You can modify the following settings:
# General settings
general:
threads: 10
timeout: 30
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
# Proxy settings
proxy:
enabled: false
http: "http://127.0.0.1:8080"
https: "https://127.0.0.1:8080"
# Output settings
output:
format: "html"
directory: "reports"
This tool is intended for educational purposes and authorized security testing only. The author is not responsible for any misuse or damage caused by this tool. Always obtain proper authorization before testing any system.```nr let inv = unsafe { hint(x) }; // o1js-scan-disable-line NOIR_UNCONSTRAINED_WITNESS
Enumere uno o más ids de reglas para suprimir solo esas; una directiva sin ids
suprime todas las reglas en la línea objetivo.
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)
Añade el escáner a CI en unas pocas líneas. Los hallazgos aparecen como anotaciones en el diff del PR y como alertas en la pestaña Security → Code scanning del repositorio.```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
### Receta de CI solo para Noir
Recomendada para proyectos Noir que quieren alertas de escaneo de código y una puerta de alta severidad:```yaml
- uses: auditinfra-io/[email protected]
with:
path: .
lang: noir
fail-on: high
O sin la Acción:```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 (por defecto .), lang (auto|o1js|noir, por defecto auto),
version (versión de PyPI a instalar, por defecto la última), upload-sarif (por defecto
true), fail-on (critical|high|medium|low|none, por defecto none),
fail-on-findings (obsoleto, por defecto false), include-tests (por defecto
false), include-examples (por defecto false). Salida: sarif-file. La carga de SARIF
requiere security-events: write y el escaneo de código habilitado.
El informe y la compuerta se construyen a partir de un único arreglo de argumentos, por lo que include-tests
e include-examples se aplican a ambos — el SARIF que lees y el código de salida sobre el que
aplicas la compuerta siempre describen el mismo conjunto de fuentes. La pasada de informes se ejecuta con
--fail-on none para que los hallazgos nunca bloqueen la carga de SARIF, pero un fallo
operativo (una ruta que no existe, un error de uso de la CLI) aún hace fallar el paso
en lugar de reportarse como un escaneo limpio.
fail-on-findings: true se mantiene por compatibilidad y se asigna a fail-on: high
cuando fail-on se deja en none; emite una advertencia de obsolescencia. Prefiere
fail-on, que puede aplicar la compuerta en cualquier severidad.
| Backend | Reglas | Capacidad alta | Capacidad media | Capacidad baja |
|---|---|---|---|---|
| o1js | 18 | 11 | 12 | 2 |
| Noir | 11 | 4 | 9 | 1 |
| Total | 29 | 15 | 21 | 3 |
Los recuentos son IDs de reglas distintos compatibles con cada backend. Una regla que asigna severidad según el contexto (por ejemplo, alta para una transferencia de valor y media para una escritura de estado) aparece en más de una columna de severidad, por lo que las columnas de severidad intencionalmente no suman el total de reglas. Actualmente no hay reglas de severidad crítica ni informativa. Las descripciones completas y las protecciones contra falsos positivos se detallan a continuación.
| Regla | Severidad | Qué significa |
|---|---|---|
O1JS_MISSING_STATE_PRECONDITION | alta | Lectura de this.x.get() sin un requireEquals(...) / getAndRequireEquals() correspondiente. Un get() sin más no añade ninguna precondición de cuenta, por lo que la prueba no vincula x a su valor en cadena — un probador puede sustituir cualquier valor. |
O1JS_UNCONSTRAINED_WITNESS | alta / media | Un argumento de @method (un testigo privado controlado por el probador) fluye hacia un monto de envío (this.send(...) o un AccountUpdate.create*(...).send(...) del mismo método) o un .set(...) de estado y nunca se asevera. Análogo directo de una señal Circom sub-restringida. Alta cuando alcanza una transferencia de valor. |
O1JS_UNCONSTRAINED_PROVABLE_WITNESS | alta / media / baja | Una variable local de Provable.witness(...) fluye hacia un efecto de envío/estado con ninguna aserción en circuito. El callback del testigo se ejecuta fuera del circuito (es solo una pista para el probador), por lo que el resultado es un valor fresco controlado por el probador — la otra fuente de testigos además de los argumentos de @method. Debe re-derivarse y aseverarse (x.assertEquals(<recomputed>)) o vincularse al estado. Alta en un monto de envío (this.send(...) o AccountUpdate.create* del mismo método). |
O1JS_UNCONSTRAINED_RECIPIENT | baja | Un argumento de @method se usa solo como el destinatario to: de un envío. Esto suele ser intencional (un usuario nombra su propio destino de retiro) y es informativo — solo importa si el destino está pensado para ser una tesorería fija o una dirección registrada en estado. No activa la compuerta del código de salida de CI. |
O1JS_WITNESS_NOT_BOUND_TO_STATE | media | Un testigo solo está restringido trivialmente (p. ej. > 0, o comparado contra una constante) antes de un efecto — nunca vinculado al estado en cadena. Confirma que la orquestación fuera de cadena hace esto seguro, o el saldo es drenable hasta su valor permanente. |
El analizador está diseñado para mantenerse silencioso ante código correcto:
@method que llama a
this.requireSignature() (o getAndRequireSignature, AccountUpdate.createSigned,
Signature.verify) tiene compuerta de propietario/administrador — sus argumentos son elegidos por el
titular de la clave, no por un probador arbitrario — por lo que sus testigos no se marcan. Este es el
equivalente en o1js de onlyOwner.getAndRequireEquals()
es sólido y no se reportará. Esto cubre tanto la forma directa —
amount.assertLessThanOrEqual(bal) — como la forma encadenada
amount.lessThanOrEqual(bal).assertTrue(). La vinculación que reside en un
helper no decorado de la misma clase (this.verifyX(arg)) también se reconoce,
incluso a través de una cadena de tales helpers.Proof / SelfProof / DynamicProof /
*Proof sobre el que se llama a .verify() está restringido por el
circuito verificado — los hallazgos de testigos sobre él (y su publicOutput /
publicInput) se suprimen. Un .verifyIf(flag) se acredita solo cuando la
condición no es un argumento de método sin restringir, o ella misma está aseverada. Lo
mismo se aplica al envoltorio canónico OffchainState
this.offchainState.settle(proof) (el framework verifica dentro de settle).
Un .settle(proof) hecho a mano no se
asume que verifica. El caso inverso (argumento tipado como prueba nunca verificado
y no liquidado por OffchainState) se reporta como O1JS_UNVERIFIED_PROOF..assertTrue() / .assertFalse(), anidado en Provable.if(...), o
asignado a una variable local que luego se referencia, no se reporta como
O1JS_UNASSERTED_BOOL.this.sender.getUnconstrained()
no se activa cuando el mismo @method también llama a
this.sender.getAndRequireSignature(), o cuando ese valor testificado se
pasa a AccountUpdate.createSigned(...) / se autentica mediante
.requireSignature() en un AccountUpdate construido a partir de él (se requiere identidad
del argumento).assert
dentro de una cadena no puede crear un resultado falso.La misma idea de solidez — testigos sub-restringidos — se aplica a
Noir (circuitos .nr). Apunta el escáner a archivos .nr
(o usa --lang noir) y los analiza con el conjunto de reglas de Noir.
Mismo enfoque léxico, sin dependencias. Calibrado contra los modismos de oráculo /
unsafe de aztec-nr — consulta docs/noir_calibration.md.
| Regla | Severidad | Qué significa |
|---|---|---|
NOIR_UNCONSTRAINED_WITNESS | alta | Un valor vinculado desde un bloque unsafe { ... } — el resultado de una unconstrained fn (pista de oráculo / Brillig) — que nunca se re-restringe mediante un assert / assert_eq (o un helper de confirmación / verificación de merkle). La pista se ejecuta fuera del circuito. Análogo de O1JS_UNCONSTRAINED_PROVABLE_WITNESS. |
NOIR_UNCONSTRAINED_INPUT | media | Una entrada privada (testigo) de fn main que no fluye hacia ningún assert / assert_eq y no forma parte de la salida pública. Análogo de O1JS_UNCONSTRAINED_WITNESS. |
NOIR_UNCONSTRAINED_PUBLIC_INPUT | media | Una entrada pública de fn main que no alcanza ninguna restricción ni salida — el circuito nunca la lee. El dual de la regla de testigo privado: el verificador suministra el valor y cree que la declaración es sobre él, mientras el circuito lo ignora (p. ej. un merkle_root: pub Field que nunca se verifica, por lo que la pertenencia nunca se probó realmente). MEDIA porque una entrada pública deliberadamente no usada también es un modismo legítimo para vincular una prueba a un contexto (nonce / id de cadena / destinatario), que es léxicamente indistinguible — por lo que no aplica la compuerta de CI con el --fail-on high por defecto. |
NOIR_UNCHECKED_CAST | media | Un valor controlado por el probador convertido a un tipo sin signo estrecho (as u8/u16/u32) con ninguna aserción de rango. Análogo de MissingRangeCheck de o1js. |
NOIR_UNCONSTRAINED_ARRAY_INDEX | media | Un valor controlado por el probador usado como índice de arreglo (arr[i]) con ninguna verificación de ningún tipo sobre él. La verificación implícita de límites de Noir establece solo que el índice está en rango — no que sea el índice correcto — por lo que el probador sigue siendo libre de seleccionar cualquier elemento y aún producir una prueba que verifica. Este es el bug de libertad de selector detrás de las posiciones de ruta de Merkle, la selección de notas y la pertenencia a listas de permitidos. Se suprime cuando el índice está acotado por rango, fijado por una igualdad, acotado antes de una conversión (), o cuando el valor leído de vuelta está a su vez fijado por un . |
unsafe.constrain_* / confirm_* / verify_* /
check_(non_)membership* / public_data_storage_read acreditan los argumentos (con
detección de resultado no usado para verificaciones descartadas).// Safety: adyacente):
random(), avm::…, y redacción diferida de kernel/rollup/discovery.let de tupla + flags aseverados vinculan testigos de merkle pasados a verificaciones de pertenencia.Ejemplo:```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 en el ejemplo de o1js anterior, `--include-examples` solo es necesario porque
estos archivos de demostración se encuentran bajo `examples/`.
## Limitaciones conocidas
El analizador es un **frontend léxico sin dependencias más una capa semántica ligera** que
realiza seguimiento de alias y propagación interprocedural a través de helpers de la misma clase. No es un frontend del compilador de TypeScript, un verificador de tipos ni un motor de flujo de datos de programa completo, y no hay una capa SMT ni de prueba formal en este escáner.
Ten en cuenta estos puntos ciegos al realizar la triaje — son conocidos e intencionales
para este diseño sin dependencias, no errores:
- **Solo se siguen alias simples.** El seguimiento de testigos sigue alias simples
del mismo método como `const q = qty`, pero no expresiones derivadas ni
desestructuración: ```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
El binding entre métodos cubre únicamente cadenas de helpers de la misma clase. Un
helper sin decorar de la misma clase invocado como this.verifyX(arg) puede enlazar
por estado el argumento de un llamador, y desde 0.19.0 las cadenas de estos
(@method → helper A → helper B) se siguen hasta un punto fijo. El
paso helper→helper mapea solo una referencia de parámetro desnuda, por lo que
helperA(x.add(1)) no se propaga. Las funciones libres e importadas
siguen sin seguirse, y el aliasing de variables locales del argumento del helper
sigue siendo una limitación documentada.
La detección de Bool no asertado tiene forma de sentencia. El Tier A solo marca
sentencias de expresión desnudas cuya llamada más externa es un predicado Bool sin nada
encadenado después. Los predicados anidados dentro de Provable.if(...), o asignados
y usados posteriormente, no se marcan. Los usos complejos de flujo de control de un local Bool
aún pueden pasarse por alto si el nombre nunca se referencia (modo de fallo: omisión,
no un falso positivo).
El signature-gating es a nivel de método y basado en subcadenas.
_method_is_signature_gated trata un @method completo como owner-gated si
contiene un idioma de firma, y reconoce un verificador solo cuando el
nombre del receptor contiene literalmente signature — por lo que sig.verify(admin, msg)
no se reconoce como gating, mientras que una comprobación de firma no relacionada en otro lugar
de un método grande puede sobresuprimir. Es todo o nada por método.
La autenticación del remitente se basa en el nombre y solo en el mismo método.
O1JS_UNCONSTRAINED_SENDER suprime cuando this.sender.getAndRequireSignature()
o AccountUpdate.createSigned(<that sender>) aparece en el mismo cuerpo de @method.
Un requisito de firma que vive solo en un helper
(this.requireSenderSig() → getAndRequireSignature dentro) no se
sigue — el modo de fallo es un falso positivo en código correcto que envuelve el
idioma, no un bug real omitido.
Los helpers cross-crate de Noir se reconocen solo por convención de nombres (sin
resolución de Nargo.toml / imports). Preferir omisión sobre falso positivo.
Estas son la razón por la que los hallazgos son un punto de partida para la revisión humana, no pruebas. Una reescritura consciente del flujo de datos está deliberadamente fuera del alcance del analizador léxico.
o1js-scan es deliberadamente un paso léxico superficial de un solo archivo — sin parser, sin flujo de datos, sin solver. Eso es lo que lo hace libre de dependencias e instantáneo en CI, y también es un techo duro. Las limitaciones anteriores no son un backlog; son consecuencias del diseño.
Por lo tanto, vale la pena ser explícito sobre lo que esta herramienta puede y no puede decirte:
Ese intercambio es el correcto para un linter que ejecutas en cada commit. Si estás trabajando en algo donde la diferencia importa — un protocolo que contiene valor real, un circuito que no puedes permitirte equivocar — trátalo como la primera pasada y presupuesta para una revisión real.
Para un análisis más profundo, el escáner completo separado se mantiene en el
repositorio audit-engine-cli.
o1js-scan es el escáner abierto intencionalmente ligero; el conocimiento de detección propietario
y los detalles de implementación del escáner completo no se reproducen
aquí. Para acceso o una revisión de circuito más completa, contacta:
[email protected].
El CLI instalado analiza archivos localmente. No tiene telemetría, cliente de red,
cuenta ni paso de subida, y su runtime de Python no tiene dependencias de terceros.
Ejecutar o1js-scan path/to/private-repo no envía el código fuente
ni los hallazgos a ningún lugar.
Como los logs del compilador, la salida del escáner puede contener rutas, identificadores y fragmentos de código fuente. SARIF también identifica ubicaciones exactas del repositorio, y la GitHub Action lo sube al code scanning de GitHub. Usa los mismos controles de acceso de repositorio y CI que ya usas para el código fuente que se está escaneando.
¿Quieres contribuir con un informe útil de falso positivo o detección omitida sin compartir una aplicación? Reproduce la sintaxis con nombres y constantes inventados, elimina la lógica de negocio una sentencia a la vez, y verifica que el fragmento sintético sigue disparando la misma regla antes de publicarlo. La guía de contribución segura para la privacidad tiene una checklist concreta y varias formas de ayudar a la comunidad o1js sin divulgar un circuito privado.
Este límite no impide que el escáner abierto mejore. La documentación pública de o1js y los repositorios pueden respaldar nuevas reglas y fixtures de compatibilidad; los ejemplos sintéticos pueden probar falsos positivos y restricciones omitidas; y la resiliencia del parser, los diagnósticos, SARIF, el rendimiento, el empaquetado y la calibración pueden mejorar sin publicar una técnica de auditoría privada o código de cliente. El escáner abierto debe hacer afirmaciones explicables de forma independiente; la investigación privada puede permanecer en el motor de auditoría separado.
El riesgo cuántico está relacionado con la seguridad del circuito, pero no es una regla de
restricción faltante. o1js-scan no determina si una firma, hash, commitment, el
sistema de pruebas Kimchi o Mina mismo cumple un objetivo de seguridad post-cuántica. Esas
respuestas dependen de la primitiva y los parámetros concretos, los supuestos de la plataforma,
el tiempo de vida requerido del despliegue y su plan de migración — no meramente de un
identificador de TypeScript que un escáner léxico puede ver.
Inspirado por Qubit or Not Qubit de O(1) Labs, la guía de revisión post-cuántica convierte ese límite en un inventario específico de o1js y una checklist de agilidad criptográfica. Úsala junto con este escáner en lugar de interpretar un escaneo limpio como una evaluación post-cuántica.
Funciona en o1js 1.x, 2.x y 3.x, incluido el hard fork Mesa al que apunta o1js
3.0.0. o1js-scan analiza el código fuente de TypeScript como texto y no tiene dependencia
en tiempo de ejecución de o1js — nada está fijado a una versión. Se basa en la API moderna
de precondiciones require* (getAndRequireEquals, requireEquals,
requireSignature, getAndRequireSignature), los
decoradores @method / @method() / @method.returns(...), los campos @state
anotados, this.send({...}), las transferencias de bajo nivel AccountUpdate.balance.subInPlace(...)
y Permissions.*. Las formas establecidas siguen siendo compatibles a través de
los límites 1.x → 2.x → 3.x, mientras que el escáner también acepta las variantes de decorador
y transferencia de bajo nivel recientemente documentadas.
El idioma de autenticación de propietario de 2.x this.sender.getAndRequireSignature() se reconoce
como signature-gating. (Las precondiciones heredadas assertEquals todavía se aceptan,
así que el código más antiguo tampoco se rompe.)
Los cambios disruptivos de Mesa son todos a nivel de runtime y protocolo — la eliminación de
Transaction.setFeePerSnarkCost() y las constantes TransactionCost.*, la
nueva forma de VerificationKey.toJSON(), las claves de verificación regeneradas,
MAX_ZKAPP_STATE_FIELDS elevado de 8 a 32, y el formato de transacción de
mina-signer v4. Ninguno de ellos renombra una API que este escáner detecte, por lo que ninguna
regla cambió para Mesa, y eso está verificado en lugar de afirmado.
scripts/o1js_release_matrix.sh escanea dos releases de o1js fijados que abarcan
el límite del protocolo — 2.15.0 (9620ef08, el último release de 2.x) y
3.0.0 (cc18a919, Mesa) — y compara cada hallazgo contra
tests/fixtures/o1js_release_matrix.json:
| Release | Hallazgos | HIGH | MEDIUM | LOW | Archivos |
|---|---|---|---|---|---|
| o1js 2.15.0 | 36 | 8 | 26 | 2 | 18 |
| o1js 3.0.0 (Mesa) | 39 | 8 | 29 | 2 | 19 |
33 hallazgos son idénticos a través del límite, ninguno se perdió, y los tres
nuevos están en src/examples/zkapps/big-state-zkapp.ts — el ejemplo de 32 campos de estado
que existe solo porque Mesa elevó MAX_ZKAPP_STATE_FIELDS. Ese
delta está fijado por un test, por lo que no puede desviarse silenciosamente. La matriz se ejecuta en cada
build de CI; el job semanal o1js-upstream-canary además rastrea o1js en
HEAD, por delante de cualquier release.
Las grafías equivalentes de restricciones se normalizan para el análisis: instancia
assertEquals(...), estático Provable.assertEqual(Type, ...), y
cadenas de igualdad equals(...).assertTrue() todas enlazan los mismos operandos. La extracción
de métodos está balanceada por llaves tras el enmascaramiento de comentarios y cadenas que preserva la longitud,
y acepta decoradores multilínea, tipos de parámetros con forma de callback anidados,
modificadores de acceso de TypeScript y alias de identidad multilínea (incluyendo
formas entre paréntesis y as Type).
El análisis de Noir apunta a la sintaxis de Noir usada por proyectos Aztec / nargo (.nr); no
invoca nargo ni compila circuitos.
Es un analizador léxico, no un parser completo de TypeScript o Noir — las fuentes de o1js y Noir están delimitadas por llaves y son tratables con regex, y la salida está pensada para ser triada por un humano. Eso lo mantiene libre de dependencias e instantáneo de ejecutar en CI. Los hallazgos son un punto de partida para la revisión, no pruebas.
Contribuciones bienvenidas — nuevas familias de reglas, más guardas de FP y arquetipos de
calibración del mundo real son todos valiosos. Consulta CONTRIBUTING.md.
Para una ruta propuesta desde el listado de Community Packages hasta una comprobación de aviso en el repositorio de o1js, consulta la propuesta de integración upstream de o1js lista para enviar.
Ejecuta los tests y el linter con:```bash pip install -e ".[dev]" pytest # unit tests + Noir/o1js corpus ruff check . # lint npm run format:check # prettier, npm wrapper only
## Licencia
Apache-2.0. Ver [`LICENSE`](https://github.com/auditinfra-io/o1js-scan/blob/main/LICENSE).
O1JS_STALE_MERKLE_ROOT | alta | Un método recalcula una raíz de Merkle a partir de un testigo proporcionado por el probador (computeRootAndKey / calculateRoot) pero no vincula ninguna de las raíces recalculadas a la raíz actual en cadena. Sin un this.root.requireEquals(...) / assertEquals contra la raíz viva, un probador puede pasar un testigo para un árbol fabricado u obsoleto — forjando pertenencia o reproduciendo estado antiguo. La vinculación puede residir en un helper no decorado de la misma clase (this.verifyX(witness)); la propagación de helpers cubre eso. |
O1JS_UNVERIFIED_PROOF | alta | Un parámetro de @method tipado como Proof<...> / SelfProof<...> / DynamicProof<...> nunca se .verify() antes de que se usen sus campos públicos. Pasar una Proof no la verifica — sin una verificación explícita el probador puede suministrar un objeto de prueba arbitrario, y cualquier uso de su publicOutput queda sin restringir. También se activa cuando .verifyIf(flag) está condicionado por un argumento de @method sin restringir y se leen los campos públicos de la prueba, porque el probador puede hacer que la condición sea falsa. |
O1JS_UNASSERTED_BOOL | alta / media | Un predicado de o1js (equals / lessThanOrEqual / …) devuelve un Bool y no añade ninguna restricción a menos que el resultado se asevere o se use. ALTA cuando la llamada es una sentencia desechada sin más; MEDIA cuando se asigna a una variable local que nunca se vuelve a referenciar. |
O1JS_UNCONSTRAINED_SENDER | alta / media | this.sender.getUnconstrained() devuelve el remitente de la tx sin probarlo. ALTA cuando ese valor (o una variable local derivada de él) fluye hacia un assert / .set de estado / send (verificación vacua); MEDIA en caso contrario. Prefiere this.sender.getAndRequireSignature(), o el modismo expandido AccountUpdate.createSigned(sender). Se mantiene silenciosa cuando (1) el mismo @method también llama a this.sender.getAndRequireSignature() en cualquier lugar (el requisito de firma tiene alcance de método), o (2) el valor del remitente testificado es el argumento de AccountUpdate.createSigned(...) / un AccountUpdate.create(...).requireSignature() sobre esa misma clave (se requiere identidad del argumento — un createSigned sobre una clave diferente no lo suprime). |
MissingRangeCheck | alta | Un Field sin procesar (no el UInt64/UInt32 con verificación de rango) se usa como monto de transferencia. Un Field es un elemento mód p y no está acotado por rango. |
O1JS_WEAK_PERMISSIONS | alta / media | editState / send establecidos en proofOrSignature() o none(), permitiendo que la clave de la cuenta zkApp evite el circuito firmando. También marca setVerificationKey / setPermissions dejados en signature / proofOrSignature / none (las ruedas de entrenamiento de actualización documentadas de Mina); ALTA cuando se combina con un editState/send débil en el mismo permissions.set. |
O1JS_LOGIC_OUTSIDE_PROOF | alta | Lógica de seguridad (assert / approve / send / .set de estado) dentro de Provable.asProver(...) o un callback de Provable.witness*. Esos callbacks se ejecutan fuera del circuito — un probador malicioso puede eliminarlos y aún producir una prueba que verifica. |
O1JS_APPROVE_WITHOUT_BINDING | media | Un @method llama a approve / approveAccountUpdate / approveBase sin leer balanceChange / publicKey y sin assertCanMint / assertCanBurn / una verificación de conservación forEachUpdate — el arquetipo de Mina FlawedTokenContract. |
O1JS_VACUOUS_ASSERT | alta / media | Un assert que se satisface por construcción: x.assertEquals(x), x.equals(x).assertTrue(), o Bool(true).assertTrue(). ALTA para autocomparaciones (casi siempre un error tipográfico); MEDIA para asserts de Bool constante. |
O1JS_CONDITIONAL_ASSERT | media | Un assert dentro de if <flag> { ... } donde <flag> es un Bool de @method controlado por el probador (o una variable local de .toBoolean()). Un condicional de JS no restringe el circuito como lo hace Provable.if. Las comparaciones en línea se dejan sin reportar por precisión. |
O1JS_GUARDED_INVERSE | media | Un .div() / .inv() / .sqrt() dentro de una rama de Provable.if, protegido por una condición sobre el mismísimo valor con el que falla. Ambas ramas se evalúan en circuito y estas llamadas aseveran incondicionalmente que la inversa o la raíz existe, por lo que la guarda no omite la aserción — el circuito es insatisfacible exactamente para la entrada que la guarda fue escrita para manejar, y el método nunca puede probarse para ella. Reportado por Veridise como V-O1J-VUL-060. Calcula primero un divisor seguro (Provable.if(isZero, Field(1), d)) y selecciona el resultado después. Se mantiene silenciosa cuando la guarda no dice nada sobre el divisor, por lo que un Provable.if no relacionado alrededor de una división segura no se marca. |
O1JS_PRECONDITION_OVERWRITTEN | media | Dos o más llamadas a requireEquals / requireBetween / requireNothing sobre la misma propiedad en un método, con argumentos diferentes. Las precondiciones se establecen en el AccountUpdate en lugar de acumularse, por lo que cada llamada sobrescribe la anterior y solo se aplica la última — a diferencia de las aserciones en circuito, que se componen. a.requireEquals(b) y luego a.requireEquals(c) implica a === c, no a === b. Reportado por Veridise como V-O1J-VUL-012. Se mantiene silenciosa cuando los argumentos son idénticos (idempotente, nada se pierde), en getAndRequireEquals() (un método diferente, por lo que las lecturas de estado repetidas están bien), y cuando las llamadas se sitúan en ramas de JS mutuamente excluyentes, que se resuelven en tiempo de construcción del circuito. Esa última exención puede ocultar una sobrescritura real que cruza un if/else no relacionado. |
O1JS_STATE_READ_AFTER_WRITE | media | Un campo @state se lee (get() / getAndRequireEquals()) después de que se completa un set(...) sobre el mismo campo, en el mismo método. set() registra el cambio en el AccountUpdate pero no escribe a través de get(), por lo que la lectura aún observa el valor de antes de la escritura y cualquier aritmética construida sobre él está silenciosamente desviada por esa escritura. Reportado por Veridise como V-O1J-VUL-030. Mantén el nuevo valor en una variable local en lugar de volver a leer el estado. Se mantiene silenciosa cuando la lectura está anidada dentro de los propios argumentos de la escritura (el modismo de lectura-modificación-escritura this.x.set(this.x.getAndRequireEquals().add(1)), que es correcto), y cuando la escritura y la lectura se sitúan en ramas de JS mutuamente excluyentes. Con alcance a un solo método — el caso de caché entre métodos que Veridise también describe necesita conocimiento del grafo de llamadas que esta regla no tiene. |
index.assert_max_bit_size::<8>(); let i = index as u32;assert_eqNOIR_UNASSERTED_BOOL | alta / media | Una comparación cuyo resultado bool se descarta. Análogo de O1JS_UNASSERTED_BOOL de o1js. |
NOIR_CONDITIONAL_ASSERT | media | Un assert dentro de if <flag> { ... } donde <flag> es un bool sin más controlado por el probador o una variable local derivada de valores controlados por el probador. Una restricción dentro de un condicional solo se aplica cuando la condición es verdadera, por lo que una rama elegida por el probador puede omitir la verificación. Las comparaciones en línea (if x != 0) se dejan intactas por precisión; asignar la guarda a una variable local (let gate = x != 0; if gate) se reporta a menos que gate esté a su vez aseverada. |
NOIR_CONDITIONAL_CONSTRAIN | media | Una llamada a constrain_* / confirm_* / verify_* solo bajo un if controlado por el probador, mientras una pista unsafe aún alcanza la salida. |
NOIR_UNUSED_CHECK_RESULT | alta / media | Un resultado de check_* / confirm_* / verify_* / constrain_* se descarta (llamada sin más) o se asigna y nunca se asevera — la verificación no vincula el circuito. |
NOIR_VACUOUS_CONSTRAINT | alta / media | Una restricción que se satisface por construcción: una autocomparación (assert(x == x), assert_eq(x, x), x >= x) o una condición constante (assert(true)). No añade ninguna restricción, pero la línea se lee como una verificación — lo que la hace más peligrosa que una restricción ausente, porque la revisión se detiene ahí. ALTA para una autocomparación (casi siempre un error tipográfico por una verificación real: assert(computed == expected) mal escrito como assert(expected == expected)); MEDIA para una constante, que más a menudo es un marcador de posición. x != x no se marca — eso es insatisfacible, un bug de vivacidad más que un agujero de solidez silencioso. |
NOIR_UNSAFE_MISSING_SAFETY | baja | Un bloque unsafe { ... } sin un comentario // Safety: adyacente. Informativo; no hace fallar CI con el --fail-on high por defecto. |