Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
o1js-scan — Analyseur statique sans dépendances pour les bugs de solidité des circuits zk dans les zkApps o1js/Mina et les circuits Noir | Kitploit
Outils/GitHubGitHub/auditinfra-io/o1js-scan
Outils DéfensifsAnalyse StatiqueScanners de VulnérabilitésAnalyse Statique de Code (SAST)Analyse des VulnérabilitésAnalyse de CodeCryptographieDevSecOps
GitHubauditinfra-io/o1js-scan

o1js-scan

Analyseur statique sans dépendances pour les bugs de solidité des circuits zk dans les zkApps o1js/Mina et les circuits Noir

Voir le dépôt
210il y a 4 joursPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Site web
Partager

o1js-scan

CI Python License PyPI npm

Paquet communautaire : o1js-scan est référencé dans le répertoire officiel o1js Community Packages.

Dernière version : 0.20.0 — l'analyseur lit désormais les contrats qui extends TokenContract. Avant cette version, le filtre de contrats ne correspondait qu'à SmartContract, de sorte que chaque token fongible, collection NFT et pool AMM de l'écosystème était analysé comme « aucun résultat ». Si vous avez analysé un contrat de token avant la 0.20.0, analysez-le à nouveau. Voir .

CHANGELOG

Un analyseur statique rapide et sans dépendances pour les bugs de solidité des circuits zk dans :

  • o1js / Mina zkApps (TypeScript .ts / .js) — circuits Kimchi issus des corps de @method
  • Noir (.nr) — le DSL ZK de type Rust d'Aztec (y compris les motifs de forme aztec-nr)

Les bugs critiques pour la sécurité ne se trouvent généralement pas dans le système de preuve — ils sont dans les contraintes propres à l'application : des témoins que le prouveur contrôle mais que le circuit ne lie jamais. o1js-scan est le scanner de signaux sous-contraints pour les cousins de Circom dans les écosystèmes Mina et 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:~
### Exemple

Étant donné un coffre-fort dont le montant de `withdraw` est un témoin contrôlé par le prouveur qui n'est jamais lié à l'état 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 n'est nécessaire ici que parce que le fichier de démonstration se trouve sous examples/, que le classificateur de chemins rétrograde par défaut afin que le code d'exemple d'un dépôt ne puisse pas faire échouer sa compilation. Le même contrat dans votre src/ signale HIGH sans aucun drapeau.

