neetob 0.5.77 → 0.5.79

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38f4190969749b22189e7f491776645f1b6f7509b35e97d1f68f45c1bab8361e
4
- data.tar.gz: ad53de0451c0ad953e7990d88ddab2498695ac273fc56f1d23d3d7ba92dc1608
3
+ metadata.gz: 121e598b9a91b4cde10c55575ba42e3e96a533707af45cadc59a579dbd273d73
4
+ data.tar.gz: e42fbf46b4b66d653b2ebcfeed16e507003e13aae0dac2cbdb3e4e1e0cd58c68
5
5
  SHA512:
6
- metadata.gz: bf8552462eecc28c138af9a2ce13c3e02c4e1aa45d371dfee948056e1ecb1bb6cd1d6e168c330c1fd9e15bb8f190ce087932945189b761ba3a7a98c835efd720
7
- data.tar.gz: 5b66f30689ae4ff35a03f93ce07763001798cc3a9699ade96827e35bb165a9f9112c53035c283e82c10dafb07d532854e683534a6935b89817f5387a3f87a650
6
+ metadata.gz: db9067e13c05726c3b40ca69f975ac27acc595982cf1ece44597d2b693c897e62b9d4a72823faf582831435217e309bbbfadc32d19b59fc2488b1cfa6623cd22
7
+ data.tar.gz: 215d568537bc0ca7c8137f8dd62e8b1d21e9b3689f2d293c63692592b99e8bd6c1071ed249a701ccc04dd308d245173545425336ff728a566ded0309c16f0073
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- neetob (0.5.77)
4
+ neetob (0.5.79)
5
5
  actionview
6
6
  activesupport
7
7
  brakeman (~> 5.0)
@@ -6,6 +6,11 @@ module Neetob
6
6
  module Github
7
7
  class UnusedAssetsAudit < MakePr::Base
8
8
  DESCRIPTION = "Fix security vulnerabilities reported by unused assets audit"
9
+ TEXT_FILE_EXTENSIONS = %w[
10
+ .rb .rake .js .jsx .ts .tsx .css .scss .sass .less
11
+ .html .erb .haml .slim .json .yml .yaml .md .txt
12
+ ].freeze
13
+
9
14
  attr_accessor :repos, :sandbox
10
15
 
11
16
  def initialize(repos, sandbox = false)
@@ -21,10 +26,9 @@ module Neetob
21
26
  begin
22
27
  ui.info("\nWorking on repo #{repo}", print_to_audit_log: false)
23
28
  clone_repo_in_tmp_dir(repo)
24
- image_dir = "/tmp/neetob/#{repo_name_without_org_suffix(repo)}/app/assets/images"
25
- src_dir = "/tmp/neetob/#{repo_name_without_org_suffix(repo)}/app/javascript/src"
26
- views_dir = "/tmp/neetob/#{repo_name_without_org_suffix(repo)}/app/views"
27
- report = find_unused_images(image_dir, src_dir, views_dir)
29
+ repo_root = "/tmp/neetob/#{repo_name_without_org_suffix(repo)}"
30
+ image_dir = "#{repo_root}/app/assets/images"
31
+ report = find_unused_images(repo_root, image_dir)
28
32
  ui.success("Successfully executed unused assets audit for #{repo}", print_to_audit_log: false)
29
33
  rescue StandardError => e
30
34
  ExceptionHandler.new(e).process
@@ -42,46 +46,114 @@ module Neetob
42
46
  Dir.glob("#{dir_path}/**/*").select { |file| File.file?(file) }
43
47
  end
44
48
 
45
- def find_unused_images(image_dir, src_dir, views_dir)
49
+ def directories_to_scan(repo_root)
50
+ [
51
+ "#{repo_root}/app/javascript/src",
52
+ "#{repo_root}/app/javascript/stylesheets",
53
+ "#{repo_root}/app/views",
54
+ "#{repo_root}/app/services",
55
+ "#{repo_root}/test",
56
+ "#{repo_root}/lib/tasks"
57
+ ].select { |dir| Dir.exist?(dir) }
58
+ end
59
+
60
+ def find_unused_images(repo_root, image_dir)
46
61
  images = Set.new(list_image_files(image_dir))
47
- images = filter_used_images(src_dir, images)
48
- images = filter_used_images(views_dir, images, true)
62
+ directories_to_scan(repo_root).each do |dir|
63
+ images = filter_used_images(dir, images)
64
+ end
49
65
  images.to_a
50
66
  end
51
67
 
52
- def filter_used_images(dir, images, is_views_path = false)
68
+ def filter_used_images(dir, images)
53
69
  new_images_set = Set.new(images)
54
70
  Dir.glob("#{dir}/**/*") do |file|
55
71
  next unless File.file?(file)
72
+ next unless text_file?(file)
73
+
74
+ file_content = read_file_safely(file)
75
+ next if file_content.nil?
56
76
 
57
- File.open(file, "r") do |file_content|
58
- new_images_set.each do |image_file_path|
59
- destructured_image_path = split_image_path(is_views_path, image_file_path)
60
- if image_file_imported(file_content.read, destructured_image_path, is_views_path)
61
- new_images_set.delete(image_file_path)
62
- end
63
- file_content.rewind
77
+ new_images_set.each do |image_file_path|
78
+ if image_used_in_file?(file_content, image_file_path)
79
+ new_images_set.delete(image_file_path)
64
80
  end
65
81
  end
66
82
  end
67
83
  new_images_set
68
84
  end
69
85
 
