Adding a New Analyzer
1. Create the package
Section titled “1. Create the package”A minimal analyzer package looks like:
my_analyzer_package/├── pyproject.toml└── my_analyzer/ ├── __init__.py exports MyAnalyzer ├── analyzer.py MyAnalyzer(BaseAnalyzer) ├── parser.py parsing logic ├── manifest.py VERSION, FILE_GLOBS └── rules/ ├── generic-rules.md └── aws-rules.mdBaseAnalyzer lives in OpenTremor Core (opentremor_core.controllers.analyzers.base) — your package depends on opentremor-core, not the other way around.
VERSION = "1.0.0"# Glob patterns of file paths this analyzer claims — used by auto-routing# (POST /{namespace}/ingest|analyze/auto and the GitHub webhook) to decide# which analyzer handles which changed file in a diff. Leave empty if the# input isn't a single file (like a full `terraform plan` text stream) —# implement sniff() below instead for that case.FILE_GLOBS: list[str] = ["*.tf"]from __future__ import annotationsfrom pathlib import Pathfrom typing import Any, Optional
from opentremor_core.controllers.analyzers.base import BaseAnalyzerfrom .manifest import FILE_GLOBS, VERSIONfrom .parser import MyParser
_RULES_DIR = Path(__file__).parent / "rules"
class MyAnalyzer(BaseAnalyzer): name = "my-analyzer" # URL slug — must be URL-safe description = "Analyzes XYZ for security issues." version = VERSION file_globs = FILE_GLOBS
# Optional: only needed if raw_input can arrive with no file path to glob # against FILE_GLOBS (e.g. a single blob pasted into the dashboard's # Analyze form, not a diff). Default is 0.0 (opt out of content-sniffing # entirely) — override only if your format is recognisable from content # alone. content_router.route_auto asks every registered analyzer for a # score and picks the highest one above its confidence threshold. def sniff(self, raw_input: str) -> float: return 1.0 if "some distinctive marker text" in raw_input else 0.0
def ingest(self, raw_input: str) -> list[dict[str, Any]]: return MyParser(raw_input).parse()
def get_rules(self, rule_type: Optional[str] = None) -> str: filename = f"{rule_type}-rules.md" if rule_type else "generic-rules.md" rules_file = _RULES_DIR / filename if not rules_file.exists(): raise FileNotFoundError(f"No rules file: {rules_file}") return rules_file.read_text()from .analyzer import MyAnalyzer
__all__ = ["MyAnalyzer"]Each unit your parser returns must be an AnalysisUnit.model_dump()-shaped dict — this model is analyzer-agnostic (it’s the “thing an analyzer produced and an LLM analyses one of”: a Terraform resource block, an Ansible task, a Packer builder block, …):
{ "hash": generate_resource_hash(body), # from opentremor_core.libs.hash "type": item["type"], "name": item["name"], "action": item["action"], "body": body, "metadata": {}, # UnitMetadata — generated_at_utc, source_path, group_path, analyzer}2. Register the analyzer via entry points
Section titled “2. Register the analyzer via entry points”Declare the entry point in your package’s own pyproject.toml — no code in OpenTremor Core changes:
[tool.poetry.dependencies]opentremor-core = "^1.0" # or a path/git dependency during development
[tool.poetry.plugins."opentremor_core.analyzers"]my-analyzer = "my_analyzer:MyAnalyzer"At startup, core discovers every installed package declaring this entry-point group and registers each one automatically (controllers/app.py::_discover_analyzers()). Installing your package is the entire integration step.
Every analyzer is enabled for every org by default once registered — an org-admin can disable one via POST /orgs/{org_id}/analyzers/{name} with {"enabled": false}.
3. Verify
Section titled “3. Verify”pip install -e ./my_analyzer_package # or poetry add, if depending on it from core's own pyproject
curl http://localhost:8000/analyzers# → {"analyzers": [..., {"name": "my-analyzer", "description": "...", "version": "1.0.0", "file_globs": [...]}]}
curl http://localhost:8000/my-analyzer/rules# → Markdown ruleset (with any enabled org custom rules for this analyzer, grouped by category, appended)
curl -X POST http://localhost:8000/my-analyzer/my-namespace/ingest \ -H "Content-Type: text/plain" \ --data-binary @my_input.txt# → [{"hash": "…", "type": "…", …}]All of the above require an authenticated principal (API key or session) unless auth is disabled — see API overview for the auth model.
Once FILE_GLOBS/sniff() are set, your analyzer is also reachable without
naming it — POST /{namespace}/ingest/auto and POST /{namespace}/analyze/auto
(see API endpoints) resolve which analyzer(s) apply from the
input itself, the same mechanism the GitHub webhook uses for PR diffs:
curl -X POST http://localhost:8000/my-namespace/ingest/auto \ -H "Content-Type: text/plain" \ --data-binary @my_input.txt# → {"units": [...], "analyzers_used": ["my-analyzer"], "detections": [...]}Checklist
Section titled “Checklist”-
nameis URL-safe (lowercase, hyphens only) - package is self-contained — no files shared with other analyzer packages
-
ingest()always returns a list (empty list is valid) - every unit dict contains a
hashkey -
hashis computed withgenerate_resource_hash()on normalised content - rule files are in the package’s own
rules/following the{type}-rules.mdconvention -
manifest.pysetsVERSIONandFILE_GLOBS -
FILE_GLOBSset (diff-shaped input) orsniff()overridden (non-diff single-blob input) so auto-routing can find this analyzer — at least one, unless the analyzer is genuinely only ever invoked by explicit name - the
opentremor_core.analyzersentry point is declared inpyproject.toml - unit tests added
- a
LICENSEis present and thelicensefield inpyproject.tomlmatches it
Licensing your analyzer
Section titled “Licensing your analyzer”OpenTremor is split across two licences. The server — OpenTremor-core,
OpenTremor-platform, OpenTremor-dashboard and OpenTremor-task-scheduler — is
AGPL-3.0-or-later. Every integration surface, including all four first-party
analyzers, is Apache-2.0.
Analyzers can be permissive despite depending on an AGPL core because core carries the
OpenTremor Analyzer Plugin Exception (LICENSE-EXCEPTION in the core repo), an
additional permission granted under section 7 of the AGPL. It says that a work which
uses core only through the analyzer plugin API may be combined with core and
distributed under any terms you choose.
The exception names that API surface explicitly, and it is exactly what this page has been describing:
| Covered | Where |
|---|---|
BaseAnalyzer, AnalyzerRegistry | opentremor_core.controllers.analyzers.base |
AnalysisUnit, UnitMetadata | opentremor_core.models.resource |
generate_resource_hash | opentremor_core.libs.hash |
the opentremor_core.analyzers entry-point group | your pyproject.toml |
If you contribute an analyzer back for inclusion alongside the first-party ones, match
their licence: license = "Apache-2.0" in pyproject.toml, with a matching LICENSE
and NOTICE copied from any existing analyzer.