Skip to main content

Static analysis tool for Odoo addons: SQL injection, N+1 queries, ORM anti-patterns, manifest dependencies, and more.

Project description

odoo-review

Static analysis tool for Odoo addons. Combines Bandit security scanning with Odoo-specific AST checks for SQL injection, N+1 queries, ORM anti-patterns, manifest dependency issues, XML/view-layer problems, and more.

Installation

pip install odoo-review
# with precise line numbers for XML findings (recommended):
pip install "odoo-review[xml]"
# or from source:
pip install -e .

XML line numbers: the XML/view checks (OR070–OR079) run with or without lxml, but lxml is needed to report the exact line of a finding (it is already standard in any Odoo environment). Without it the checks still fire, reporting line 0.

Usage

# Scan a single addon
odoo-review ./my_addon

# Scan all addons in a directory
odoo-review ./addons --all

# Verbose output (show code snippets + suggestions)
odoo-review ./my_addon --verbose

# JSON output (for CI/CD)
odoo-review ./my_addon --format json -o report.json

# Skip Bandit (faster, Odoo checks only)
odoo-review ./my_addon --no-bandit

# Force a target Odoo series (otherwise auto-detected from __manifest__.py)
odoo-review ./my_addon --odoo-version 17

# Fail CI if any HIGH or CRITICAL finding exists
odoo-review ./my_addon --fail-on HIGH

Odoo version awareness

Some rules depend on the target Odoo series. The version is resolved as:

  1. --odoo-version N if passed (explicit override), else
  2. auto-detected from the addon's __manifest__.py version key, else
  3. unknown — version-aware rules fall back to conservative defaults.

When the version is known it sharpens results, e.g.:

  • OR020sudo(True) is reported HIGH ("removed in Odoo 13") on v13+, but downgraded to MEDIUM (legacy-but-valid) on v12 and earlier.
  • OR042 — with an explicit --odoo-version, a manifest whose version prefix does not match the target series is flagged as a mismatch.

Suppressing findings