70
- def image_file_imported(file, image_path, is_views_path)
71
- if is_views_path
72
- regex = /"#{Regexp.escape(image_path)}"/
73
- else
74
- regex = /import .* from ".*#{Regexp.escape(image_path)}.*";/
75
- end
76
- file.match(regex)
86
+ def text_file?(file)
87
+ TEXT_FILE_EXTENSIONS.include?(File.extname(file).downcase)
77
88
  end
78
89
 
79
- def split_image_path(is_views_path, image_file_path)
80
- if is_views_path
81
- image_file_path.split("images/").last
82
- else
83
- image_file_path[image_file_path.index("images")..image_file_path.rindex(".") - 1]
90
+ def read_file_safely(file)
91
+ File.read(file, encoding: "UTF-8", invalid: :replace, undef: :replace)
92
+ rescue StandardError
93
+ nil
94
+ end
95
+
96
+ def image_used_in_file?(file_content, image_file_path)
97
+ # Extract relative path from images directory (e.g., "sample_data/camel.jpeg")
98
+ relative_path = image_file_path.split("images/").last
99
+ filename = File.basename(image_file_path)
100
+ filename_without_ext = File.basename(image_file_path, ".*")
101
+ extension = File.extname(image_file_path)
102
+ parent_dir = File.dirname(relative_path)
103
+
104
+ # Pattern 1: Direct filename reference (e.g., "camel.jpeg", "landingBg.png")
105
+ return true if file_content.include?(filename)
106
+
107
+ # Pattern 2: Relative path reference (e.g., "sample_data/camel.jpeg")
108
+ return true if file_content.include?(relative_path)
109
+
110
+ # Pattern 3: Import statement without extension (e.g., import from "images/sample_data/camel")
111
+ # Skip for short numeric filenames (like "1", "2", "3") to avoid false positives with paths like "i18next"
112
+ # The filename must be at the end of the path (after a "/" or at the start) to avoid matching substrings
113
+ # e.g., "user" should not match "UserContext" in import paths
114
+ unless filename_without_ext.match?(/^\d{1,2}$/)
115
+ return true if file_content.match?(/import .* from "(.*\/)?#{Regexp.escape(filename_without_ext)}";/)
84
116
  end
117
+
118
+ # Pattern 4: CSS url() with relative path containing the filename
119
+ return true if file_content.match?(/url\(["']?[^)"']*#{Regexp.escape(filename)}["']?\)/)
120
+
121
+ # Pattern 5: Rails.root.join with split arguments (e.g., Rails.root.join("app", "assets", "images", "sample_data"))
122
+ # Check if the parent directory or filename is referenced in Rails.root.join calls
123
+ if parent_dir != "."
124
+ return true if file_content.match?(/Rails\.root\.join\([^)]*["']#{Regexp.escape(parent_dir)}["'][^)]*\)/)
125
+ end
126
+ return true if file_content.match?(/Rails\.root\.join\([^)]*["']#{Regexp.escape(filename)}["'][^)]*\)/)
127
+
128
+ # Pattern 6: Rails.root.join with single path argument containing the path
129
+ rails_join_path_regex = /Rails\.root\.join\(["'][^"']*#{Regexp.escape(relative_path)}[^"']*["']\)/
130
+ return true if file_content.match?(rails_join_path_regex)
131
+
132
+ # Pattern 7: File.read/File.open with path containing the filename or relative path
133
+ return true if file_content.match?(/File\.(read|open)\(["'][^"']*#{Regexp.escape(filename)}[^"']*["']/)
134
+ return true if file_content.match?(/File\.(read|open)\([^)]*#{Regexp.escape(relative_path)}/)
135
+
136
+ # Pattern 8: Dynamic path construction with interpolation
137
+ # e.g., "app/assets/images/sample/#{[1, 2, 3].sample}.jpg" should match "sample/1.jpg"
138
+ # Check if parent directory is referenced with string interpolation for numbered files
139
+ if parent_dir != "." && filename_without_ext.match?(/^\d+$/)
140
+ # For files like "1.jpg", "2.jpg" in a directory, check for interpolation patterns
141
+ # Allow any path prefix before the parent directory (e.g., "app/assets/images/sample/...")
142
+ escaped_dir = Regexp.escape(parent_dir)
143
+ escaped_ext = Regexp.escape(extension)
144
+ interpolation_with_prefix = /["'][^"']*\/#{escaped_dir}\/\#\{.*\}#{escaped_ext}["']/
145
+ return true if file_content.match?(interpolation_with_prefix)
146
+
147
+ # Also match when parent_dir is at the start of the path (e.g., "sample/#{...}.jpg")
148
+ interpolation_at_start = /["']#{escaped_dir}\/\#\{.*\}#{escaped_ext}["']/
149
+ return true if file_content.match?(interpolation_at_start)
150
+ end
151
+
152
+ # Pattern 9: image_path.join with filename (used in sample data loaders)
153
+ # e.g., image_path.join("camel.jpeg")
154
+ return true if file_content.match?(/\.join\(["']#{Regexp.escape(filename)}["']\)/)
155
+
156
+ false
85
157
  end
86
158
  end
87
159
  end
data/lib/neetob/cli.rb CHANGED
@@ -27,6 +27,12 @@ module Neetob
27
27
  super
28
28
  end
29
29
 
30
+ desc "version", "Display version"
31
+ map %w[--version -v] => :version
32
+ def version
33
+ puts Neetob::VERSION
34
+ end
35
+
30
36
  desc "heroku", "Interact with any resource in Heroku"
31
37
  subcommand "heroku", Heroku::Commands
32
38
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Neetob
4
- VERSION = "0.5.77"
4
+ VERSION = "0.5.79"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: neetob
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.77
4
+ version: 0.5.79
5
5
  platform: ruby
6
6
  authors:
7
7
  - Udai Gupta
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-23 00:00:00.000000000 Z
11
+ date: 2026-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor