trapline 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +585 -0
- data/config.ru.example +21 -0
- data/exe/trapline +8 -0
- data/lib/generators/trapline/install_generator.rb +34 -0
- data/lib/generators/trapline/templates/initializer.rb +23 -0
- data/lib/tasks/trapline.rake +19 -0
- data/lib/trapline/cli.rb +212 -0
- data/lib/trapline/configuration.rb +19 -0
- data/lib/trapline/cve/client.rb +203 -0
- data/lib/trapline/cve/record.rb +160 -0
- data/lib/trapline/engine.rb +243 -0
- data/lib/trapline/generator.rb +236 -0
- data/lib/trapline/languages.rb +65 -0
- data/lib/trapline/railtie.rb +26 -0
- data/lib/trapline/rule.rb +165 -0
- data/lib/trapline/runtime.rb +148 -0
- data/lib/trapline/store.rb +113 -0
- data/lib/trapline/suite.rb +44 -0
- data/lib/trapline/templates/cwe/cwe-22.yml +167 -0
- data/lib/trapline/templates/cwe/cwe-327.yml +124 -0
- data/lib/trapline/templates/cwe/cwe-502.yml +123 -0
- data/lib/trapline/templates/cwe/cwe-601.yml +124 -0
- data/lib/trapline/templates/cwe/cwe-611.yml +87 -0
- data/lib/trapline/templates/cwe/cwe-78.yml +166 -0
- data/lib/trapline/templates/cwe/cwe-79.yml +164 -0
- data/lib/trapline/templates/cwe/cwe-798.yml +125 -0
- data/lib/trapline/templates/cwe/cwe-89.yml +189 -0
- data/lib/trapline/templates/cwe/cwe-917.yml +46 -0
- data/lib/trapline/templates/cwe/cwe-918.yml +166 -0
- data/lib/trapline/templates/cwe/cwe-94.yml +131 -0
- data/lib/trapline/templates.rb +261 -0
- data/lib/trapline/validator.rb +359 -0
- data/lib/trapline/version.rb +5 -0
- data/lib/trapline/web/app.rb +234 -0
- data/lib/trapline/web/assets/THIRD_PARTY_LICENSES.txt +214 -0
- data/lib/trapline/web/assets/app.css +399 -0
- data/lib/trapline/web/assets/app.js +804 -0
- data/lib/trapline/web/assets/codemirror.css +344 -0
- data/lib/trapline/web/assets/codemirror.js +13263 -0
- data/lib/trapline/web/assets/index.html +165 -0
- data/lib/trapline/web/assets/jetbrains-mono-latin-400-normal.woff2 +0 -0
- data/lib/trapline/web/assets/jetbrains-mono-latin-600-normal.woff2 +0 -0
- data/lib/trapline/web/assets/public-sans-latin-400-normal.woff2 +0 -0
- data/lib/trapline/web/assets/public-sans-latin-500-normal.woff2 +0 -0
- data/lib/trapline/web/assets/public-sans-latin-600-normal.woff2 +0 -0
- data/lib/trapline/web/assets/public-sans-latin-700-normal.woff2 +0 -0
- data/lib/trapline/web/server.rb +108 -0
- data/lib/trapline.rb +70 -0
- metadata +136 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4512a1d91ced4516d053f35a63b9c3e421e13445a047bd6204890e1b28094dcf
|
|
4
|
+
data.tar.gz: 63389b3d583eea89f84dcb04ce2fedf502ee5c917d9eb363b455237b3a117ba5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 52530cb305ad1f19e1740f7265b80fbd0345031cad2f4f033bf28908d122c272b04e1ea2279895a11a07b23ec776aa683787126f3199bb41ceb3f50e36dc22ed
|
|
7
|
+
data.tar.gz: 67d866ff50f4d6fd28da3568322f34bb46928c95216f691257753e19a244a15544cbef9eddabfeb2791eeba4f2dcbc61c33896c9c6d48709f1fd33799ecfa949
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jose Augusto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
# Trapline
|
|
2
|
+
|
|
3
|
+
> **Trapline — set traps for vulnerable code: from CVE to opengrep/semgrep rule.**
|
|
4
|
+
|
|
5
|
+
A trapline is the route of traps a trapper sets and checks regularly. Here, each rule is a trap for one kind of vulnerable code, and the rule set runs on every change.
|
|
6
|
+
|
|
7
|
+
Trapline is a web editor and CLI for writing, testing and maintaining [opengrep](https://github.com/opengrep/opengrep) and [semgrep](https://semgrep.dev) rules, plus a generator that turns a CVE into rule drafts. It runs on its own, as a Rack app, or mounted inside a Rails app. Its only requirements are Ruby and the engine binary.
|
|
8
|
+
|
|
9
|
+
- **Web editor.** Validates the YAML as you type, keeps a metadata form in sync with it, and runs the rule against test code. Findings are highlighted in the code. You mark which lines must or must not match by clicking the margin or accepting results; you don't write annotation comments by hand.
|
|
10
|
+
- **CVE generator.** Looks the CVE up in NVD and OSV (CWE, CVSS, affected packages and versions) and proposes drafts: a template for the vulnerability class, a check for vulnerable runtime images, a rule that finds usage of the affected package, and a taint skeleton. The metadata is filled in for you.
|
|
11
|
+
- **Plain files.** Each rule is saved as `name.yaml` next to its test file `name.<ext>`. This is the layout `opengrep --test` and `semgrep --test` expect, so the folder works without Trapline.
|
|
12
|
+
- **CI-friendly.** `trapline test` runs every rule against its test file and exits non-zero on failure.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Contents
|
|
17
|
+
|
|
18
|
+
- [Requirements](#requirements)
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Quick start](#quick-start)
|
|
21
|
+
- [The web editor](#the-web-editor)
|
|
22
|
+
- [Generating rules from a CVE](#generating-rules-from-a-cve)
|
|
23
|
+
- [Command line](#command-line)
|
|
24
|
+
- [Rules folder and test annotations](#rules-folder-and-test-annotations)
|
|
25
|
+
- [Using the rules in CI](#using-the-rules-in-ci)
|
|
26
|
+
- [Rails integration](#rails-integration)
|
|
27
|
+
- [Running on any Rack server](#running-on-any-rack-server)
|
|
28
|
+
- [Configuration reference](#configuration-reference)
|
|
29
|
+
- [Ruby API](#ruby-api)
|
|
30
|
+
- [Security](#security)
|
|
31
|
+
- [Development](#development)
|
|
32
|
+
- [License](#license)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- **Ruby 3.3 or later.**
|
|
39
|
+
- **An engine: opengrep or semgrep.** Trapline looks for `opengrep` first, then `semgrep`, on your `PATH`. Without an engine, the editor still opens and validates rule structure, but it cannot run rules.
|
|
40
|
+
- **Network access (optional)** to `services.nvd.nist.gov` and `api.osv.dev`, for the CVE generator. Without it, you can type the CVE data in by hand.
|
|
41
|
+
|
|
42
|
+
There are no required gem dependencies. If `rackup` and `puma` (or `webrick`) are installed, `trapline server` uses them; otherwise it uses a small built-in HTTP server that is enough for local use.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
### 1. Install the gem
|
|
47
|
+
|
|
48
|
+
From RubyGems:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
gem install trapline
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or add it to a project's `Gemfile`:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
gem "trapline"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or build it from source:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/JAugusto42/trapline.git
|
|
64
|
+
cd trapline
|
|
65
|
+
gem build trapline.gemspec
|
|
66
|
+
gem install ./trapline-*.gem
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Install an engine
|
|
70
|
+
|
|
71
|
+
opengrep (recommended):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh | bash
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The installer puts the binary in `~/.opengrep/cli/latest/`. Make sure that directory is on your `PATH`, or point Trapline to the binary with `--engine-bin` or `TRAPLINE_ENGINE`.
|
|
78
|
+
|
|
79
|
+
Or semgrep:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install semgrep # or: brew install semgrep
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 3. Check the setup
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
trapline version
|
|
89
|
+
# trapline 0.1.0
|
|
90
|
+
# engine: opengrep 1.33.1 (/Users/you/.opengrep/cli/latest/opengrep)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Quick start
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
mkdir rules
|
|
97
|
+
trapline server -r ./rules
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Open <http://127.0.0.1:9292>. Then either:
|
|
101
|
+
|
|
102
|
+
- click **New rule**, pick a rule type and a language, and edit the example; or
|
|
103
|
+
- click **Generate from CVE**, enter an id such as `CVE-2021-44228`, pick a draft and click **Open in editor**.
|
|
104
|
+
|
|
105
|
+
Click **Run** (or press Ctrl/Cmd+Enter) to run the rule against the test code. Click **Save** (Ctrl/Cmd+S) to write it to `./rules`.
|
|
106
|
+
|
|
107
|
+
## The web editor
|
|
108
|
+
|
|
109
|
+
The screen has three columns:
|
|
110
|
+
|
|
111
|
+
| Column | What it holds |
|
|
112
|
+
|---|---|
|
|
113
|
+
| **Left** | Your saved rules (with a filter by id, CVE or CWE), **New rule** and **Generate from CVE** |
|
|
114
|
+
| **Middle** | **Rule details**, a form for id, severity, languages, message, CWE, CVE, confidence and references, kept in sync with the **YAML** editor below it. Validation issues appear under the YAML as you type. |
|
|
115
|
+
| **Right** | **Test code**: an example file the rule runs against, with the run result on top and the per-line results below |
|
|
116
|
+
|
|
117
|
+
### Writing test code
|
|
118
|
+
|
|
119
|
+
The test code is an example file with code the rule **must** flag and code it **must not** flag. Trapline checks each run against those expectations:
|
|
120
|
+
|
|
121
|
+
- **Mark lines from the margin.** Click the margin next to a line to cycle its expectation: **must match** → **must not match** → **no expectation**.
|
|
122
|
+
- **Accept results.** After a run, each flagged line that has no expectation yet shows two buttons. **Expected** keeps it as a test; **False positive** records that the rule should not match there. **Accept all as expected** does this for every new finding at once.
|
|
123
|
+
- **Fix failing expectations.** A line expected to match that doesn't gets a **Remove** button. A line expected not to match that does gets an **Expect it** button.
|
|
124
|
+
|
|
125
|
+
A typical loop is: paste a vulnerable snippet and a fixed one, run, accept what is right, mark what is wrong, then refine the rule until everything passes.
|
|
126
|
+
|
|
127
|
+
The expectations are stored as ordinary `ruleid:` / `ok:` comments in the test file, so the file stays in the `--test` format that opengrep and semgrep read. Trapline writes and removes them for you, and if you rename the rule's id, it rewrites them too. In the editor each comment is drawn as a compact badge (**must match**, **must not match**, or a *todo* variant) on its own line, so line numbers stay the same as in the file. Click a badge to edit the comment by hand, or tick **Show comments** to see them all as text. See [Rules folder and test annotations](#rules-folder-and-test-annotations) for the format.
|
|
128
|
+
|
|
129
|
+
### Running
|
|
130
|
+
|
|
131
|
+
- **Run** runs the rule once. With **Run on edit** checked, it also runs after each pause in typing.
|
|
132
|
+
- The card above the test code shows the outcome:
|
|
133
|
+
- **green**: all expectations met;
|
|
134
|
+
- **red**: an expectation failed, or the engine reported an error, such as an invalid pattern;
|
|
135
|
+
- **blue**: the code has no expectations yet.
|
|
136
|
+
- A run started with the button or the shortcut also shows a notification.
|
|
137
|
+
- Markers in the margin show the result for each line: ✓ passed, ✕ failed, ! flagged with no expectation.
|
|
138
|
+
|
|
139
|
+
### Saving and downloading
|
|
140
|
+
|
|
141
|
+
- **Save** (Ctrl/Cmd+S) writes the rule and its test code to the rules folder. The file name defaults to the last part of the rule id; for example, `php.lang.security.cve-2026-109618-custom` is saved as `cve-2026-109618-custom.yaml`.
|
|
142
|
+
- **Download** saves the YAML through the browser.
|
|
143
|
+
- **Delete** removes a saved rule and its test file.
|
|
144
|
+
- In read-only mode (`--read-only`, or the Rails default outside development), saving and deleting are disabled. Validating and running still work.
|
|
145
|
+
|
|
146
|
+
### Keyboard shortcuts
|
|
147
|
+
|
|
148
|
+
| Shortcut | Action |
|
|
149
|
+
|---|---|
|
|
150
|
+
| Ctrl/Cmd + Enter | Run |
|
|
151
|
+
| Ctrl/Cmd + S | Save |
|
|
152
|
+
| Tab / Shift + Tab | Indent / outdent |
|
|
153
|
+
|
|
154
|
+
## Generating rules from a CVE
|
|
155
|
+
|
|
156
|
+
In the editor, click **Generate from CVE**, enter a `CVE-…` or `GHSA-…` id, optionally choose a **Language**, and click **Search**.
|
|
157
|
+
|
|
158
|
+
Trapline shows:
|
|
159
|
+
|
|
160
|
+
- a summary of the advisory: severity, CVSS, CWEs, aliases and sources;
|
|
161
|
+
- **notes** that explain what it inferred and what needs review;
|
|
162
|
+
- a list of **drafts**.
|
|
163
|
+
|
|
164
|
+
Pick a draft and click **Open in editor**.
|
|
165
|
+
|
|
166
|
+
If the machine has no network access, open **No network access? Enter the data manually** and fill in the CWE, ecosystem, package, fixed version and description.
|
|
167
|
+
|
|
168
|
+
From the command line:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
trapline cve CVE-2021-44228 # lists the drafts and prints the first
|
|
172
|
+
trapline cve CVE-2021-44228 --lang java # prefer one language
|
|
173
|
+
trapline cve CVE-2021-44228 --pick 2 --save # save the 2nd draft to the rules folder
|
|
174
|
+
trapline cve --osv-json advisory.json # offline: use a downloaded OSV record
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Draft types
|
|
178
|
+
|
|
179
|
+
| Draft | When it appears | What it does |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| **Vulnerability class** (e.g. *SQL injection (CWE-89)*) | A bundled template matches the CWE | Taint or pattern rule for that class, in the chosen language, with an annotated example |
|
|
182
|
+
| **Vulnerable runtime version** | The CVE affects PHP, Python, Node.js, Ruby, Go or .NET itself and NVD lists version ranges (for Go, OSV's `stdlib` ranges also count) | Regex rule that flags references to the official image (`FROM php:8.2.34-fpm`, `image: php:8.2.34` in Compose, Kubernetes or CI files, Helm `repository:` + `tag:`, `docker run`) when the tag falls in an affected range. For .NET, `mcr.microsoft.com/dotnet/runtime` and `aspnet`. The notes recommend an image scanner (Trivy, Grype, osv-scanner) for built images and OS packages |
|
|
183
|
+
| **Usage of package** | OSV lists affected packages in RubyGems, PyPI, npm, Maven or Go | Finds `require` / `import` / `gem "…"` of the package; the message names the fixed version |
|
|
184
|
+
| **Taint skeleton** | Always | Source / sanitizer / sink skeleton with the CVE metadata filled in, for writing the rule by hand |
|
|
185
|
+
|
|
186
|
+
### How the generator decides
|
|
187
|
+
|
|
188
|
+
- **Vulnerability class.** If the CVE reports a CWE that has a template, Trapline uses it. Aliases are mapped, so CWE-77 uses the CWE-78 template and CWE-564 uses CWE-89. When several CWEs match, the one that best fits the description comes first.
|
|
189
|
+
|
|
190
|
+
Trapline only guesses from keywords in the description ("SQL injection", "path traversal"…) when the CVE reports no CWE or only a generic one, such as CWE-20. It never treats "remote code execution" as a class: that is an impact, shared by memory corruption, deserialization and command injection.
|
|
191
|
+
- **Language.** The one you pick. Otherwise, the ecosystem of the affected packages. Otherwise, the affected runtime or framework named in the NVD CPE data (e.g. `php:php` → PHP, `djangoproject:django` → Python). As a last resort, Ruby, and a note says so. When a CVE affects several languages, a note lists them so you can regenerate for another one.
|
|
192
|
+
- **Severity.** Copied from the CVE: CRITICAL, HIGH, MEDIUM or LOW. MODERATE and unknown map to MEDIUM. opengrep and current semgrep accept this scale; semgrep releases from before 2024 only accept ERROR / WARNING / INFO.
|
|
193
|
+
- **Rule id.** Follows the registry convention `<language>.<framework or "lang">.security.<name>`, for example `java.lang.security.cve-2021-44228-expression-language-jndi-lookup-injection` or `regex.php.security.cve-2026-109618-vulnerable-php-image`.
|
|
194
|
+
- **Memory-safety CVEs** (use-after-free, buffer overflow…) live in native code, so no rule over application code can find them. The notes say so, and the runtime-version draft is usually the most useful one.
|
|
195
|
+
|
|
196
|
+
Every draft is a starting point. Read the notes, then refine the rule and its test code before relying on it.
|
|
197
|
+
|
|
198
|
+
### Bundled templates
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
trapline templates
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
| CWE | Class | Languages |
|
|
205
|
+
|---|---|---|
|
|
206
|
+
| 22 | Path traversal | ruby, python, javascript, java, go |
|
|
207
|
+
| 78 | OS command injection | ruby, python, javascript, java, go |
|
|
208
|
+
| 79 | Cross-site scripting (XSS) | ruby, python, javascript, java, go |
|
|
209
|
+
| 89 | SQL injection | ruby, python, javascript, java, go |
|
|
210
|
+
| 94 | Code injection | ruby, python, javascript, java |
|
|
211
|
+
| 327 | Weak cryptography | ruby, python, javascript, java, go |
|
|
212
|
+
| 502 | Insecure deserialization | ruby, python, javascript, java |
|
|
213
|
+
| 601 | Open redirect | ruby, python, javascript, java, go |
|
|
214
|
+
| 611 | XML external entities (XXE) | java, python, ruby |
|
|
215
|
+
| 798 | Hard-coded credentials | ruby, python, javascript, java, go |
|
|
216
|
+
| 917 | Expression language / JNDI lookup injection | java |
|
|
217
|
+
| 918 | Server-side request forgery (SSRF) | ruby, python, javascript, java, go |
|
|
218
|
+
|
|
219
|
+
The **New rule** dialog also offers four starting points, each with example test code: a simple pattern, a combination with `pattern-not`, data flow (taint) and a regular expression.
|
|
220
|
+
|
|
221
|
+
## Command line
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
trapline <command> [options]
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Global options, accepted by every command:
|
|
228
|
+
|
|
229
|
+
| Option | Meaning |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `-r, --rules DIR` | Rules folder (default: `./rules`, or `$TRAPLINE_RULES_PATH`) |
|
|
232
|
+
| `--engine-bin BIN` | Engine to use: `opengrep`, `semgrep` or a path |
|
|
233
|
+
|
|
234
|
+
### `trapline server`
|
|
235
|
+
|
|
236
|
+
Opens the web editor.
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
trapline server -r ./rules # http://127.0.0.1:9292
|
|
240
|
+
trapline server -p 8080 # other port
|
|
241
|
+
trapline server --host 0.0.0.0 --basic-auth admin:secret
|
|
242
|
+
trapline server --read-only # validate and run only
|
|
243
|
+
trapline server --offline # no NVD/OSV requests
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
| Option | Meaning |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `-p, --port PORT` | Port (default 9292) |
|
|
249
|
+
| `-b, --host HOST` | Interface (default `127.0.0.1`) |
|
|
250
|
+
| `--basic-auth USER:PASS` | Require HTTP Basic authentication |
|
|
251
|
+
| `--read-only` | Disable saving and deleting |
|
|
252
|
+
| `--offline` | Disable network lookups; CVE data must be typed in |
|
|
253
|
+
|
|
254
|
+
### `trapline validate FILE...`
|
|
255
|
+
|
|
256
|
+
Checks rule structure: required keys, patterns, languages, severity, metavariables and recommended metadata.
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
trapline validate rules/*.yaml
|
|
260
|
+
trapline validate rules/*.yaml --engine # also ask opengrep/semgrep to load the rule
|
|
261
|
+
trapline validate rules/sqli.yaml --json
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Exits with 1 if any file has errors.
|
|
265
|
+
|
|
266
|
+
### `trapline test [DIR]`
|
|
267
|
+
|
|
268
|
+
Runs every rule in the rules folder against its test file and checks the annotations. Use it in CI.
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
trapline test -r rules
|
|
272
|
+
# ok sql-injection (4/4)
|
|
273
|
+
# FAIL open-redirect (2/3)
|
|
274
|
+
# false negative: expected a finding on line 12
|
|
275
|
+
#
|
|
276
|
+
# 2 rules, 1 with problems
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Exits with 1 if any rule fails or errors. Rules without a test file are reported as `no test`, and test files without annotations as `no annotations`. Neither of those fails the run.
|
|
280
|
+
|
|
281
|
+
### `trapline run RULE TARGET`
|
|
282
|
+
|
|
283
|
+
Runs one rule against one source file. The language comes from the file extension.
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
trapline run rules/sqli.yaml app/models/user.rb
|
|
287
|
+
trapline run rules/sqli.yaml app/models/user.rb --json
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### `trapline cve ID`
|
|
291
|
+
|
|
292
|
+
See [Generating rules from a CVE](#generating-rules-from-a-cve).
|
|
293
|
+
|
|
294
|
+
| Option | Meaning |
|
|
295
|
+
|---|---|
|
|
296
|
+
| `-l, --lang LANG` | Preferred language |
|
|
297
|
+
| `--pick N` | Choose the Nth draft (default 1) |
|
|
298
|
+
| `--save` | Save the chosen draft to the rules folder instead of printing it |
|
|
299
|
+
| `--osv-json FILE` | Read a downloaded OSV record instead of querying the network |
|
|
300
|
+
|
|
301
|
+
### `trapline templates` and `trapline version`
|
|
302
|
+
|
|
303
|
+
`templates` lists the bundled CWE templates. `version` shows the Trapline version and the detected engine.
|
|
304
|
+
|
|
305
|
+
## Rules folder and test annotations
|
|
306
|
+
|
|
307
|
+
Trapline stores rules in the `--test` layout used by opengrep and semgrep: one YAML file per rule, with a test file of the same name next to it.
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
rules/
|
|
311
|
+
sql-injection.yaml # the rule
|
|
312
|
+
sql-injection.rb # its test code
|
|
313
|
+
web/
|
|
314
|
+
open-redirect.yaml # subfolders are allowed
|
|
315
|
+
open-redirect.py
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The test file marks the expected results with comments on the line **above** the code. The comment can also go at the end of the line itself.
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
# ruleid: ruby.rails.security.sql-injection
|
|
322
|
+
User.where("name = '#{params[:name]}'")
|
|
323
|
+
|
|
324
|
+
# ok: ruby.rails.security.sql-injection
|
|
325
|
+
User.where(name: params[:name])
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
| Annotation | Meaning |
|
|
329
|
+
|---|---|
|
|
330
|
+
| `ruleid: <id>` | The next line **must** produce a finding |
|
|
331
|
+
| `ok: <id>` | The next line **must not** produce a finding |
|
|
332
|
+
| `todoruleid: <id>` | Known false negative: reported, but does not fail |
|
|
333
|
+
| `todook: <id>` | Known false positive: reported, but does not fail |
|
|
334
|
+
|
|
335
|
+
Additional rules:
|
|
336
|
+
|
|
337
|
+
- Several ids can share a comment: `# ruleid: rule-a, rule-b`.
|
|
338
|
+
- As in `--test`, every id in a `ruleid`, `ok` or `todoruleid` comment must be a rule of the file; otherwise the annotation fails. `todook` is exempt.
|
|
339
|
+
- A finding on a line with no annotation counts as a failure ("unexpected match on line N (the line is not marked as expected)").
|
|
340
|
+
- An annotation with no code below it, such as one followed by a blank line or at the end of the file, counts as a failure. Otherwise it would pass without testing anything.
|
|
341
|
+
- The comment prefix follows the language: `#` for Ruby, Python, YAML and Dockerfile; `//` for JavaScript, Java, Go, PHP and the C family.
|
|
342
|
+
|
|
343
|
+
Because the layout is the standard one, you can also run `opengrep --test rules/` or `semgrep --test rules/` directly.
|
|
344
|
+
|
|
345
|
+
## Using the rules in CI
|
|
346
|
+
|
|
347
|
+
Two separate jobs are useful:
|
|
348
|
+
|
|
349
|
+
1. **Test the rules** against their test files, with `trapline test`.
|
|
350
|
+
2. **Scan your code** with the rules, using the engine directly.
|
|
351
|
+
|
|
352
|
+
Example for GitHub Actions:
|
|
353
|
+
|
|
354
|
+
```yaml
|
|
355
|
+
name: security-rules
|
|
356
|
+
on: [pull_request]
|
|
357
|
+
|
|
358
|
+
jobs:
|
|
359
|
+
rules:
|
|
360
|
+
runs-on: ubuntu-latest
|
|
361
|
+
steps:
|
|
362
|
+
- uses: actions/checkout@v4
|
|
363
|
+
- uses: ruby/setup-ruby@v1
|
|
364
|
+
with: { ruby-version: "3.3" }
|
|
365
|
+
- name: Install opengrep
|
|
366
|
+
run: |
|
|
367
|
+
curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh | bash
|
|
368
|
+
echo "$HOME/.opengrep/cli/latest" >> "$GITHUB_PATH"
|
|
369
|
+
- run: gem install trapline
|
|
370
|
+
- name: Test the rules
|
|
371
|
+
run: trapline test -r security/semgrep
|
|
372
|
+
- name: Scan the code
|
|
373
|
+
run: opengrep scan --config security/semgrep --exclude security/semgrep --error .
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Keep the `--exclude` on the scan: the rules folder contains the test files, which are vulnerable on purpose.
|
|
377
|
+
|
|
378
|
+
## Rails integration
|
|
379
|
+
|
|
380
|
+
Add the gem and run the installer:
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
# Gemfile
|
|
384
|
+
gem "trapline"
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
bundle install
|
|
389
|
+
bin/rails generate trapline:install # mounts the editor at /trapline
|
|
390
|
+
bin/rails generate trapline:install --path /admin/rules
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The installer:
|
|
394
|
+
|
|
395
|
+
- adds the route `mount Trapline::Web::App.new, at: "/trapline"` to `config/routes.rb`;
|
|
396
|
+
- creates `config/initializers/trapline.rb`;
|
|
397
|
+
- creates the rules folder `security/semgrep/`.
|
|
398
|
+
|
|
399
|
+
The generated initializer:
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
Trapline.configure do |config|
|
|
403
|
+
config.rules_path = Rails.root.join("security", "semgrep").to_s
|
|
404
|
+
|
|
405
|
+
# Who can open the editor. Receives the Rack env; return true to allow access.
|
|
406
|
+
# config.authorize = ->(env) { env["warden"]&.user&.admin? } # Devise/Warden
|
|
407
|
+
# config.authorize = [ENV.fetch("TRAPLINE_USER"), ENV.fetch("TRAPLINE_PASSWORD")] # HTTP Basic
|
|
408
|
+
config.authorize = ->(_env) { Rails.env.development? }
|
|
409
|
+
|
|
410
|
+
# Read-only outside development (the editor still validates and runs rules).
|
|
411
|
+
config.read_only = !Rails.env.development?
|
|
412
|
+
end
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The defaults:
|
|
416
|
+
|
|
417
|
+
- **Access.** The editor is only reachable in development. To use it elsewhere, change `authorize`.
|
|
418
|
+
- **Read-only.** Outside development the editor is read-only, even if you remove the line above: the Railtie sets it. Set `config.read_only = false` in the initializer to allow writes.
|
|
419
|
+
- **Rules folder.** `security/semgrep`, unless `TRAPLINE_RULES_PATH` is set.
|
|
420
|
+
|
|
421
|
+
> **Note.** If you mount the editor without the initializer, `authorize` is `nil` and the editor is open to anyone who can reach the route. Always set `authorize`.
|
|
422
|
+
|
|
423
|
+
Rake tasks, for CI:
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
bin/rails trapline:test # run every rule against its test file
|
|
427
|
+
bin/rails trapline:validate # validate the structure of every rule
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
You can also set options from `config/application.rb` or an environment file through `config.trapline`:
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
config.trapline.engine_binary = "/usr/local/bin/opengrep"
|
|
434
|
+
config.trapline.allow_network = false
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Running on any Rack server
|
|
438
|
+
|
|
439
|
+
Copy `config.ru.example` (shipped with the gem) to `config.ru`:
|
|
440
|
+
|
|
441
|
+
```ruby
|
|
442
|
+
require "trapline"
|
|
443
|
+
|
|
444
|
+
credentials = %w[TRAPLINE_USER TRAPLINE_PASSWORD].map do |name|
|
|
445
|
+
ENV.fetch(name) { abort "config.ru: set TRAPLINE_USER and TRAPLINE_PASSWORD (HTTP Basic credentials)" }
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
Trapline.configure do |c|
|
|
449
|
+
c.rules_path = ENV.fetch("TRAPLINE_RULES_PATH", File.expand_path("rules", __dir__))
|
|
450
|
+
c.authorize = credentials
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
run Trapline::Web::App.new
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
TRAPLINE_USER=admin TRAPLINE_PASSWORD=change-me bundle exec puma
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Your `Gemfile` needs `gem "trapline"` and a server such as `gem "puma"`.
|
|
461
|
+
|
|
462
|
+
## Configuration reference
|
|
463
|
+
|
|
464
|
+
### Ruby options
|
|
465
|
+
|
|
466
|
+
Set them with `Trapline.configure { |c| ... }`:
|
|
467
|
+
|
|
468
|
+
| Option | Default | Meaning |
|
|
469
|
+
|---|---|---|
|
|
470
|
+
| `rules_path` | `./rules` | Folder where rules and their tests are stored |
|
|
471
|
+
| `engine_binary` | auto (`opengrep`, then `semgrep`) | Engine name or absolute path |
|
|
472
|
+
| `engine_timeout` | `60` | Seconds allowed for a single engine run |
|
|
473
|
+
| `authorize` | `nil` (no check) | Who can use the web editor: a callable receiving the Rack env and returning true/false, or a `[user, password]` pair for HTTP Basic. Any other value denies access. |
|
|
474
|
+
| `read_only` | `false` (Rails: `true` outside development) | Disable saving and deleting in the editor |
|
|
475
|
+
| `allow_network` | `true` | Allow NVD/OSV lookups |
|
|
476
|
+
| `cve_timeout` | `30` | Seconds allowed for a whole CVE lookup (several NVD/OSV requests); requests left over are skipped and a note says the data may be incomplete |
|
|
477
|
+
| `nvd_api_key` | `nil` | NVD API key, which raises the NVD rate limit |
|
|
478
|
+
|
|
479
|
+
### Environment variables
|
|
480
|
+
|
|
481
|
+
| Variable | Effect |
|
|
482
|
+
|---|---|
|
|
483
|
+
| `TRAPLINE_RULES_PATH` | Rules folder |
|
|
484
|
+
| `TRAPLINE_ENGINE` | Engine binary (name or path) |
|
|
485
|
+
| `TRAPLINE_ENGINE_TIMEOUT` | Engine timeout in seconds |
|
|
486
|
+
| `TRAPLINE_CVE_TIMEOUT` | CVE lookup time limit in seconds |
|
|
487
|
+
| `TRAPLINE_OFFLINE=1` | Disable NVD/OSV lookups |
|
|
488
|
+
| `NVD_API_KEY` | NVD API key ([request one](https://nvd.nist.gov/developers/request-an-api-key)) |
|
|
489
|
+
| `TRAPLINE_BUILTIN_SERVER=1` | Use the built-in HTTP server even when puma/webrick are installed |
|
|
490
|
+
| `TRAPLINE_LOG=1` | Log requests (built-in server) |
|
|
491
|
+
|
|
492
|
+
## Ruby API
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
require "trapline"
|
|
496
|
+
|
|
497
|
+
# Validate
|
|
498
|
+
result = Trapline.validate(File.read("rules/sqli.yaml"), engine: true)
|
|
499
|
+
result.valid? # => true / false
|
|
500
|
+
result.issues # => [#<Issue level="warning" path="rules[0].metadata" message="...">]
|
|
501
|
+
|
|
502
|
+
# Run with annotations
|
|
503
|
+
run = Trapline.engine.run(File.read("rules/sqli.yaml"), code: File.read("rules/sqli.rb"), language: "ruby")
|
|
504
|
+
run.findings # => [#<Finding rule_id="..." line=4 code="...">]
|
|
505
|
+
run.annotations # => { total: 5, passed: 5, failed: 0, results: [...] }
|
|
506
|
+
|
|
507
|
+
# Generate from a CVE
|
|
508
|
+
draft = Trapline.generate_from_cve("CVE-2021-44228")
|
|
509
|
+
draft[:notes] # what was inferred
|
|
510
|
+
draft[:candidates].first[:yaml]
|
|
511
|
+
draft[:candidates].first[:test_code]
|
|
512
|
+
|
|
513
|
+
# Generate from data typed in by hand (no network)
|
|
514
|
+
Trapline::Generator.new.analyze({
|
|
515
|
+
id: "CVE-2099-0001", cwes: "CWE-78", severity: "HIGH",
|
|
516
|
+
description: "OS command injection in the host parameter",
|
|
517
|
+
packages: [{ ecosystem: "npm", name: "ping-lib", fixed: "2.0.0" }]
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
# Rules on disk
|
|
521
|
+
store = Trapline.store # uses config.rules_path
|
|
522
|
+
store.list # => [{ slug: "sql-injection", id: "...", ... }]
|
|
523
|
+
store.write("sql-injection", yaml, test_code: code, language: "ruby")
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Security
|
|
527
|
+
|
|
528
|
+
- The standalone server listens on `127.0.0.1` by default. Bind it to another interface only together with `--basic-auth` or `authorize`.
|
|
529
|
+
- **The editor runs the engine on code the user types.** Only give access to people you trust.
|
|
530
|
+
- `authorize` fails closed: a value that is neither a callable nor a `[user, password]` pair denies access.
|
|
531
|
+
- Requests that change state require the `X-Trapline: 1` header, which protects against CSRF.
|
|
532
|
+
- Rule names are checked against path traversal before touching the disk.
|
|
533
|
+
- The engine runs in a temporary directory with metrics and version checks disabled. Temporary paths are removed from error messages.
|
|
534
|
+
|
|
535
|
+
## Development
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
bundle install
|
|
539
|
+
bundle exec rake test # tests that need the engine are skipped without one
|
|
540
|
+
ruby -Ilib exe/trapline server -r tmp/rules
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
To run the server with puma inside this repository, enable the optional group first: `bundle config set --local with server && bundle install`.
|
|
544
|
+
|
|
545
|
+
Layout:
|
|
546
|
+
|
|
547
|
+
```
|
|
548
|
+
lib/trapline/
|
|
549
|
+
cli.rb command-line interface
|
|
550
|
+
configuration.rb options and environment variables
|
|
551
|
+
rule.rb parsing and dumping readable YAML
|
|
552
|
+
validator.rb structural validation (+ optional validation by the engine)
|
|
553
|
+
engine.rb opengrep/semgrep wrapper and annotation checking
|
|
554
|
+
store.rb rules on disk in the --test layout
|
|
555
|
+
suite.rb runs every rule against its test file
|
|
556
|
+
cve/ NVD 2.0 and OSV clients and the normalized record
|
|
557
|
+
templates.rb template library (CWE templates, starters, package usage)
|
|
558
|
+
templates/cwe/ one YAML file per CWE: rule body and annotated test code per language
|
|
559
|
+
runtime.rb runtime-version (container image) rules from NVD version ranges
|
|
560
|
+
generator.rb CVE -> rule drafts
|
|
561
|
+
web/app.rb Rack app: JSON API and editor
|
|
562
|
+
web/server.rb standalone server (rackup when available, built-in otherwise)
|
|
563
|
+
web/assets/ editor: plain JavaScript, with CodeMirror 5 and fonts bundled (works offline)
|
|
564
|
+
railtie.rb Rails integration
|
|
565
|
+
lib/generators/ rails generate trapline:install
|
|
566
|
+
lib/tasks/ rake trapline:test / trapline:validate
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
To add a template, create `lib/trapline/templates/cwe/cwe-<n>.yml` with the rule body and annotated test code for each language. `test/test_templates.rb` runs every template through the engine, so the new one is covered automatically.
|
|
570
|
+
|
|
571
|
+
### Releasing
|
|
572
|
+
|
|
573
|
+
Pushing a `v*` tag runs `.github/workflows/release.yml`, which checks that the tag matches `Trapline::VERSION`, runs the tests with opengrep, publishes the gem to RubyGems and creates a GitHub release with the `.gem` attached:
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
# bump lib/trapline/version.rb, commit, then:
|
|
577
|
+
git tag v0.1.0
|
|
578
|
+
git push origin v0.1.0
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
Publishing uses [RubyGems trusted publishing](https://guides.rubygems.org/trusted-publishing/), so no API key is stored in the repository. It must be set up once on rubygems.org: as a pending trusted publisher for the first release, with repository `JAugusto42/trapline`, workflow `release.yml` and environment `release`. A pending publisher expires after about 12 hours, so create it right before pushing the first tag. The workflow names the owning account (`RUBYGEMS_OWNER_ID`) when it exchanges the GitHub token, as RubyGems requires for a new gem's first push.
|
|
582
|
+
|
|
583
|
+
## License
|
|
584
|
+
|
|
585
|
+
MIT. See [LICENSE](LICENSE). CodeMirror and the bundled fonts keep their own licenses, listed in `lib/trapline/web/assets/THIRD_PARTY_LICENSES.txt`.
|
data/config.ru.example
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Runs the editor on any Rack server (puma, unicorn, passenger...).
|
|
2
|
+
#
|
|
3
|
+
# cp config.ru.example config.ru
|
|
4
|
+
# TRAPLINE_USER=admin TRAPLINE_PASSWORD=change-me bundle exec puma
|
|
5
|
+
#
|
|
6
|
+
# Your Gemfile needs `gem "trapline"` and a server such as `gem "puma"`.
|
|
7
|
+
# Inside the trapline repository, enable the optional group first:
|
|
8
|
+
# bundle config set --local with server && bundle install
|
|
9
|
+
require "trapline"
|
|
10
|
+
|
|
11
|
+
credentials = %w[TRAPLINE_USER TRAPLINE_PASSWORD].map do |name|
|
|
12
|
+
ENV.fetch(name) { abort "config.ru: set TRAPLINE_USER and TRAPLINE_PASSWORD (HTTP Basic credentials)" }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Trapline.configure do |c|
|
|
16
|
+
c.rules_path = ENV.fetch("TRAPLINE_RULES_PATH", File.expand_path("rules", __dir__))
|
|
17
|
+
c.authorize = credentials
|
|
18
|
+
# c.read_only = true # validate and run only, no writes
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
run Trapline::Web::App.new
|
data/exe/trapline
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Trapline
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
desc "Installs Trapline: initializer, editor route and rules folder"
|
|
10
|
+
|
|
11
|
+
class_option :path, type: :string, default: "/trapline", desc: "Path where the editor will be mounted"
|
|
12
|
+
|
|
13
|
+
def create_initializer
|
|
14
|
+
template "initializer.rb", "config/initializers/trapline.rb"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def mount_editor
|
|
18
|
+
route <<~RUBY.strip
|
|
19
|
+
# semgrep/opengrep rule editor. See config/initializers/trapline.rb
|
|
20
|
+
mount Trapline::Web::App.new, at: "#{options[:path]}"
|
|
21
|
+
RUBY
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_rules_dir
|
|
25
|
+
create_file "security/semgrep/.keep", ""
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def done
|
|
29
|
+
say "\nTrapline installed. Run `bin/rails server` and open #{options[:path]}", :green
|
|
30
|
+
say "Also install the engine: `pip install semgrep` or the opengrep binary."
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|