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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Promptmenot
|
|
4
|
+
class Sanitizer
|
|
5
|
+
SanitizeResult = Struct.new(:original, :sanitized, :matches, :changed?, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
attr_reader :sensitivity, :categories, :replacement
|
|
8
|
+
|
|
9
|
+
def initialize(sensitivity: nil, categories: nil, replacement: nil)
|
|
10
|
+
@sensitivity = sensitivity || Promptmenot.configuration.sensitivity
|
|
11
|
+
@categories = categories
|
|
12
|
+
@replacement = replacement || Promptmenot.configuration.replacement_text
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def sanitize(text)
|
|
16
|
+
return SanitizeResult.new(original: text.to_s, sanitized: text.to_s, matches: [], changed?: false) if text.nil?
|
|
17
|
+
|
|
18
|
+
detector = Detector.new(sensitivity: @sensitivity, categories: @categories)
|
|
19
|
+
result = detector.detect(text)
|
|
20
|
+
|
|
21
|
+
return SanitizeResult.new(original: text.to_s, sanitized: text.to_s, matches: [], changed?: false) if result.safe?
|
|
22
|
+
|
|
23
|
+
cleaned = remove_matches(text.to_s, result.matches)
|
|
24
|
+
|
|
25
|
+
SanitizeResult.new(
|
|
26
|
+
original: text.to_s,
|
|
27
|
+
sanitized: cleaned,
|
|
28
|
+
matches: result.matches,
|
|
29
|
+
changed?: true
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def remove_matches(text, matches)
|
|
36
|
+
result = text.dup
|
|
37
|
+
sorted = matches.sort_by { |m| -m.position.begin }
|
|
38
|
+
|
|
39
|
+
sorted.each do |match|
|
|
40
|
+
result[match.position] = @replacement
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
normalize_whitespace(result)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def normalize_whitespace(text)
|
|
47
|
+
text.gsub(/[[:blank:]]{2,}/, " ").gsub(/(\n\s*){3,}/, "\n\n").strip
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
|
|
5
|
+
class PromptSafetyValidator < ActiveModel::EachValidator
|
|
6
|
+
def validate_each(record, attribute, value)
|
|
7
|
+
return if value.nil? || value.to_s.strip.empty?
|
|
8
|
+
|
|
9
|
+
sensitivity = options.fetch(:sensitivity, Promptmenot.configuration.sensitivity)
|
|
10
|
+
mode = options.fetch(:mode, Promptmenot.configuration.mode)
|
|
11
|
+
|
|
12
|
+
case mode.to_sym
|
|
13
|
+
when :reject
|
|
14
|
+
validate_reject(record, attribute, value, sensitivity)
|
|
15
|
+
when :sanitize
|
|
16
|
+
validate_sanitize(record, attribute, value, sensitivity)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def validate_reject(record, attribute, value, sensitivity)
|
|
23
|
+
detector = Promptmenot::Detector.new(sensitivity: sensitivity)
|
|
24
|
+
result = detector.detect(value)
|
|
25
|
+
return if result.safe?
|
|
26
|
+
|
|
27
|
+
message = options[:message] || :prompt_injection_detected
|
|
28
|
+
record.errors.add(attribute, message)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_sanitize(record, attribute, value, sensitivity)
|
|
32
|
+
replacement = options.fetch(:replacement, Promptmenot.configuration.replacement_text)
|
|
33
|
+
sanitizer = Promptmenot::Sanitizer.new(sensitivity: sensitivity, replacement: replacement)
|
|
34
|
+
sanitize_result = sanitizer.sanitize(value)
|
|
35
|
+
return unless sanitize_result.changed?
|
|
36
|
+
|
|
37
|
+
record.send(:"#{attribute}=", sanitize_result.sanitized)
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/promptmenot.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require "active_support/lazy_load_hooks"
|
|
5
|
+
|
|
6
|
+
require_relative "promptmenot/version"
|
|
7
|
+
require_relative "promptmenot/errors"
|
|
8
|
+
require_relative "promptmenot/pattern"
|
|
9
|
+
require_relative "promptmenot/match"
|
|
10
|
+
require_relative "promptmenot/result"
|
|
11
|
+
require_relative "promptmenot/configuration"
|
|
12
|
+
require_relative "promptmenot/pattern_registry"
|
|
13
|
+
require_relative "promptmenot/patterns/base"
|
|
14
|
+
require_relative "promptmenot/patterns/direct_instruction_override"
|
|
15
|
+
require_relative "promptmenot/patterns/role_manipulation"
|
|
16
|
+
require_relative "promptmenot/patterns/delimiter_injection"
|
|
17
|
+
require_relative "promptmenot/patterns/encoding_obfuscation"
|
|
18
|
+
require_relative "promptmenot/patterns/indirect_injection"
|
|
19
|
+
require_relative "promptmenot/patterns/context_manipulation"
|
|
20
|
+
require_relative "promptmenot/detector"
|
|
21
|
+
require_relative "promptmenot/sanitizer"
|
|
22
|
+
require_relative "promptmenot/validator"
|
|
23
|
+
|
|
24
|
+
module Promptmenot
|
|
25
|
+
@monitor = Monitor.new
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def configuration
|
|
29
|
+
@monitor.synchronize { @configuration ||= Configuration.new }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def configure
|
|
33
|
+
@monitor.synchronize do
|
|
34
|
+
yield(configuration)
|
|
35
|
+
register_custom_patterns
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def registry
|
|
40
|
+
@monitor.synchronize { @registry ||= build_registry }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset!
|
|
44
|
+
@monitor.synchronize do
|
|
45
|
+
@configuration = Configuration.new
|
|
46
|
+
@registry = nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def root
|
|
51
|
+
File.expand_path("..", __dir__)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Convenience API
|
|
55
|
+
|
|
56
|
+
def safe?(text, sensitivity: nil)
|
|
57
|
+
detect(text, sensitivity: sensitivity).safe?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def detect(text, sensitivity: nil)
|
|
61
|
+
Detector.new(sensitivity: sensitivity).detect(text)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def sanitize(text, sensitivity: nil, replacement: nil)
|
|
65
|
+
Sanitizer.new(sensitivity: sensitivity, replacement: replacement).sanitize(text)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def build_registry
|
|
71
|
+
reg = PatternRegistry.new
|
|
72
|
+
pattern_classes.each { |klass| reg.register_all(klass.patterns) }
|
|
73
|
+
register_custom_patterns(reg)
|
|
74
|
+
reg
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def pattern_classes
|
|
78
|
+
[
|
|
79
|
+
Patterns::DirectInstructionOverride,
|
|
80
|
+
Patterns::RoleManipulation,
|
|
81
|
+
Patterns::DelimiterInjection,
|
|
82
|
+
Patterns::EncodingObfuscation,
|
|
83
|
+
Patterns::IndirectInjection,
|
|
84
|
+
Patterns::ContextManipulation
|
|
85
|
+
]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def register_custom_patterns(reg = @registry)
|
|
89
|
+
return unless reg
|
|
90
|
+
|
|
91
|
+
configuration.custom_patterns.each { |p| reg.register(p) }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
require_relative "promptmenot/railtie" if defined?(Rails::Railtie)
|
data/promptmenot.gemspec
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/promptmenot/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "promptmenot"
|
|
7
|
+
spec.version = Promptmenot::VERSION
|
|
8
|
+
spec.authors = ["promptmenot contributors"]
|
|
9
|
+
spec.email = []
|
|
10
|
+
|
|
11
|
+
spec.summary = "Detect and sanitize prompt injection attacks in user-submitted text"
|
|
12
|
+
spec.description = "A Ruby on Rails gem that detects and sanitizes prompt injection attacks. " \
|
|
13
|
+
"Protects against direct injection (users hacking your LLMs via form inputs) " \
|
|
14
|
+
"and indirect injection (malicious prompts stored for other LLMs to scrape)."
|
|
15
|
+
spec.homepage = "https://github.com/kevinl05/promptmenot"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = ">= 3.0"
|
|
18
|
+
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
21
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
22
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
26
|
+
(File.expand_path(f) == __FILE__) ||
|
|
27
|
+
f.start_with?("spec/", "test/", ".git", ".github", "bin/")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
spec.require_paths = ["lib"]
|
|
31
|
+
|
|
32
|
+
spec.add_dependency "activemodel", "~> 6.0"
|
|
33
|
+
spec.add_dependency "activesupport", "~> 6.0"
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: promptmenot
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- promptmenot contributors
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-18 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activemodel
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '6.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '6.0'
|
|
41
|
+
description: A Ruby on Rails gem that detects and sanitizes prompt injection attacks.
|
|
42
|
+
Protects against direct injection (users hacking your LLMs via form inputs) and
|
|
43
|
+
indirect injection (malicious prompts stored for other LLMs to scrape).
|
|
44
|
+
email: []
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- ".rspec"
|
|
50
|
+
- ".rubocop.yml"
|
|
51
|
+
- CHANGELOG.md
|
|
52
|
+
- CONTRIBUTING.md
|
|
53
|
+
- Gemfile
|
|
54
|
+
- LICENSE.txt
|
|
55
|
+
- README.md
|
|
56
|
+
- Rakefile
|
|
57
|
+
- agents.md
|
|
58
|
+
- config/locales/en.yml
|
|
59
|
+
- lib/generators/promptmenot/install_generator.rb
|
|
60
|
+
- lib/generators/promptmenot/templates/promptmenot.rb
|
|
61
|
+
- lib/promptmenot.rb
|
|
62
|
+
- lib/promptmenot/configuration.rb
|
|
63
|
+
- lib/promptmenot/detector.rb
|
|
64
|
+
- lib/promptmenot/errors.rb
|
|
65
|
+
- lib/promptmenot/match.rb
|
|
66
|
+
- lib/promptmenot/pattern.rb
|
|
67
|
+
- lib/promptmenot/pattern_registry.rb
|
|
68
|
+
- lib/promptmenot/patterns/base.rb
|
|
69
|
+
- lib/promptmenot/patterns/context_manipulation.rb
|
|
70
|
+
- lib/promptmenot/patterns/delimiter_injection.rb
|
|
71
|
+
- lib/promptmenot/patterns/direct_instruction_override.rb
|
|
72
|
+
- lib/promptmenot/patterns/encoding_obfuscation.rb
|
|
73
|
+
- lib/promptmenot/patterns/indirect_injection.rb
|
|
74
|
+
- lib/promptmenot/patterns/role_manipulation.rb
|
|
75
|
+
- lib/promptmenot/railtie.rb
|
|
76
|
+
- lib/promptmenot/result.rb
|
|
77
|
+
- lib/promptmenot/sanitizer.rb
|
|
78
|
+
- lib/promptmenot/validator.rb
|
|
79
|
+
- lib/promptmenot/version.rb
|
|
80
|
+
- promptmenot.gemspec
|
|
81
|
+
homepage: https://github.com/kevinl05/promptmenot
|
|
82
|
+
licenses:
|
|
83
|
+
- MIT
|
|
84
|
+
metadata:
|
|
85
|
+
homepage_uri: https://github.com/kevinl05/promptmenot
|
|
86
|
+
source_code_uri: https://github.com/kevinl05/promptmenot
|
|
87
|
+
changelog_uri: https://github.com/kevinl05/promptmenot/blob/main/CHANGELOG.md
|
|
88
|
+
rubygems_mfa_required: 'true'
|
|
89
|
+
post_install_message:
|
|
90
|
+
rdoc_options: []
|
|
91
|
+
require_paths:
|
|
92
|
+
- lib
|
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '3.0'
|
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
requirements: []
|
|
104
|
+
rubygems_version: 3.4.10
|
|
105
|
+
signing_key:
|
|
106
|
+
specification_version: 4
|
|
107
|
+
summary: Detect and sanitize prompt injection attacks in user-submitted text
|
|
108
|
+
test_files: []
|