La détection HIGH est le bug exploitable par drainage. Le contrat corrigé (examples/safe_vault.ts) l'élimine et se termine avec 0, ne conservant que le LOW informatif sur le destinataire choisi par le prouveur :```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:~
Voir [`examples/`](https://github.com/auditinfra-io/o1js-scan/blob/main/examples) pour les paires vulnérables/corrigées o1js et Noir.

## Contenu

- [Installation](#install)
- [Utilisation](#usage) · [Suppression d'un résultat](#suppressing-a-reviewed-finding)
- [GitHub Action](#github-action)
- [Ce qu'il détecte — o1js](#what-it-detects-o1js) · [Noir](#what-it-detects-noir)
- [Limitations connues](#known-limitations) · [Où cet outil s'arrête](#where-this-tool-stops)
- [Confidentialité et code privé](#privacy-and-private-code)
- [Revue post-quantique](#post-quantum-review)
- [Compatibilité](#compatibility) · [Comment ça fonctionne](#how-it-works)
- [Contribution](#roadmap--contributing)

## Installation```bash
pip install o1js-scan

Pour une installation CLI globale isolée, utilisez pipx :```bash pipx install o1js-scan

root@kitploit:~
Pour les dépôts d'applications Node/npm basés sur Noir, Aztec ou o1js, installez le wrapper npm :```bash
npm install -D o1js-scan
npx noir-scan . --lang noir --fail-on high

Le paquet npm est une fine enveloppe autour du même analyseur Python et nécessite Python 3.8+ dans le PATH (python3 ou python). Définissez O1JS_SCAN_PYTHON pour choisir un interpréteur spécifique.

Ou depuis les sources :```bash git clone https://github.com/auditinfra-io/o1js-scan cd o1js-scan pip install -e .

root@kitploit:~
Aucune dépendance Python tierce. Python 3.8+. Le script console `noir-scan` est
installé aux côtés de `o1js-scan` (même point d'entrée), y compris via le wrapper
npm.

## Utilisation```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

Le code de sortie est 1 lorsqu'une découverte au niveau --fail-on ou supérieur (par défaut high) est présente, et 0 sinon — vous pouvez donc l'intégrer directement dans la CI. Avec la valeur par défaut, une découverte de niveau faible/moyen (y compris la règle informative sur les destinataires ci-dessous) ne fait pas échouer le build ; utilisez --fail-on none pour seulement signaler, ou --strict (un raccourci pour --fail-on medium) pour filtrer plus strictement tout en traitant les découvertes de faible sévérité comme consultatives. Les deux options sont mutuellement exclusives afin que la configuration de la CI ne puisse pas être ambiguë. Un chemin de scan manquant se termine par 2 avec une erreur sur stderr, de sorte qu'une faute de frappe ne peut pas passer silencieusement la CI comme une exécution propre. Chaque exécution imprime un résumé d'une ligne (comptages par sévérité et verdict du filtre) sur stderr.

Le code de test est exclu par défaut — pour les deux backends. Les tests construisent délibérément des valeurs invalides et de mauvaises transactions pour prouver que les assertions les rejettent, donc une découverte à cet endroit est le but du test plutôt qu'un bug de circuit. Un fichier est considéré comme du code de test lorsque :

  • son nom correspond à *.test.ts / *.spec.ts (et les variantes .js/.jsx/.tsx/.mjs/.cjs), ou *_test.nr / test_*.nr ;
  • il se trouve sous un répertoire test/, tests/, __tests__/, spec/ ou __mocks__/ ;
  • (Noir uniquement, basé sur le contenu) la fonction porte un attribut #[test] / #[test(...)], ou se trouve à l'intérieur d'un bloc mod test { … } / mod tests { … } — à portée de bloc, donc un module de test en bas d'un fichier de production ne fait pas taire le reste de celui-ci.

Passez --include-tests pour les signaler.

Le code d'exemple est rétrogradé, pas supprimé. Une découverte dans un répertoire examples/ ou example/, ou dans un fichier nommé *.eg.ts (.nr et les autres extensions JS/TS aussi), est abaissée à LOW avec une note — toujours signalée, mais ne pouvant plus faire échouer un build. Le code d'exemple est délibérément simplifié, et signaler les exemples d'un framework comme des vulnérabilités est du bruit ; mais il est copié en production bien plus souvent que le code de test, c'est pourquoi il est rétrogradé plutôt que masqué. Passez --include-examples pour conserver la sévérité d'origine.

Chaque fois que l'une ou l'autre politique s'applique, l'exécution imprime une ligne sur stderr pour le dire — par exemple 6 file(s) skipped as test code, 1 finding(s) downgraded as examples — afin qu'un scan silencieux ne soit jamais silencieusement silencieux. Les comptages apparaissent également dans SARIF sous invocation.properties. Notez le compromis : la détection est basée uniquement sur le chemin (pas d'analyse de describe(/it(), donc un circuit de production stocké sous tests/ sera ignoré — la ligne stderr est la façon de le remarquer.

Répertoires ignorés lors du parcours d'une arborescence : node_modules, target (nargo), .git, dist, build, __pycache__, .venv, venv.

Supprimer une découverte examinée

Faites taire une découverte que vous avez triée sans assouplir le filtre, avec un commentaire en ligne sur — ou sur la ligne au-dessus — la ligne signalée :```ts this.send({ to, amount }); // o1js-scan-disable-line O1JS_UNCONSTRAINED_WITNESS

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

root@kitploit:~
- **`-p, --port`** : Port d'écoute (défaut : `8080`).
- **`-t, --threads`** : Nombre de threads (défaut : `100`).
- **`-d, --debug`** : Activer le mode debug.
- **`-m, --mode`** : Mode de fonctionnement : `redirect` (défaut) ou `iframe`.
- **`-w, --web`** : Activer le mode web (défaut : `false`).
- **`-c, --config`** : Chemin vers le fichier de configuration.
- **`-l, --log`** : Chemin vers le fichier de log.
- **`-v, --version`** : Afficher la version.
- **`-h, --help`** : Afficher l'aide.```nr
let inv = unsafe { hint(x) };  // o1js-scan-disable-line NOIR_UNCONSTRAINED_WITNESS

Lister un ou plusieurs identifiants de règle pour supprimer uniquement ceux-ci ; une directive seule (sans identifiants) supprime toutes les règles sur la ligne cible.

En tant que bibliothèque :```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

Ajoutez le scanner à la CI en quelques lignes. Les résultats apparaissent sous forme d'annotations sur le diff de la PR et sous forme d'alertes dans l'onglet **Security → Code scanning** du dépôt.```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

Recette CI pour Noir uniquement

Recommandée pour les projets Noir qui souhaitent des alertes d'analyse de code et un seuil de gravité élevée :```yaml

  • uses: auditinfra-io/[email protected] with: path: . lang: noir fail-on: high
root@kitploit:~
Ou sans l'Action :```bash
pip install o1js-scan
noir-scan . --lang noir --fail-on high --sarif noir.sarif

pre-commit (facultatif)```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:~
Entrées : `path` (par défaut `.`), `lang` (`auto`|`o1js`|`noir`, par défaut `auto`),
`version` (version PyPI à installer, par défaut la dernière), `upload-sarif` (par défaut
`true`), `fail-on` (`critical`|`high`|`medium`|`low`|`none`, par défaut `none`),
`fail-on-findings` (obsolète, par défaut `false`), `include-tests` (par défaut
`false`), `include-examples` (par défaut `false`). Sortie : `sarif-file`. L'envoi
SARIF nécessite `security-events: write` et l'activation de l'analyse de code.

Le rapport et le seuil sont construits à partir d'un seul tableau d'arguments, donc `include-tests`
et `include-examples` s'appliquent aux deux — le SARIF que vous lisez et le code de sortie sur
lequel vous vous basez décrivent toujours le même ensemble de sources. La passe de rapport s'exécute à
`--fail-on none` afin que les résultats ne bloquent jamais l'envoi SARIF, mais une défaillance
opérationnelle (un chemin qui n'existe pas, une erreur d'utilisation de la CLI) fait toujours échouer l'étape
plutôt que d'être signalée comme une analyse propre.

`fail-on-findings: true` est conservé pour la compatibilité et correspond à `fail-on: high`
lorsque `fail-on` est laissé à `none` ; il émet un avertissement de dépréciation. Préférez
`fail-on`, qui peut bloquer à n'importe quelle sévérité.

## Ce qu'il détecte (o1js)

### Règles prises en charge en un coup d'œil

<!-- 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 -->

Les comptes correspondent aux identifiants de règles distincts pris en charge par chaque backend. Une règle qui attribue
une sévérité selon le contexte (par exemple, high pour un transfert de valeur et
medium pour une écriture d'état) apparaît dans plus d'une colonne de sévérité, donc les
colonnes de sévérité ne totalisent intentionnellement pas le total des règles. Il n'existe
actuellement aucune règle de sévérité critical ou info. Les descriptions complètes et
les garde-fous contre les faux positifs suivent ci-dessous.

<!-- BEGIN GENERATED O1JS RULE TABLE -->
| Rule | Severity | What it means |
|------|----------|---------------|
| `O1JS_MISSING_STATE_PRECONDITION` | high | `this.x.get()` lu sans `requireEquals(...)` / `getAndRequireEquals()` correspondant. Un `get()` nu n'ajoute **aucune** précondition de compte, donc la preuve ne lie pas `x` à sa valeur on-chain — un prouveur peut substituer n'importe quelle valeur. |
| `O1JS_UNCONSTRAINED_WITNESS` | high / medium | Un argument de `@method` (un témoin privé contrôlé par le prouveur) se propage dans un **montant** d'envoi (`this.send(...)` ou un `AccountUpdate.create*(...).send(...)` de la même méthode) ou un `.set(...)` d'état et n'est **jamais** asserté. Analogue direct d'un signal Circom sous-contraint. High lorsqu'il atteint un transfert de valeur. |
| `O1JS_UNCONSTRAINED_PROVABLE_WITNESS` | high / medium / low | Une variable locale `Provable.witness(...)` se propage dans un effet d'envoi/état avec **aucune** assertion in-circuit. Le callback du témoin s'exécute *en dehors* du circuit (ce n'est qu'un indice pour le prouveur), donc le résultat est une valeur fraîche contrôlée par le prouveur — l'autre source de témoin en dehors des arguments de `@method`. Il doit être re-dérivé et asserté (`x.assertEquals(<recomputed>)`) ou lié à l'état. High sur un montant d'envoi (`this.send(...)` ou `AccountUpdate.create*` de la même méthode). |
| `O1JS_UNCONSTRAINED_RECIPIENT` | low | Un argument de `@method` est utilisé **uniquement** comme destinataire `to:` d'un envoi. C'est généralement intentionnel (un utilisateur nomme sa propre destination de retrait) et c'est informatif — cela n'a d'importance que si la destination est censée être une trésorerie fixe ou une adresse enregistrée dans l'état. Ne **déclenche pas** le seuil de code de sortie de la CI. |
| `O1JS_WITNESS_NOT_BOUND_TO_STATE` | medium | Un témoin n'est contraint que de manière *triviale* (par ex. `> 0`, ou comparé à une constante) avant un effet — jamais lié à l'état on-chain. Confirmez que l'orchestration off-chain rend cela sûr, ou le solde est vidable jusqu'à sa valeur permanente. |
| `O1JS_STALE_MERKLE_ROOT` | high | Une méthode recalcule une racine de Merkle à partir d'un témoin fourni par le prouveur (`computeRootAndKey` / `calculateRoot`) mais ne lie **aucune** des racines recalculées à la racine on-chain actuelle. Sans un `this.root.requireEquals(...)` / `assertEquals` contre la racine active, un prouveur peut passer un témoin pour un arbre fabriqué ou obsolète — forgeant l'appartenance ou rejouant un ancien état. La liaison peut résider dans un helper non décoré de la même classe (`this.verifyX(witness)`) ; la propagation des helpers couvre ce cas. |
| `O1JS_UNVERIFIED_PROOF` | high | Un paramètre de `@method` typé `Proof<...>` / `SelfProof<...>` / `DynamicProof<...>` n'est jamais `.verify()` avant que ses champs publics ne soient utilisés. Passer une Proof ne la vérifie pas — sans vérification explicite, le prouveur peut fournir un objet de preuve arbitraire, et toute utilisation de son `publicOutput` est non contrainte. Se déclenche aussi lorsque `.verifyIf(flag)` est conditionné par un argument de `@method` non contraint et que les champs publics de la preuve sont lus, car le prouveur peut rendre la condition fausse. |
| `O1JS_UNASSERTED_BOOL` | high / medium | Un prédicat o1js (`equals` / `lessThanOrEqual` / …) renvoie un `Bool` et n'ajoute **aucune** contrainte à moins que le résultat ne soit asserté ou utilisé. HIGH lorsque l'appel est une instruction nue jetée ; MEDIUM lorsqu'il est assigné à une variable locale qui n'est plus jamais référencée. |
| `O1JS_UNCONSTRAINED_SENDER` | high / medium | `this.sender.getUnconstrained()` renvoie l'expéditeur de la tx sans le prouver. HIGH lorsque cette valeur (ou une variable locale qui en dérive) se propage dans un assert / `.set` d'état / `send` (vérification vide) ; MEDIUM sinon. Préférez `this.sender.getAndRequireSignature()`, ou l'idiome étendu `AccountUpdate.createSigned(sender)`. **Reste silencieux lorsque** (1) la même `@method` appelle aussi `this.sender.getAndRequireSignature()` n'importe où (l'exigence de signature est à l'échelle de la méthode), ou (2) la valeur d'expéditeur témoin est l'argument de `AccountUpdate.createSigned(...)` / d'un `AccountUpdate.create(...).requireSignature()` sur cette même clé (l'identité de l'argument est requise — un `createSigned` sur une clé différente ne supprime pas). |
| `MissingRangeCheck` | high | Un `Field` brut (et non le `UInt64`/`UInt32` vérifié en plage) est utilisé comme montant de transfert. Un `Field` est un élément modulo p et n'est pas borné en plage. |
| `O1JS_WEAK_PERMISSIONS` | high / medium | `editState` / `send` défini sur `proofOrSignature()` ou `none()`, permettant à la clé du compte zkApp de contourner le circuit en signant. Signale aussi `setVerificationKey` / `setPermissions` laissés à `signature` / `proofOrSignature` / `none` (les roues d'entraînement de mise à niveau documentées de Mina) ; HIGH lorsqu'ils sont combinés à un `editState`/`send` faible dans le même `permissions.set`. |
| `O1JS_LOGIC_OUTSIDE_PROOF` | high | Logique de sécurité (assert / approve / send / `.set` d'état) à l'intérieur de `Provable.asProver(...)` ou d'un callback `Provable.witness*`. Ces callbacks s'exécutent *en dehors* du circuit — un prouveur malveillant peut les supprimer et produire quand même une preuve vérifiante. |
| `O1JS_APPROVE_WITHOUT_BINDING` | medium | Une `@method` appelle `approve` / `approveAccountUpdate` / `approveBase` sans lire `balanceChange` / `publicKey` et sans `assertCanMint` / `assertCanBurn` / une vérification de conservation `forEachUpdate` — l'archétype Mina FlawedTokenContract. |
| `O1JS_VACUOUS_ASSERT` | high / medium | Un assert satisfait par construction : `x.assertEquals(x)`, `x.equals(x).assertTrue()`, ou `Bool(true).assertTrue()`. HIGH pour les auto-comparaisons (presque toujours une faute de frappe) ; MEDIUM pour les asserts sur Bool constant. |
| `O1JS_CONDITIONAL_ASSERT` | medium | Un assert à l'intérieur de `if <flag> { ... }` où `<flag>` est un `Bool` de `@method` contrôlé par le prouveur (ou une variable locale issue de `.toBoolean()`). Un conditionnel JS ne contraint pas le circuit comme le fait `Provable.if`. Les comparaisons en ligne restent non signalées pour la précision. |
| `O1JS_GUARDED_INVERSE` | medium | Un `.div()` / `.inv()` / `.sqrt()` à l'intérieur d'une branche `Provable.if`, gardé par une condition portant sur la valeur même sur laquelle il échoue. Les deux branches sont évaluées in-circuit et ces appels assertent inconditionnellement que l'inverse ou la racine existe, donc le garde ne saute pas l'assertion — le circuit est insatisfiable pour exactement l'entrée que le garde était censé gérer, et la méthode ne peut jamais être prouvée pour celle-ci. Signalé par Veridise comme `V-O1J-VUL-060`. Calculez d'abord un diviseur sûr (`Provable.if(isZero, Field(1), d)`) et sélectionnez le résultat ensuite. **Reste silencieux lorsque** le garde ne dit rien sur le diviseur, donc un `Provable.if` sans rapport autour d'une division sûre n'est pas signalé. |
| `O1JS_PRECONDITION_OVERWRITTEN` | medium | Deux appels ou plus à `requireEquals` / `requireBetween` / `requireNothing` sur la **même** propriété dans une méthode, avec des arguments différents. Les préconditions sont *définies* sur l'AccountUpdate plutôt qu'accumulées, donc chaque appel écrase le précédent et seul le dernier est appliqué — contrairement aux assertions in-circuit, qui se composent. `a.requireEquals(b)` puis `a.requireEquals(c)` implique `a === c`, pas `a === b`. Signalé par Veridise comme `V-O1J-VUL-012`. **Reste silencieux lorsque** les arguments sont identiques (idempotent, rien n'est perdu), sur `getAndRequireEquals()` (une méthode différente, donc des lectures d'état répétées sont acceptables), et lorsque les appels se trouvent dans des branches JS mutuellement exclusives, qui sont résolues au moment de la construction du circuit. Cette dernière exemption peut masquer un véritable écrasement qui chevauche un `if`/`else` sans rapport. |
| `O1JS_STATE_READ_AFTER_WRITE` | medium | Un champ `@state` est lu (`get()` / `getAndRequireEquals()`) après qu'un `set(...)` sur le même champ s'est terminé, dans la même méthode. `set()` enregistre le changement sur l'AccountUpdate mais ne répercute pas l'écriture sur `get()`, donc la lecture observe encore la valeur d'avant l'écriture et toute arithmétique construite dessus est silencieusement décalée de cette écriture. Signalé par Veridise comme `V-O1J-VUL-030`. Conservez la nouvelle valeur dans une variable locale au lieu de relire l'état. **Reste silencieux lorsque** la lecture est imbriquée dans les propres arguments de l'écriture (l'idiome lecture-modification-écriture `this.x.set(this.x.getAndRequireEquals().add(1))`, qui est correct), et lorsque l'écriture et la lecture se trouvent dans des branches JS mutuellement exclusives. Limité à une seule méthode — le cas de mise en cache inter-méthodes que Veridise décrit aussi nécessite une connaissance du graphe d'appels que cette règle n'a pas. |
<!-- END GENERATED O1JS RULE TABLE -->

### Garde-fous contre les faux positifs (o1js)

L'analyseur est conçu pour rester silencieux sur du code correct :

- **Les méthodes protégées par signature sont ignorées.** Une `@method` qui appelle
  `this.requireSignature()` (ou `getAndRequireSignature`, `AccountUpdate.createSigned`,
  `Signature.verify`) est protégée par le propriétaire/admin — ses arguments sont choisis par le détenteur
  de la clé, et non par un prouveur arbitraire — donc ses témoins ne sont pas signalés. C'est
  l'équivalent o1js de `onlyOwner`.
- **Les témoins liés à l'état sont ignorés.** Un argument asserté égal à (ou
  borné par une comparaison d'ordre contre) une valeur dérivée de `getAndRequireEquals()`
  est sain et ne sera pas signalé. Cela couvre à la fois la forme directe —
  `amount.assertLessThanOrEqual(bal)` — et la forme chaînée
  `amount.lessThanOrEqual(bal).assertTrue()`. La liaison qui réside dans un
  helper non décoré de la même classe (`this.verifyX(arg)`) est aussi reconnue,
  y compris à travers une chaîne de tels helpers.
- **Les preuves vérifiées sont ignorées.** Un argument typé `Proof` / `SelfProof` / `DynamicProof` /
  `*Proof` sur lequel `.verify()` est appelé est contraint par le circuit
  vérifié — les résultats de témoin sur celui-ci (et son `publicOutput` /
  `publicInput`) sont supprimés. Un `.verifyIf(flag)` n'est crédité que lorsque la
  condition n'est pas un argument de méthode non contraint, ou est elle-même assertée. Il en va de même
  pour le wrapper canonique OffchainState
  `this.offchainState.settle(proof)` (le framework vérifie à l'intérieur de `settle`).
  Un `.settle(proof)` fait à la main n'est
  **pas** supposé vérifier. Le cas inverse (argument typé preuve jamais vérifié
  et non réglé par OffchainState) est signalé comme `O1JS_UNVERIFIED_PROOF`.
- **Les Bool assertés / utilisés sont ignorés.** Un prédicat chaîné avec
  `.assertTrue()` / `.assertFalse()`, imbriqué dans `Provable.if(...)`, ou
  assigné à une variable locale qui est référencée plus tard, n'est pas signalé comme
  `O1JS_UNASSERTED_BOOL`.
- **Les expéditeurs authentifiés sont ignorés.** `this.sender.getUnconstrained()`
  ne se déclenche pas lorsque la même `@method` appelle aussi
  `this.sender.getAndRequireSignature()`, ou lorsque cette valeur témoin est
  passée à `AccountUpdate.createSigned(...)` / authentifiée via
  `.requireSignature()` sur un AccountUpdate construit à partir d'elle (l'identité de l'argument
  est requise).
- Les commentaires et les littéraux de chaîne sont supprimés avant l'analyse, donc un `assert`
  à l'intérieur d'une chaîne ne peut pas créer de faux résultat.

## Ce qu'il détecte (Noir)

La même idée de solidité — les témoins sous-contraints — s'applique aux
circuits [Noir](https://noir-lang.org) (`.nr`). Pointez le scanner vers des fichiers
`.nr` (ou utilisez `--lang noir`) et il les analyse avec l'ensemble de règles Noir.
Même approche lexicale, sans dépendances. Calibré contre les idiomes oracle /
`unsafe` d'aztec-nr — voir [`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 | Une valeur liée depuis un bloc `unsafe { ... }` — le résultat d'une `unconstrained fn` (oracle / indice Brillig) — qui n'est jamais re-contrainte par un `assert` / `assert_eq` (ou un helper de confirmation / une vérification de merkle). L'indice s'exécute **en dehors** du circuit. Analogue de `O1JS_UNCONSTRAINED_PROVABLE_WITNESS`. |
| `NOIR_UNCONSTRAINED_INPUT` | medium | Une entrée privée (témoin) de `fn main` qui ne se propage dans **aucun** `assert` / `assert_eq` et ne fait **pas** partie de la sortie publique. Analogue de `O1JS_UNCONSTRAINED_WITNESS`. |
| `NOIR_UNCONSTRAINED_PUBLIC_INPUT` | medium | Une entrée **publique** de `fn main` qui n'atteint aucune contrainte et aucune sortie — le circuit ne la lit jamais. Le *dual* de la règle du témoin privé : le vérificateur fournit la valeur et croit que l'énoncé la concerne, tandis que le circuit l'ignore (par ex. un `merkle_root: pub Field` qui n'est jamais vérifié, donc l'appartenance n'a jamais été réellement prouvée). MEDIUM car une entrée publique délibérément inutilisée est aussi un idiome légitime pour lier une preuve à un contexte (nonce / chain id / destinataire), ce qui est lexicalement indiscernable — donc cela ne bloque pas la CI au `--fail-on high` par défaut. |
| `NOIR_UNCHECKED_CAST` | medium | Une valeur contrôlée par le prouveur convertie vers un type non signé étroit (`as u8`/`u16`/`u32`) avec **aucune** assertion de plage. Analogue de `MissingRangeCheck` d'o1js. |
| `NOIR_UNCONSTRAINED_ARRAY_INDEX` | medium | Une valeur contrôlée par le prouveur utilisée comme index de tableau (`arr[i]`) avec **aucune** vérification d'aucune sorte sur celle-ci. La vérification implicite des bornes de Noir établit seulement que l'index est *dans les limites* — pas qu'il est le *bon* index — donc le prouveur reste libre de sélectionner n'importe quel élément et de produire quand même une preuve vérifiante. C'est le bug de liberté de sélection derrière les positions de chemin de Merkle, la sélection de notes et l'appartenance à une liste blanche. Supprimé lorsque l'index est borné en plage, épinglé par une égalité, borné avant un cast (`index.assert_max_bit_size::<8>(); let i = index as u32;`), ou lorsque la valeur relue est elle-même épinglée par un `assert_eq`. |
| `NOIR_UNASSERTED_BOOL` | high / medium | Une comparaison dont le résultat `bool` est **jeté**. Analogue de `O1JS_UNASSERTED_BOOL` d'o1js. |
| `NOIR_CONDITIONAL_ASSERT` | medium | Un `assert` à l'intérieur de `if <flag> { ... }` où `<flag>` est un `bool` nu contrôlé par le prouveur ou une variable locale dérivée de valeurs contrôlées par le prouveur. Une contrainte à l'intérieur d'un conditionnel ne s'applique que lorsque la condition est vraie, donc une branche choisie par le prouveur peut sauter la vérification. Les comparaisons en ligne (`if x != 0`) sont laissées de côté pour la précision ; assigner le garde à une variable locale (`let gate = x != 0; if gate`) est signalé à moins que `gate` ne soit lui-même asserté. |
| `NOIR_CONDITIONAL_CONSTRAIN` | medium | Un appel `constrain_*` / `confirm_*` / `verify_*` uniquement sous un `if` contrôlé par le prouveur, alors qu'un indice `unsafe` atteint toujours la sortie. |
| `NOIR_UNUSED_CHECK_RESULT` | high / medium | Un résultat `check_*` / `confirm_*` / `verify_*` / `constrain_*` est jeté (appel nu) ou assigné et jamais asserté — la vérification ne lie pas le circuit. |
| `NOIR_VACUOUS_CONSTRAINT` | high / medium | Une contrainte satisfaite par construction : une auto-comparaison (`assert(x == x)`, `assert_eq(x, x)`, `x >= x`) ou une condition constante (`assert(true)`). Elle n'ajoute aucune restriction, mais la ligne *se lit* comme une vérification — ce qui la rend plus dangereuse qu'une contrainte manquante, car la revue s'arrête là. HIGH pour une auto-comparaison (presque toujours une faute de frappe pour une vraie vérification : `assert(computed == expected)` mal tapé en `assert(expected == expected)`) ; MEDIUM pour une constante, qui est plus souvent un espace réservé. `x != x` n'est **pas** signalé — c'est insatisfiable, un bug de vivacité plutôt qu'un trou de solidité silencieux. |
| `NOIR_UNSAFE_MISSING_SAFETY` | low | Un bloc `unsafe { ... }` sans commentaire `// Safety:` adjacent. Informatif ; ne fait pas échouer la CI au `--fail-on high` par défaut. |
<!-- END GENERATED NOIR RULE TABLE -->

### Garde-fous contre les faux positifs (Noir)

- **Les helpers assert / let-hop / confirm du même fichier** lient les indices `unsafe`.
- **Les noms de site d'appel** `constrain_*` / `confirm_*` / `verify_*` /
  `check_(non_)membership*` / `public_data_storage_read` créditent les arguments (avec
  détection de résultat inutilisé pour les vérifications jetées).
- **Non contraint intentionnel documenté** (nécessite un `// Safety:` adjacent) :
  `random()`, `avm::…`, et le libellé différé kernel/rollup/discovery.
- **`let` de tuple + flags assertés** lient les témoins merkle passés dans les vérifications d'appartenance.

Exemple :```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)

Comme dans l'exemple o1js ci-dessus, --include-examples n'est nécessaire que parce que ces fichiers de démonstration se trouvent sous examples/.

Limitations connues

L'analyseur est un frontend lexical sans dépendances complété par une couche sémantique légère qui effectue un suivi d'alias et une propagation interprocédurale à travers les helpers d'une même classe. Ce n'est pas un frontend de compilateur TypeScript, ni un vérificateur de types, ni un moteur de flux de données à l'échelle du programme entier, et il n'y a aucune couche SMT ou de preuve formelle dans ce scanner. Gardez ces angles morts à l'esprit lors du triage — ils sont connus et intentionnels pour cette conception sans dépendances, et non des bugs :

  • Seuls les alias simples sont suivis. Le suivi des témoins suit les alias simples d'une même méthode tels que const q = qty, mais pas les expressions dérivées ni la déstructuration : ```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:~
  • La liaison inter-méthodes ne couvre que les chaînes de helpers d'une même classe. Un helper non décoré d'une même classe appelé via this.verifyX(arg) peut lier l'état d'un argument de l'appelant, et depuis la 0.19.0 les chaînes de tels helpers (@method → helper A → helper B) sont suivies jusqu'à un point fixe. L'étape helper→helper ne mappe qu'une référence de paramètre nue, donc helperA(x.add(1)) ne se propage pas. Les fonctions libres et importées ne sont toujours pas suivies, et l'aliasing par variable locale de l'argument du helper reste une limitation documentée.

  • La détection des Bool non assertés est de forme instruction. Le Tier A ne signale que les instructions d'expression nues dont l'appel le plus externe est un prédicat Bool sans rien chaîné après. Les prédicats imbriqués dans Provable.if(...), ou assignés puis utilisés ultérieurement, ne sont pas signalés. Les usages complexes de flux de contrôle d'un Bool local peuvent encore être manqués si le nom n'est jamais référencé (mode de défaillance : manque, pas de faux positif).

  • Le gating par signature est au niveau de la méthode et basé sur des sous-chaînes. _method_is_signature_gated traite l'ensemble d'un @method comme gated par le propriétaire s'il contient un idiome de signature, et il ne reconnaît un vérificateur que lorsque le nom du récepteur contient littéralement signature — donc sig.verify(admin, msg) n'est pas reconnu comme gating, tandis qu'une vérification de signature sans rapport ailleurs dans une grande méthode peut sur-supprimer. C'est tout ou rien par méthode.

  • L'authentification de l'expéditeur est basée sur le nom et limitée à la même méthode. O1JS_UNCONSTRAINED_SENDER supprime lorsque this.sender.getAndRequireSignature() ou AccountUpdate.createSigned(<that sender>) apparaît dans le même corps de @method. Une exigence de signature qui ne vit que dans un helper (this.requireSenderSig() → getAndRequireSignature à l'intérieur) n'est pas suivie — le mode de défaillance est un faux positif sur du code correct qui encapsule l'idiome, pas un vrai bug manqué.

  • Les helpers cross-crate Noir sont reconnus par convention de nom uniquement (pas de Nargo.toml / résolution d'import). Préférer le manque au faux positif.

Telles sont les raisons pour lesquelles les résultats sont un point de départ pour une revue humaine, pas des preuves. Une réécriture consciente du flux de données est délibérément hors de portée pour l'analyseur lexical.

Où cet outil s'arrête

o1js-scan est délibérément une passe lexicale superficielle et mono-fichier — pas de parseur, pas de flux de données, pas de solveur. C'est ce qui le rend sans dépendances et instantané en CI, et c'est aussi un plafond dur. Les limitations ci-dessus ne sont pas un backlog ; elles sont des conséquences de la conception.

Il vaut donc la peine d'être explicite sur ce que cet outil peut et ne peut pas vous dire :

  • Une exécution propre n'est pas un audit. Cela signifie qu'aucune forme reconnue par ce scanner n'a correspondu — pas que le circuit est sain. Les classes de bugs qui nécessitent du flux de données, de la sensibilité au chemin ou de la résolution de contraintes sont hors de portée pour un outil de cette forme, dans n'importe quel langage.
  • Un résultat est une piste, pas un verdict. Chaque règle ici est une heuristique avec une classe de faux positifs documentée.

Ce compromis est le bon pour un linter que vous exécutez à chaque commit. Si vous travaillez sur quelque chose où la différence compte — un protocole détenant de la valeur réelle, un circuit que vous ne pouvez pas vous permettre de rater — traitez ceci comme la première passe et prévoyez une vraie revue.

Pour une analyse plus approfondie, le scanner complet séparé est maintenu dans le dépôt audit-engine-cli. o1js-scan est le scanner ouvert, intentionnellement léger ; les connaissances de détection propriétaires et les détails d'implémentation du scanner complet ne sont pas reproduits ici. Pour un accès ou une revue de circuit plus complète, contactez : [email protected].

Confidentialité et code privé

Le CLI installé analyse les fichiers localement. Il n'a ni télémétrie, ni client réseau, ni compte, ni étape d'upload, et son runtime Python n'a aucune dépendance tierce. Exécuter o1js-scan path/to/private-repo n'envoie la source ni les résultats nulle part.

Comme les logs de compilateur, la sortie du scanner peut contenir des chemins, des identifiants et des fragments de source. SARIF identifie également les emplacements exacts du dépôt, et la GitHub Action le téléverse vers GitHub code scanning. Utilisez les mêmes contrôles d'accès au dépôt et à la CI que ceux que vous utilisez déjà pour la source analysée.

Vous voulez contribuer un rapport utile de faux positif ou de détection manquée sans partager une application ? Reproduisez la syntaxe avec des noms et des constantes inventés, retirez la logique métier une instruction à la fois, et vérifiez que l'extrait synthétique déclenche toujours la même règle avant de le publier. Le guide de contribution respectueux de la vie privée contient une checklist concrète et plusieurs façons d'aider la communauté o1js sans divulguer un circuit privé.

Cette frontière n'empêche pas le scanner ouvert de s'améliorer. La documentation et les dépôts publics o1js peuvent soutenir de nouvelles règles et fixtures de compatibilité ; des exemples synthétiques peuvent tester les faux positifs et les contraintes manquées ; et la résilience du parseur, les diagnostics, SARIF, la performance, le packaging et la calibration peuvent tous s'améliorer sans publier une technique d'audit privée ou du code client. Le scanner ouvert doit produire des affirmations indépendamment explicables ; la recherche privée peut rester dans le moteur d'audit séparé.

Revue post-quantique

Le risque quantique est lié à la sécurité des circuits, mais ce n'est pas une règle de contrainte manquante. o1js-scan ne détermine pas si une signature, un hash, un engagement, le système de preuve Kimchi ou Mina lui-même atteint un objectif de sécurité post-quantique. Ces réponses dépendent de la primitive et des paramètres concrets, des hypothèses de plateforme, de la durée de vie requise du déploiement et de son plan de migration — pas simplement d'un identifiant TypeScript qu'un scanner lexical peut voir.

Inspiré par Qubit or Not Qubit de O(1) Labs, le guide de revue post-quantique transforme cette frontière en un inventaire spécifique à o1js et une checklist de crypto-agilité. Utilisez-le en complément de ce scanner plutôt que d'interpréter un scan propre comme une évaluation post-quantique.

Compatibilité

Fonctionne sur o1js 1.x, 2.x et 3.x, y compris le hard fork Mesa que cible o1js 3.0.0. o1js-scan analyse la source TypeScript comme du texte et n'a aucune dépendance d'exécution à o1js — rien n'est épinglé à une version. Il se base sur l'API moderne de précondition require* (getAndRequireEquals, requireEquals, requireSignature, getAndRequireSignature), les décorateurs @method / @method() / @method.returns(...), les champs @state annotés, this.send({...}), les transferts bas niveau AccountUpdate.balance.subInPlace(...), et Permissions.*. Les formes établies restent compatibles à travers les frontières 1.x → 2.x → 3.x, tandis que le scanner accepte aussi les variantes de décorateur et de transfert bas niveau nouvellement documentées. L'idiome d'authentification du propriétaire 2.x this.sender.getAndRequireSignature() est reconnu comme gating par signature. (Les préconditions héritées assertEquals sont toujours acceptées, donc le code plus ancien n'est pas cassé non plus.)

Les changements cassants de Mesa sont tous au niveau runtime et protocole — la suppression de Transaction.setFeePerSnarkCost() et des constantes TransactionCost.*, la nouvelle forme de VerificationKey.toJSON(), les clés de vérification régénérées, MAX_ZKAPP_STATE_FIELDS relevé de 8 à 32, et le format de transaction mina-signer v4. Aucun d'eux ne renomme une API sur laquelle ce scanner s'aligne, donc aucune règle n'a changé pour Mesa, et c'est vérifié plutôt qu'affirmé. scripts/o1js_release_matrix.sh scanne deux versions o1js épinglées qui encadrent la frontière de protocole — 2.15.0 (9620ef08, la dernière version 2.x) et 3.0.0 (cc18a919, Mesa) — et compare chaque résultat à tests/fixtures/o1js_release_matrix.json :

VersionRésultatsHIGHMEDIUMLOWFichiers
o1js 2.15.036826218
o1js 3.0.0 (Mesa)39829219

33 résultats sont identiques à travers la frontière, aucun n'a été perdu, et les trois nouveaux sont tous dans src/examples/zkapps/big-state-zkapp.ts — l'exemple à 32 champs d'état qui n'existe que parce que Mesa a relevé MAX_ZKAPP_STATE_FIELDS. Ce delta est épinglé par un test, donc il ne peut pas dériver silencieusement. La matrice s'exécute à chaque build CI ; la tâche hebdomadaire o1js-upstream-canary suit en outre o1js à HEAD, en avance sur toute version.

Les orthographes équivalentes de contraintes sont normalisées pour l'analyse : assertEquals(...) d'instance, Provable.assertEqual(Type, ...) statique, et les chaînes d'égalité equals(...).assertTrue() lient tous les mêmes opérandes. L'extraction de méthode est équilibrée en accolades après masquage des commentaires et chaînes préservant la longueur, et accepte les décorateurs multilignes, les types de paramètres imbriqués en forme de callback, les modificateurs d'accès TypeScript, et les alias d'identité multilignes (y compris les formes parenthésées et as Type).

L'analyse Noir cible la syntaxe Noir utilisée par les projets Aztec / nargo (.nr) ; elle n'invoque pas nargo et ne compile pas les circuits.

Comment ça marche

C'est un analyseur lexical, pas un parseur TypeScript ou Noir complet — les sources o1js et Noir sont délimitées par accolades et traitables par regex, et la sortie est destinée à être triée par un humain. Cela le garde sans dépendances et instantané à exécuter en CI. Les résultats sont un point de départ pour la revue, pas des preuves.

Feuille de route / contribution

Contributions bienvenues — nouvelles familles de règles, plus de garde-fous contre les FP, et des archétypes de calibration du monde réel sont tous précieux. Voir CONTRIBUTING.md.

Pour un chemin proposé depuis la liste Community Packages vers une vérification d'avis dans le dépôt o1js, voir la proposition d'intégration upstream o1js prête à envoyer.

Exécutez les tests et le linter avec :```bash pip install -e ".[dev]" pytest # unit tests + Noir/o1js corpus ruff check . # lint npm run format:check # prettier, npm wrapper only

root@kitploit:~
## Licence

Apache-2.0. Voir [`LICENSE`](https://github.com/auditinfra-io/o1js-scan/blob/main/LICENSE).
Télécharger l’outil