There are two ways to silence findings you have reviewed and accepted. Both are applied as a final filter, so they cover Bandit results (B###) as well as the built-in OR### rules.

Inline — # noqa

A trailing comment on the offending line:

self.env.cr.execute(query)          # noqa: OR001      # one rule
self.env.cr.execute(query)          # noqa: OR001,B608 # several rules
self.sudo()                         # noqa             # everything on this line

# noqa must be a real comment — a "# noqa" inside a string does not count. Trailing prose is fine: # noqa: OR026 — intentional, validated input.

Project-wide — .odoo-review

Drop a .odoo-review INI file at your repo root (discovered by walking up from each addon, like Bandit's .bandit):

[odoo-review]
target-version = 17                     ; sets the Odoo series (overrides manifest auto-detect)
disable  = OR025, OR044                 ; never report these rules
select   = OR001, OR002, OR026          ; if set, report ONLY these rules
severity = OR021:INFO, OR010:CRITICAL   ; remap a rule's severity
exclude  = legacy/*, scratch/*          ; glob paths to skip (relative to addon)

The same keys are also accepted in pyproject.toml (needs a TOML reader — stdlib tomllib on Python 3.11+, or the tomli backport):

[tool.odoo-review]
target-version = 17
disable  = ["OR025", "OR044"]
severity = { OR021 = "INFO" }
exclude  = ["legacy/*"]

A Muted: N finding(s) suppressed … line in the report (and a suppressed field in JSON output) shows how many findings were filtered.

Use as a pre-commit hook

In any Odoo addon repo, add to .pre-commit-config.yaml:

repos:
  - repo: https://github.com/fahrizaardhi/odoo-review
    rev: v0.2.0
    hooks:
      - id: odoo-review
        # optional: args: [--no-bandit, --fail-on, MEDIUM]

Then pre-commit install. On each commit, the changed .py/.csv files are mapped to their addon and scanned; HIGH/CRITICAL findings block the commit (tune via args).

GitHub Actions Integration

- name: Run odoo-review
  run: |
    pip install odoo-review
    odoo-review ./addons --all --format json -o review.json --fail-on HIGH

- name: Upload review report
  uses: actions/upload-artifact@v4
  with:
    name: odoo-review-report
    path: review.json

Rules

Security

Rule Severity Description
OR001 CRITICAL f-string used directly in cr.execute() — SQL injection
OR002 HIGH %-format, .format(), or string concat in cr.execute() — SQL injection
OR020 HIGH sudo(True) forces superuser context (legacy API)
OR021 LOW bare sudo() escalates privileges — confirm it is justified
OR026 CRITICAL eval() / exec() / compile() builtin usage — use safe_eval instead
OR050 HIGH new model (_name) with no ir.model.access rule — only superuser can use it

Performance (N+1)

Rule Severity Description
OR010 HIGH search() / search_count() / browse() / read() called inside a loop
OR011 MEDIUM write() / create() / unlink() / copy() inside a loop (should batch)
OR012 MEDIUM self.env['Model'] registry lookup inside a loop

ORM Best Practices

Rule Severity Description
OR022 MEDIUM Hardcoded integer ID passed to browse() — use env.ref()
OR023 LOW Model class missing _description
OR024 INFO _name + _inherit without _description
OR025 LOW _compute_* method missing @api.depends()

Manifest & Dependencies

Rule Severity Description
OR040 MEDIUM Missing required manifest keys (name, version, depends)
OR041 INFO 'base' in depends — implicit, can be removed
OR042 LOW Version not following XX.0.X.X.X convention
OR043 MEDIUM auto_install=True with installable=False
OR044 INFO Missing license key
OR045 MEDIUM Python package listed in depends (should be external_dependencies)
OR046 HIGH Module lists itself in depends (circular)
OR047 HIGH __manifest__.py missing or unparseable

Deprecations (version-aware)

Severity scales with the target series: HIGH once removed, INFO while still supported, MEDIUM when the version is unknown.

Rule Severity Description
OR060 scaled @api.multi / @api.one — removed in Odoo 13
OR061 MEDIUM explicit cr.commit() in addon code — breaks transaction handling
OR062 scaled legacy API: from openerp, osv.osv, _columns, fields.function
OR063 scaled self.pool / self.pool.get() — old API, use self.env

XML / View layer

Every *.xml file is parsed (views, QWeb templates, data, actions). The deprecation rules are version-aware; attrs/states/<tree> are only flagged on the versions where they actually changed, so older code stays quiet.

Rule Severity Description
OR070 scaled attrs="..." attribute — deprecated in Odoo 17, removed in 18 (HIGH on v18+, INFO on v17)
OR071 scaled states="..." attribute — same deprecation path as OR070
OR072 INFO <tree> renamed to <list> in Odoo 17
OR073 MEDIUM/HIGH t-raw QWeb directive — unescaped HTML / XSS; removed in 17 (use t-out)
OR074 HIGH duplicate XML record id within the addon — the second one overwrites the first
OR075 HIGH ir.actions.act_window record / <act_window/> with no res_model
OR079 HIGH malformed XML — Odoo would refuse to load it

Scan scope: __pycache__, .git, node_modules, static, migrations, and tests directories are skipped by the AST checks, the XML checks, and Bandit. Test code intentionally contains anti-patterns, so scanning it only adds noise.

Bandit (via integration)

All Bandit rules run automatically unless --no-bandit is passed. Rules OR001/OR002/OR026 are excluded from Bandit to avoid duplicates with our Odoo-aware versions.

Output Example

======================================================================
  📦 Addon: sale_attachment_mandatory
  Files  : 3 Python file(s) scanned
======================================================================

  💀 CRITICAL (1)
  ──────────────────────────────────────────────────────────
  OR001   models/sale_order.py:42
    [OR001] Potential SQL injection: f-string used as SQL query

  🔴 HIGH (2)
  ──────────────────────────────────────────────────────────
  OR010   models/sale_order.py:67
    [OR010] .search() called inside a loop — N+1 query risk.

  Summary: Critical: 1  |  High: 2  |  Medium: 3  |  Total: 6

Extending with Custom Rules

# my_checker.py
import ast
from pathlib import Path
from typing import Generator
from odoo_review.checkers import BaseChecker
from odoo_review.models import Finding, Severity, Category

class MyCustomChecker(BaseChecker):
    def check_file(self, filepath: Path, tree: ast.Module, source: str) -> Generator[Finding, None, None]:
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                # ... your logic
                yield Finding(
                    rule_id="CX001",
                    severity=Severity.MEDIUM,
                    category=Category.BEST_PRACTICE,
                    message="[CX001] Custom rule triggered",
                    filepath=str(filepath),
                    line=node.lineno,
                )

Then expose it through the odoo_review.checkers entry-point group in your own package's pyproject.toml — no need to edit odoo-review itself:

# pyproject.toml of your plugin package
[project.entry-points."odoo_review.checkers"]
my_custom = "my_package.my_checker:MyCustomChecker"

After pip install-ing your package, odoo-review auto-discovers the checker and runs it alongside the built-ins. The entry point may point to a BaseChecker subclass (instantiated automatically) or to an already-instantiated checker. A plugin that fails to import or instantiate is skipped without aborting the scan.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

odoo_review-0.3.0.tar.gz (42.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

odoo_review-0.3.0-py3-none-any.whl (40.8 kB view details)

Uploaded Python 3

File details

Details for the file odoo_review-0.3.0.tar.gz.

File metadata

  • Download URL: odoo_review-0.3.0.tar.gz
  • Upload date:
  • Size: 42.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for odoo_review-0.3.0.tar.gz
Algorithm Hash digest
SHA256 ed7900c66cb6580c0548f6cb89cd010f3632c2f387ce8046e2e2c5075bfe6b74
MD5 7d2cacbb81d1161dc9d01097079dc2f8
BLAKE2b-256 324dfaa77c13e5c08daa99b5fe876b385c9f2c35c84abb4896f069e085e4c606

See more details on using hashes here.

Provenance

The following attestation bundles were made for odoo_review-0.3.0.tar.gz:

Publisher: publish.yml on fahrizaardhi/odoo-review

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file odoo_review-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: odoo_review-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 40.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for odoo_review-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b6c73a312f6c0c40d77c95767ac72f7541fd9173be0563bb923ae8efc167fffc
MD5 69c08276e5686eea1b3ad0127b11585b
BLAKE2b-256 fa29fa87141fafd5a15896b74d9e2fc96516b121b179707d7d398f541fa58239

See more details on using hashes here.

Provenance

The following attestation bundles were made for odoo_review-0.3.0-py3-none-any.whl:

Publisher: publish.yml on fahrizaardhi/odoo-review

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page