OpenGrep engine¶
The Controller server runs OpenGrep as a second static-analysis engine during a
whole-codebase scan (security-review-ci scan-code). The built-in scanner remains enabled and
unchanged. Fragment scans (scan_code) do not start an external process.
Docker image¶
The -opengrep image (latest-opengrep,
X.Y.Z-opengrep) ships a pinned OpenGrep release in /usr/local/bin/opengrep, checked against its
SHA-256 digest at build time (opengrep target of the Dockerfile) and unpacked there, read-only,
so that a scan never extracts it: use it for the Controller server. The
default rules are installed and kept up to date at each start. The default image does not contain OpenGrep and sets SECREVIEW_OPENGREP_ENABLED=false. With an installation from
the PyPI package, install OpenGrep yourself and set SECREVIEW_OPENGREP_BINARY if it is not in
/usr/local/bin.
Configuration¶
The engine is enabled by default (except in the default Docker image, without OpenGrep), but it only runs once at least one YAML rule is installed:
without any rule, scan-code runs the built-in scanner only. Set
SECREVIEW_OPENGREP_ENABLED=false to turn it off. Environment variables of the Controller server:
| Variable | Default | Description |
|---|---|---|
SECREVIEW_OPENGREP_ENABLED |
true |
Enable OpenGrep for whole-codebase scans |
SECREVIEW_OPENGREP_BINARY |
/usr/local/bin/opengrep |
OpenGrep executable |
SECREVIEW_OPENGREP_RULES_DIR |
/var/lib/secreview/rules/opengrep |
Directory containing OpenGrep YAML rule packs |
SECREVIEW_OPENGREP_TIMEOUT_SECONDS |
600 |
Maximum duration of one OpenGrep scan |
SECREVIEW_OPENGREP_MAX_CONCURRENT_SCANS |
1 |
OpenGrep scans running at the same time in one Controller server process; a scan beyond is refused with 503 and Retry-After |
SECREVIEW_OPENGREP_JOBS |
2 |
Cores used by one OpenGrep scan (--jobs) |
SECREVIEW_OPENGREP_MAX_MEMORY_MB |
2048 |
Memory in MiB of one OpenGrep scan (--max-memory, 0: no limit) |
The default rules are installed at start-up by the -opengrep image, or by
security-review-controller opengrep install; add your own rule packs in the configured
directory, for example by mounting an administrator-managed directory at
/var/lib/secreview/rules/opengrep (keep it on a volume, so that the edits and deletions made
on the OpenGrep rules page survive a new container). Rules are never downloaded. The
directory must contain at least one .yml or .yaml file.
When enabled, the controller writes the source files already received from the CI client into a
private temporary directory, invokes OpenGrep without a shell, parses its JSON output, and removes
the workspace. Findings are normalized into the existing report format and are included in the
same report and SARIF export as built-in findings. OpenGrep rule IDs are prefixed with
OPENGREP: (OPENGREP:secreview.python.taint-sql). OpenGrep skips the paths of its default
ignore list (test directories such as tests/, node_modules/...).
Default rules¶
Security Review ships 90 OpenGrep rules written for the project (Apache-2.0 license, like the
rest of the code): the community packs are not redistributed, because their licenses
(Semgrep Rules License, or LGPL with the Commons Clause for the opengrep-rules fork) forbid
their use in a product like this one. They complement the built-in rules with
what a line-by-line pattern cannot see:
- taint analysis: a value read from the request (Flask, Django, FastAPI, Spring, servlets,
Express, NestJS, ASP.NET Core,
net/http, Gin, Echo) is followed through variables and string building until it reaches a dangerous call (SQL, command, file path, outgoing URL, redirect, template, HTML, deserialization). A sanitizer on the way (int(),secure_filename,path.basename,html.EscapeString...) stops it; - structure: an option that is missing (an XML factory without
disallow-doctype-decl, an archive extracted without a filter, an HTTP server without timeouts, a Dockerfile withoutUSER) or a combination of arguments (CORS with any origin and credentials).
| Language (directory) | Rules | Main checks |
|---|---|---|
Python (python) |
18 | Taint: SQL, command, eval, SSRF, path, redirect, template, HTML, deserialization; TLS, YAML, JWT, debug, CORS, archives, hashes, cookies |
Java, Spring (java) |
18 | Taint: SQL/JPQL, command, path, SSRF, redirect, XSS, SpEL/OGNL, JNDI/LDAP; deserialization, XXE (DOM, SAX, StAX), CSRF, trust-all TLS, host name, ciphers, hashes, cookies, Jackson |
JavaScript, TypeScript, Angular (javascript) |
16 | Taint: SQL, command, eval/vm, path, SSRF, redirect, XSS, RegExp, DOM XSS from the URL or the route; message data read without checking its origin, bypassSecurityTrust*, TLS, CORS, JWT, cookies, crypto |
Go (go) |
11 | Taint: SQL, command, path, SSRF, redirect, XSS; InsecureSkipVerify, zip slip, server timeouts, crypto, math/rand for secrets |
C#, ASP.NET Core (csharp) |
12 | Taint: SQL (ADO.NET, EF Core, Dapper), process, path, SSRF, redirect; deserializers, certificate validation, JWT, crypto, DTD, HTML encoding, CORS |
Rust (rust) |
9 | Taint: SQL (sqlx, diesel, drivers) from axum and actix-web extractors; SQL built with format!, shell, TLS, crypto, JWT, CORS, cookies, HTML |
Dockerfile (dockerfile) |
2 | No USER before CMD/ENTRYPOINT, last USER is root |
GitHub Actions (github-actions) |
4 | Script injection with ${{ }}, pull_request_target checking out the pull request, action not pinned on a SHA, write-all |
Their ids follow secreview.<language>.<check> (secreview.go.zip-slip) and their metadata
give the CWE and the OWASP category, copied into the findings.
While the engine is enabled (SECREVIEW_OPENGREP_ENABLED) and the OpenGrep executable is present
(-opengrep image), the Controller server brings them up to date at each start, like the default TOML rules: a new rule is installed, a rule
left as installed is updated with the new version, and a rule edited or deleted on the
OpenGrep rules page is left alone. The digests of the installed versions are kept in
.secreview-default-rules.json in the rules directory; delete it to start over. The command
installs them on demand, on the Controller server:
security-review-controller opengrep list # bundled rules by language
security-review-controller opengrep install # every language
security-review-controller opengrep install --language python --language javascript
security-review-controller opengrep install --overwrite # after an upgrade
install copies one file per rule into SECREVIEW_OPENGREP_RULES_DIR and prints the installed,
replaced and skipped ids (JSON). A rule that already exists is kept, so that an edit made on the
OpenGrep rules page survives; --overwrite replaces it with the version of the package
(after an upgrade, for instance). To drop a rule, delete it on that page. In a container, run
the command in the Controller server container
(docker compose exec controller security-review-controller opengrep install).
Operational security¶
- Run the controller as the existing unprivileged
secreviewuser. - Keep rule packs administrator-controlled; treat rule updates as security-sensitive changes.
- Use container-level CPU, memory and process limits for the controller. The scan timeout, the number of concurrent scans and the cores and memory of a scan are enforced by the adapter (on timeout, OpenGrep and its workers are killed); they are not a substitute for container resource limits.
- If OpenGrep is busy with other scans or unavailable,
scan-codegets a503answer: retry later. - Do not mount application secrets or unrelated host paths into the scan workspace.
- The controller does not execute scanned source files. It does write the received source payload to a temporary directory for OpenGrep to read.
- If rules are installed but the OpenGrep binary is missing, or the scan fails, the code scan fails rather than silently reporting a successful built-in-only scan.
Managing rules from the web interface¶
The Controller exposes an authenticated rule-management page at /ui/opengrep-rules.
Users with rules.read can inspect the YAML files in SECREVIEW_OPENGREP_RULES_DIR;
users with rules.manage can create, edit, validate and delete them. Each managed YAML file
contains exactly one rule, and its rule id must match the file identifier. Writes are validated
with opengrep validate <file> before an atomic replacement. The Controller does not download
rule packs or follow remote rule sources on behalf of the administrator.
Rules proposed by agents¶
During a review, when a confirmed issue follows a vulnerable pattern the scan missed and that
may recur, the agent can propose an OpenGrep rule with the MCP tool propose_opengrep_rule
(tools): the YAML rule, its reason and test code (ruleid: and
ok: cases). The rule is not installed: the MCP server only stores the proposal in the
database, and the review prompts tell the agent to propose a rule only after saving the report.
- The rule must hold exactly one rule whose
idis the proposed identifier; thesecreview.prefix is kept for the default rules. A rule that looks like it holds a secret, or with invisible characters, is refused. The reason is masked and bounded like the reason of a status proposal. - The token must reach the project and hold
reports.writeon it (as forsave_security_report). A new proposal with the identifier of a pending one replaces it; at mostSECREVIEW_OPENGREP_MAX_PENDING_PROPOSALSproposals wait at the same time. - Pending proposals are listed at the top of the OpenGrep rules page for the users with
rules.manage. Review opens one in the editor with its project, author, reason and test code: the approver may correct the YAML (not its identifier), validate it with OpenGrep, then Approve and install it or Reject it with an optional reason. - Only a person logged in to the web interface decides: a bearer token, which an agent may
hold, gets
403. An approval validates the rule withopengrep validateand creates its file; it never replaces an installed rule (409). The proposal, the approval and the rejection are written to the audit log.
| Route | Permission | Role |
|---|---|---|
GET /api/v1/opengrep/proposals?status=pending |
rules.manage |
Proposals of a status (pending by default, approved, rejected), newest first |
POST /api/v1/opengrep/proposals/{id}/approve |
rules.manage, web session |
Body {"content": null} (or the corrected YAML): validates and installs the rule |
POST /api/v1/opengrep/proposals/{id}/reject |
rules.manage, web session |
Body {"reason": null}: the rule is not installed |