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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +585 -0
  4. data/config.ru.example +21 -0
  5. data/exe/trapline +8 -0
  6. data/lib/generators/trapline/install_generator.rb +34 -0
  7. data/lib/generators/trapline/templates/initializer.rb +23 -0
  8. data/lib/tasks/trapline.rake +19 -0
  9. data/lib/trapline/cli.rb +212 -0
  10. data/lib/trapline/configuration.rb +19 -0
  11. data/lib/trapline/cve/client.rb +203 -0
  12. data/lib/trapline/cve/record.rb +160 -0
  13. data/lib/trapline/engine.rb +243 -0
  14. data/lib/trapline/generator.rb +236 -0
  15. data/lib/trapline/languages.rb +65 -0
  16. data/lib/trapline/railtie.rb +26 -0
  17. data/lib/trapline/rule.rb +165 -0
  18. data/lib/trapline/runtime.rb +148 -0
  19. data/lib/trapline/store.rb +113 -0
  20. data/lib/trapline/suite.rb +44 -0
  21. data/lib/trapline/templates/cwe/cwe-22.yml +167 -0
  22. data/lib/trapline/templates/cwe/cwe-327.yml +124 -0
  23. data/lib/trapline/templates/cwe/cwe-502.yml +123 -0
  24. data/lib/trapline/templates/cwe/cwe-601.yml +124 -0
  25. data/lib/trapline/templates/cwe/cwe-611.yml +87 -0
  26. data/lib/trapline/templates/cwe/cwe-78.yml +166 -0
  27. data/lib/trapline/templates/cwe/cwe-79.yml +164 -0
  28. data/lib/trapline/templates/cwe/cwe-798.yml +125 -0
  29. data/lib/trapline/templates/cwe/cwe-89.yml +189 -0
  30. data/lib/trapline/templates/cwe/cwe-917.yml +46 -0
  31. data/lib/trapline/templates/cwe/cwe-918.yml +166 -0
  32. data/lib/trapline/templates/cwe/cwe-94.yml +131 -0
  33. data/lib/trapline/templates.rb +261 -0
  34. data/lib/trapline/validator.rb +359 -0
  35. data/lib/trapline/version.rb +5 -0
  36. data/lib/trapline/web/app.rb +234 -0
  37. data/lib/trapline/web/assets/THIRD_PARTY_LICENSES.txt +214 -0
  38. data/lib/trapline/web/assets/app.css +399 -0
  39. data/lib/trapline/web/assets/app.js +804 -0
  40. data/lib/trapline/web/assets/codemirror.css +344 -0
  41. data/lib/trapline/web/assets/codemirror.js +13263 -0
  42. data/lib/trapline/web/assets/index.html +165 -0
  43. data/lib/trapline/web/assets/jetbrains-mono-latin-400-normal.woff2 +0 -0
  44. data/lib/trapline/web/assets/jetbrains-mono-latin-600-normal.woff2 +0 -0
  45. data/lib/trapline/web/assets/public-sans-latin-400-normal.woff2 +0 -0
  46. data/lib/trapline/web/assets/public-sans-latin-500-normal.woff2 +0 -0
  47. data/lib/trapline/web/assets/public-sans-latin-600-normal.woff2 +0 -0
  48. data/lib/trapline/web/assets/public-sans-latin-700-normal.woff2 +0 -0
  49. data/lib/trapline/web/server.rb +108 -0
  50. data/lib/trapline.rb +70 -0
  51. 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,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+ require "trapline"
6
+ require "trapline/cli"
7
+
8
+ exit Trapline::CLI.start(ARGV)
@@ -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