neetob 0.5.78 → 0.5.80
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 +4 -4
- data/Gemfile.lock +2 -2
- data/lib/neetob/cli/github/unused_assets_audit.rb +99 -27
- data/lib/neetob/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72c4fda7b0a63934743c86ea9c318d249f03363e95b842bfbc037a5140501df5
|
|
4
|
+
data.tar.gz: 8fa0bb5da4de2bd950651979aa1b74b4045e6d890eb8cbb4339f6a5ff8ace1fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c68a711f36a47c1066fe0b0200fd4d900f324b4992305aa91b2d3570b3d7a6b5395c1bc393fbb7b19f5c510ab4ed38b1b618ac29ac9618eb208ec49795abf3b5
|
|
7
|
+
data.tar.gz: e7c0f16354008c8b3d27af875451194fc5d488f2a10313f6551cff3199a1190f5d355d46c2ac697c2ebbe0ccbebcaaee3fa363ae788c8019f90d218e81233b5b
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
neetob (0.5.
|
|
4
|
+
neetob (0.5.80)
|
|
5
5
|
actionview
|
|
6
6
|
activesupport
|
|
7
7
|
brakeman (~> 5.0)
|
|
@@ -472,7 +472,7 @@ GEM
|
|
|
472
472
|
unicode (0.4.4.5)
|
|
473
473
|
unicode-display_width (2.6.0)
|
|
474
474
|
uniform_notifier (1.16.0)
|
|
475
|
-
uri (1.0.
|
|
475
|
+
uri (1.0.4)
|
|
476
476
|
version_gem (1.1.4)
|
|
477
477
|
webmock (3.24.0)
|
|
478
478
|
addressable (>= 2.8.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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
71
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.5.80
|
|
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-
|
|
11
|
+
date: 2026-01-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|