promptmenot 0.1.1
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/.rspec +3 -0
- data/.rubocop.yml +36 -0
- data/CHANGELOG.md +21 -0
- data/CONTRIBUTING.md +69 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +127 -0
- data/Rakefile +12 -0
- data/agents.md +150 -0
- data/config/locales/en.yml +4 -0
- data/lib/generators/promptmenot/install_generator.rb +17 -0
- data/lib/generators/promptmenot/templates/promptmenot.rb +27 -0
- data/lib/promptmenot/configuration.rb +54 -0
- data/lib/promptmenot/detector.rb +67 -0
- data/lib/promptmenot/errors.rb +7 -0
- data/lib/promptmenot/match.rb +36 -0
- data/lib/promptmenot/pattern.rb +66 -0
- data/lib/promptmenot/pattern_registry.rb +53 -0
- data/lib/promptmenot/patterns/base.rb +36 -0
- data/lib/promptmenot/patterns/context_manipulation.rb +63 -0
- data/lib/promptmenot/patterns/delimiter_injection.rb +81 -0
- data/lib/promptmenot/patterns/direct_instruction_override.rb +95 -0
- data/lib/promptmenot/patterns/encoding_obfuscation.rb +79 -0
- data/lib/promptmenot/patterns/indirect_injection.rb +79 -0
- data/lib/promptmenot/patterns/role_manipulation.rb +79 -0
- data/lib/promptmenot/railtie.rb +13 -0
- data/lib/promptmenot/result.rb +41 -0
- data/lib/promptmenot/sanitizer.rb +50 -0
- data/lib/promptmenot/validator.rb +39 -0
- data/lib/promptmenot/version.rb +5 -0
- data/lib/promptmenot.rb +96 -0
- data/promptmenot.gemspec +34 -0
- metadata +108 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 000e08f45a4388639584116f108d8e5d9f94f8e2d606e1f1823c82fb67645cf7
|
|
4
|
+
data.tar.gz: 0460fc97672f78cd0f16a67b0ea10139bbcc73b477eef6ab47d19802636cbc1d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8d922773aa2f10bc810bdf285a7d28bea76817ad57fdddb49b69168b0e573b2c3e1b78f9d045d457279843ee3850d4024d480997aa1bd48392cbbca002a69124
|
|
7
|
+
data.tar.gz: 7158d333c28534f1f6d49a21652497ac765c60b7df39c4a49d989dc0e97a55d38c620e84e54eeae5f85af0a858ecd56b84ca83dc0e090172687ed1f0b30e8187
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.0
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
|
|
6
|
+
Style/Documentation:
|
|
7
|
+
Enabled: false
|
|
8
|
+
|
|
9
|
+
Style/FrozenStringLiteralComment:
|
|
10
|
+
EnforcedStyle: always
|
|
11
|
+
|
|
12
|
+
Metrics/MethodLength:
|
|
13
|
+
Max: 20
|
|
14
|
+
|
|
15
|
+
Metrics/ClassLength:
|
|
16
|
+
Max: 150
|
|
17
|
+
|
|
18
|
+
Metrics/BlockLength:
|
|
19
|
+
Exclude:
|
|
20
|
+
- "spec/**/*"
|
|
21
|
+
- "promptmenot.gemspec"
|
|
22
|
+
|
|
23
|
+
Layout/LineLength:
|
|
24
|
+
Max: 120
|
|
25
|
+
Exclude:
|
|
26
|
+
- "lib/promptmenot/patterns/**/*"
|
|
27
|
+
|
|
28
|
+
Metrics/AbcSize:
|
|
29
|
+
Exclude:
|
|
30
|
+
- "lib/promptmenot/detector.rb"
|
|
31
|
+
|
|
32
|
+
Style/StringLiterals:
|
|
33
|
+
EnforcedStyle: double_quotes
|
|
34
|
+
|
|
35
|
+
Style/StringLiteralsInInterpolation:
|
|
36
|
+
EnforcedStyle: double_quotes
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-02-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Core detection engine with 6 pattern categories (~60 patterns)
|
|
8
|
+
- Direct instruction override
|
|
9
|
+
- Role manipulation
|
|
10
|
+
- Delimiter injection
|
|
11
|
+
- Encoding obfuscation
|
|
12
|
+
- Indirect injection
|
|
13
|
+
- Context manipulation
|
|
14
|
+
- Filter-based sensitivity levels: `:low`, `:medium`, `:high`, `:paranoid`
|
|
15
|
+
- Two operating modes: `:reject` (validation error) and `:sanitize` (strip content)
|
|
16
|
+
- ActiveModel validator (`prompt_safety`)
|
|
17
|
+
- Standalone API (`Promptmenot.safe?`, `.detect`, `.sanitize`)
|
|
18
|
+
- Global configuration DSL with custom pattern support
|
|
19
|
+
- Detection callbacks
|
|
20
|
+
- Rails generator (`rails g promptmenot:install`)
|
|
21
|
+
- I18n support for error messages
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Contributing to PromptMeNot
|
|
2
|
+
|
|
3
|
+
We'd love your help improving PromptMeNot! Here's how to contribute:
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/kevinl05/promptmenot.git
|
|
9
|
+
cd promptmenot
|
|
10
|
+
bundle install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Running Tests
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Run full test suite
|
|
17
|
+
bundle exec rspec
|
|
18
|
+
|
|
19
|
+
# Run specific test file
|
|
20
|
+
bundle exec rspec spec/promptmenot/detector_spec.rb
|
|
21
|
+
|
|
22
|
+
# Run with coverage
|
|
23
|
+
bundle exec rspec --coverage
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Code Quality
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Run RuboCop linter
|
|
30
|
+
bundle exec rubocop
|
|
31
|
+
|
|
32
|
+
# Auto-fix offenses
|
|
33
|
+
bundle exec rubocop -a
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Making Changes
|
|
37
|
+
|
|
38
|
+
1. **Fork** the repository on GitHub
|
|
39
|
+
2. **Create a branch** for your feature: `git checkout -b feature/my-feature`
|
|
40
|
+
3. **Make your changes** and add tests
|
|
41
|
+
4. **Ensure all tests pass**: `bundle exec rspec`
|
|
42
|
+
5. **Ensure code is clean**: `bundle exec rubocop -a`
|
|
43
|
+
6. **Commit** with clear messages: `git commit -am 'Add new pattern for X'`
|
|
44
|
+
7. **Push** to your fork: `git push origin feature/my-feature`
|
|
45
|
+
8. **Open a PR** on GitHub
|
|
46
|
+
|
|
47
|
+
## Adding New Patterns
|
|
48
|
+
|
|
49
|
+
New injection attack patterns go in `lib/promptmenot/patterns/`.
|
|
50
|
+
|
|
51
|
+
See existing pattern files for the DSL. Each pattern registers with:
|
|
52
|
+
- `name` — unique identifier
|
|
53
|
+
- `regex` — detection pattern
|
|
54
|
+
- `sensitivity` — `:low`, `:medium`, `:high`, or `:paranoid`
|
|
55
|
+
- `confidence` — `:high`, `:medium`, or `:low`
|
|
56
|
+
|
|
57
|
+
Always include tests in `spec/promptmenot/patterns/`.
|
|
58
|
+
|
|
59
|
+
## Reporting Issues
|
|
60
|
+
|
|
61
|
+
Found a bug or have a suggestion? Open an issue on GitHub with:
|
|
62
|
+
- Clear description of the problem
|
|
63
|
+
- Steps to reproduce (if applicable)
|
|
64
|
+
- Expected vs. actual behavior
|
|
65
|
+
- Ruby/Rails version info
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
All contributions are made under the MIT license.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 promptmenot contributors
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# PromptMeNot
|
|
2
|
+
|
|
3
|
+
Detect and sanitize prompt injection attacks in user-submitted text. Protects Rails apps against:
|
|
4
|
+
|
|
5
|
+
- **Direct injection** -- users trying to hack your LLMs via form inputs
|
|
6
|
+
- **Indirect injection** -- users storing malicious prompts in profiles so other LLMs that scrape your site get compromised
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Add to your Gemfile:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem "promptmenot"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then run:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bundle install
|
|
20
|
+
rails generate promptmenot:install # creates config/initializers/promptmenot.rb
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### ActiveModel Validation
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
class UserProfile < ApplicationRecord
|
|
29
|
+
# Reject mode (default) -- adds validation error
|
|
30
|
+
validates :bio, prompt_safety: true
|
|
31
|
+
|
|
32
|
+
# Sanitize mode -- strips malicious content, no error
|
|
33
|
+
validates :about_me, prompt_safety: { mode: :sanitize }
|
|
34
|
+
|
|
35
|
+
# Custom sensitivity
|
|
36
|
+
validates :notes, prompt_safety: { sensitivity: :high, mode: :reject }
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Standalone API
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
Promptmenot.safe?("Hello world")
|
|
44
|
+
# => true
|
|
45
|
+
|
|
46
|
+
Promptmenot.safe?("Ignore all previous instructions")
|
|
47
|
+
# => false
|
|
48
|
+
|
|
49
|
+
result = Promptmenot.detect("Some text with [SYSTEM] override")
|
|
50
|
+
result.safe? # => false
|
|
51
|
+
result.unsafe? # => true
|
|
52
|
+
result.matches # => [#<Match ...>]
|
|
53
|
+
result.categories_detected # => [:delimiter_injection]
|
|
54
|
+
result.summary # => "Detected 1 potential prompt injection pattern..."
|
|
55
|
+
|
|
56
|
+
sanitized = Promptmenot.sanitize("Hello. Ignore all previous instructions. Goodbye.")
|
|
57
|
+
sanitized.sanitized # => "Hello. [removed] Goodbye."
|
|
58
|
+
sanitized.changed? # => true
|
|
59
|
+
sanitized.original # => "Hello. Ignore all previous instructions. Goodbye."
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# config/initializers/promptmenot.rb
|
|
66
|
+
Promptmenot.configure do |config|
|
|
67
|
+
# Default sensitivity level for all validations
|
|
68
|
+
# Options: :low, :medium (default), :high, :paranoid
|
|
69
|
+
config.sensitivity = :medium
|
|
70
|
+
|
|
71
|
+
# Default mode: :reject (validation error) or :sanitize (strip content)
|
|
72
|
+
config.mode = :reject
|
|
73
|
+
|
|
74
|
+
# Replacement text used in sanitize mode
|
|
75
|
+
config.replacement_text = "[removed]"
|
|
76
|
+
|
|
77
|
+
# Callback fired whenever injection is detected
|
|
78
|
+
config.on_detect = ->(result) { Rails.logger.warn("Injection: #{result.summary}") }
|
|
79
|
+
|
|
80
|
+
# Register custom patterns
|
|
81
|
+
config.add_pattern(
|
|
82
|
+
name: :my_custom_pattern,
|
|
83
|
+
regex: /my dangerous regex/i,
|
|
84
|
+
category: :custom,
|
|
85
|
+
sensitivity: :medium,
|
|
86
|
+
confidence: :high
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Sensitivity Levels
|
|
92
|
+
|
|
93
|
+
Sensitivity controls which patterns are active. Each pattern declares a minimum sensitivity level -- it only runs when the requested sensitivity is at or above that level.
|
|
94
|
+
|
|
95
|
+
| Pattern sensitivity | Active at `:low` | `:medium` | `:high` | `:paranoid` |
|
|
96
|
+
|---|---|---|---|---|
|
|
97
|
+
| `:low` | Yes | Yes | Yes | Yes |
|
|
98
|
+
| `:medium` | No | Yes | Yes | Yes |
|
|
99
|
+
| `:high` | No | No | Yes | Yes |
|
|
100
|
+
| `:paranoid` | No | No | No | Yes |
|
|
101
|
+
|
|
102
|
+
**`:low`** catches only the most obvious attacks (e.g., "ignore all previous instructions"). **`:paranoid`** flags anything remotely suspicious, including mixed-script text.
|
|
103
|
+
|
|
104
|
+
## Pattern Categories
|
|
105
|
+
|
|
106
|
+
| Category | Examples | Count |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `direct_instruction_override` | "ignore previous instructions", "new instructions:" | ~12 |
|
|
109
|
+
| `role_manipulation` | "jailbreak mode", "act as unrestricted AI", "DAN" | ~10 |
|
|
110
|
+
| `delimiter_injection` | `<\|system\|>`, `[SYSTEM]`, ChatML tokens | ~10 |
|
|
111
|
+
| `encoding_obfuscation` | Base64 payloads, zero-width chars, hex escapes | ~10 |
|
|
112
|
+
| `indirect_injection` | "Dear AI", "if you are an LLM", "note to chatbot" | ~10 |
|
|
113
|
+
| `context_manipulation` | `===RESET===`, "the above is a test", prompt leaking | ~8 |
|
|
114
|
+
|
|
115
|
+
## False Positive Mitigation
|
|
116
|
+
|
|
117
|
+
Patterns use contextual qualifiers to minimize false positives:
|
|
118
|
+
|
|
119
|
+
- "ignore" alone is fine -- "ignore **previous instructions**" is flagged
|
|
120
|
+
- "act as" requires malicious qualifiers -- "act as a consultant" passes
|
|
121
|
+
- "you are now" requires AI/restriction qualifiers -- "you are now subscribed" passes
|
|
122
|
+
- "from now on" requires imperative "you must/will" -- "from now on I'll work from home" passes
|
|
123
|
+
- Broad patterns are placed at `:high`/`:paranoid` sensitivity so they don't fire at default settings
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
data/agents.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Agents Reference Guide
|
|
2
|
+
|
|
3
|
+
This document provides operational knowledge for AI agents and developers working on PromptMeNot.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
- **Project Name:** PromptMeNot
|
|
8
|
+
- **Type:** Ruby gem (Rails plugin)
|
|
9
|
+
- **Framework:** ActiveModel / ActiveSupport (>= 6.0)
|
|
10
|
+
- **Language:** Ruby >= 3.0
|
|
11
|
+
- **Package Manager:** Bundler
|
|
12
|
+
- **Test Framework:** RSpec
|
|
13
|
+
- **Linter:** RuboCop
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Getting Started
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Install dependencies
|
|
21
|
+
bundle install
|
|
22
|
+
|
|
23
|
+
# Run tests
|
|
24
|
+
bundle exec rspec
|
|
25
|
+
|
|
26
|
+
# Run linter
|
|
27
|
+
bundle exec rubocop
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Build & Release
|
|
33
|
+
|
|
34
|
+
### Build the Gem
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Build .gem file
|
|
38
|
+
gem build promptmenot.gemspec
|
|
39
|
+
|
|
40
|
+
# Install locally for testing
|
|
41
|
+
gem install promptmenot-*.gem
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Release
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Bump version in lib/promptmenot/version.rb, then:
|
|
48
|
+
gem build promptmenot.gemspec
|
|
49
|
+
gem push promptmenot-*.gem
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Architecture
|
|
55
|
+
|
|
56
|
+
### Pattern System
|
|
57
|
+
|
|
58
|
+
Patterns are registered via a DSL in `lib/promptmenot/patterns/*.rb`. Each pattern declares:
|
|
59
|
+
- **name** — unique identifier
|
|
60
|
+
- **category** — which pattern category it belongs to
|
|
61
|
+
- **regex** — the detection regex
|
|
62
|
+
- **sensitivity** — minimum sensitivity level to activate (`:low`, `:medium`, `:high`, `:paranoid`)
|
|
63
|
+
- **confidence** — how confident the match is (`:high`, `:medium`, `:low`)
|
|
64
|
+
|
|
65
|
+
### Sensitivity Levels (Filter-Based)
|
|
66
|
+
|
|
67
|
+
Each pattern declares its minimum sensitivity. At runtime, only patterns at or below the requested level are active:
|
|
68
|
+
|
|
69
|
+
| Pattern sensitivity | Active at :low | :medium | :high | :paranoid |
|
|
70
|
+
|---|---|---|---|---|
|
|
71
|
+
| :low | Yes | Yes | Yes | Yes |
|
|
72
|
+
| :medium | No | Yes | Yes | Yes |
|
|
73
|
+
| :high | No | No | Yes | Yes |
|
|
74
|
+
| :paranoid | No | No | No | Yes |
|
|
75
|
+
|
|
76
|
+
### Detection Flow
|
|
77
|
+
|
|
78
|
+
1. `Detector` receives text + sensitivity level
|
|
79
|
+
2. `PatternRegistry` filters patterns by sensitivity
|
|
80
|
+
3. Each pattern's regex runs against the text
|
|
81
|
+
4. Overlapping matches are deduplicated
|
|
82
|
+
5. `Result` object is returned (safe?/unsafe?, matches, categories)
|
|
83
|
+
|
|
84
|
+
### Modes
|
|
85
|
+
|
|
86
|
+
- **reject** — adds ActiveModel validation error (default)
|
|
87
|
+
- **sanitize** — strips matched content from the field value
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Common Scripts Reference
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Development
|
|
95
|
+
bundle install # Install dependencies
|
|
96
|
+
bundle console # Open IRB with gem loaded (if configured)
|
|
97
|
+
|
|
98
|
+
# Testing
|
|
99
|
+
bundle exec rspec # Run full test suite
|
|
100
|
+
bundle exec rspec spec/promptmenot/detector_spec.rb # Run single spec
|
|
101
|
+
|
|
102
|
+
# Linting
|
|
103
|
+
bundle exec rubocop # Run linter
|
|
104
|
+
bundle exec rubocop -a # Auto-fix offenses
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Key File Locations
|
|
110
|
+
|
|
111
|
+
| File | Purpose |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `lib/promptmenot.rb` | Root entry point, convenience API |
|
|
114
|
+
| `lib/promptmenot/version.rb` | Gem version |
|
|
115
|
+
| `lib/promptmenot/configuration.rb` | Global config DSL |
|
|
116
|
+
| `lib/promptmenot/detector.rb` | Core detection engine |
|
|
117
|
+
| `lib/promptmenot/sanitizer.rb` | Content sanitization |
|
|
118
|
+
| `lib/promptmenot/validator.rb` | ActiveModel validator |
|
|
119
|
+
| `lib/promptmenot/pattern_registry.rb` | Central pattern registry |
|
|
120
|
+
| `lib/promptmenot/patterns/` | All pattern category definitions |
|
|
121
|
+
| `lib/promptmenot/railtie.rb` | Rails auto-config |
|
|
122
|
+
| `config/locales/en.yml` | I18n error messages |
|
|
123
|
+
| `promptmenot.gemspec` | Gem specification |
|
|
124
|
+
| `spec/` | All test specs |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Troubleshooting
|
|
129
|
+
|
|
130
|
+
### Bundle Install Fails
|
|
131
|
+
|
|
132
|
+
**Symptom:** Dependency resolution errors
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Remove lockfile and retry
|
|
136
|
+
rm Gemfile.lock && bundle install
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### RSpec Can't Find Patterns
|
|
140
|
+
|
|
141
|
+
**Symptom:** Tests pass but no patterns are detected
|
|
142
|
+
|
|
143
|
+
Check that all pattern files in `lib/promptmenot/patterns/` are required in `lib/promptmenot.rb`.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Related Documentation
|
|
148
|
+
|
|
149
|
+
- `README.md` - Usage examples, configuration guide, pattern reference
|
|
150
|
+
- `CHANGELOG.md` - Version history
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Promptmenot
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Creates a Promptmenot initializer in your application."
|
|
11
|
+
|
|
12
|
+
def copy_initializer
|
|
13
|
+
template "promptmenot.rb", "config/initializers/promptmenot.rb"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Promptmenot.configure do |config|
|
|
4
|
+
# Default sensitivity level for all validations.
|
|
5
|
+
# Options: :low, :medium (default), :high, :paranoid
|
|
6
|
+
# config.sensitivity = :medium
|
|
7
|
+
|
|
8
|
+
# Default mode for the prompt_safety validator.
|
|
9
|
+
# :reject — adds a validation error (default)
|
|
10
|
+
# :sanitize — strips matched content from the field
|
|
11
|
+
# config.mode = :reject
|
|
12
|
+
|
|
13
|
+
# Replacement text used in sanitize mode.
|
|
14
|
+
# config.replacement_text = "[removed]"
|
|
15
|
+
|
|
16
|
+
# Callback fired whenever an injection is detected.
|
|
17
|
+
# config.on_detect = ->(result) { Rails.logger.warn("Prompt injection: #{result.summary}") }
|
|
18
|
+
|
|
19
|
+
# Register custom patterns:
|
|
20
|
+
# config.add_pattern(
|
|
21
|
+
# name: :my_custom_pattern,
|
|
22
|
+
# regex: /my custom regex/i,
|
|
23
|
+
# category: :custom,
|
|
24
|
+
# sensitivity: :medium,
|
|
25
|
+
# confidence: :high
|
|
26
|
+
# )
|
|
27
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Promptmenot
|
|
4
|
+
class Configuration
|
|
5
|
+
VALID_SENSITIVITIES = %i[low medium high paranoid].freeze
|
|
6
|
+
VALID_MODES = %i[reject sanitize].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :sensitivity, :mode
|
|
9
|
+
attr_accessor :replacement_text, :on_detect, :max_length
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@sensitivity = :medium
|
|
13
|
+
@mode = :reject
|
|
14
|
+
@replacement_text = "[removed]"
|
|
15
|
+
@max_length = 50_000
|
|
16
|
+
@custom_patterns_list = []
|
|
17
|
+
@custom_patterns = nil
|
|
18
|
+
@on_detect = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def sensitivity=(value)
|
|
22
|
+
sym = value.to_sym
|
|
23
|
+
unless VALID_SENSITIVITIES.include?(sym)
|
|
24
|
+
raise ConfigurationError, "Invalid sensitivity: #{value}. Must be one of: #{VALID_SENSITIVITIES.join(", ")}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@sensitivity = sym
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def mode=(value)
|
|
31
|
+
sym = value.to_sym
|
|
32
|
+
unless VALID_MODES.include?(sym)
|
|
33
|
+
raise ConfigurationError, "Invalid mode: #{value}. Must be one of: #{VALID_MODES.join(", ")}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@mode = sym
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def custom_patterns
|
|
40
|
+
@custom_patterns ||= @custom_patterns_list.dup.freeze
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add_pattern(name:, regex:, category: :custom, sensitivity: :medium, confidence: :medium)
|
|
44
|
+
@custom_patterns_list << Pattern.new(
|
|
45
|
+
name: name,
|
|
46
|
+
category: category,
|
|
47
|
+
regex: regex,
|
|
48
|
+
sensitivity: sensitivity,
|
|
49
|
+
confidence: confidence
|
|
50
|
+
)
|
|
51
|
+
@custom_patterns = nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Promptmenot
|
|
4
|
+
class Detector
|
|
5
|
+
attr_reader :sensitivity, :categories
|
|
6
|
+
|
|
7
|
+
def initialize(sensitivity: nil, categories: nil)
|
|
8
|
+
@sensitivity = sensitivity || Promptmenot.configuration.sensitivity
|
|
9
|
+
@categories = categories
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def detect(text)
|
|
13
|
+
return Result.new(text: text.to_s) if text.nil? || text.to_s.strip.empty?
|
|
14
|
+
|
|
15
|
+
input = text.to_s
|
|
16
|
+
max = Promptmenot.configuration.max_length
|
|
17
|
+
input = input[0, max] if max && input.length > max
|
|
18
|
+
|
|
19
|
+
patterns = Promptmenot.registry.for_sensitivity_and_categories(
|
|
20
|
+
@sensitivity,
|
|
21
|
+
categories: @categories
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
all_matches = patterns.flat_map { |pattern| pattern.match(input) }
|
|
25
|
+
deduped = deduplicate(all_matches)
|
|
26
|
+
|
|
27
|
+
result = Result.new(text: input, matches: deduped)
|
|
28
|
+
fire_callback(result) if result.unsafe?
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def deduplicate(matches)
|
|
35
|
+
return matches if matches.size <= 1
|
|
36
|
+
|
|
37
|
+
sorted = matches.sort_by { |m| [m.position.begin, -m.position.size] }
|
|
38
|
+
kept = []
|
|
39
|
+
|
|
40
|
+
sorted.each do |match|
|
|
41
|
+
existing = kept.find { |m| overlaps?(m, match) }
|
|
42
|
+
if existing
|
|
43
|
+
# Keep the larger match when overlapping
|
|
44
|
+
if match.position.size > existing.position.size
|
|
45
|
+
kept.delete(existing)
|
|
46
|
+
kept << match
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
kept << match
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
kept
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def overlaps?(first, second)
|
|
57
|
+
first.position.begin < second.position.end && second.position.begin < first.position.end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def fire_callback(result)
|
|
61
|
+
callback = Promptmenot.configuration.on_detect
|
|
62
|
+
callback&.call(result)
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
warn "[Promptmenot] on_detect callback raised #{e.class}: #{e.message}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Promptmenot
|
|
4
|
+
class Match
|
|
5
|
+
attr_reader :pattern, :matched_text, :position
|
|
6
|
+
|
|
7
|
+
def initialize(pattern:, matched_text:, position:)
|
|
8
|
+
@pattern = pattern
|
|
9
|
+
@matched_text = matched_text
|
|
10
|
+
@position = position
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def category
|
|
14
|
+
pattern.category
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def pattern_name
|
|
18
|
+
pattern.name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def confidence
|
|
22
|
+
pattern.confidence
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def sensitivity
|
|
26
|
+
pattern.sensitivity
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ==(other)
|
|
30
|
+
other.is_a?(Match) &&
|
|
31
|
+
pattern_name == other.pattern_name &&
|
|
32
|
+
matched_text == other.matched_text &&
|
|
33
|
+
position == other.position
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|