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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Promptmenot
4
+ VERSION = "0.1.1"
5
+ end
@@ -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)
@@ -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: []