neetob 0.5.83 → 0.5.85

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.

Potentially problematic release.


This version of neetob might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 580fe72d0904ef8524725b4f9e1bbc41e9347708e7559be5f843bcc848fb7a82
4
- data.tar.gz: 0edab938a9ea750dd2dd8e57415d05983de862e0d778b0e708fcacb1d756cea7
3
+ metadata.gz: 16306ac8fba41db1469e0138632f44a7ff12013c144817937924635899f7f00b
4
+ data.tar.gz: 3fd69cacef27a582301e59efd916fdc35a4a1217ea5c4411f42e4bcb4415f19c
5
5
  SHA512:
6
- metadata.gz: d849c140be635bef936f785010a5077cecb1e407eeaa3ed065247a5d15c73cbc7b30d8c576ec0826e4ba0d6dd93f8d33e9689af9a1cf553f09ab7e62d4b27e2a
7
- data.tar.gz: a07fd1d571e3b671821eaac254406aa56281857ca63b817c1343e1b00505f4c423f39dc1626297d362b14bcffcc99d31c776c416503eae1b33ece975a99dda2a
6
+ metadata.gz: b0f4d24a0a4541da437e892fb66c3eaf1d360e4c54c550e472da7ef4c3cba386867cf168289532ddfd1a2e43a51ba5c666a9a5cb1d398fdf075918b2af9f6aac
7
+ data.tar.gz: a8268492066a76bbd9234284c6009a7c0aebf40b2052fed58566e3199a7ce081aa69c5a5ba08a6976ce8097f3669f1d4c1d324a1d695a00366d92a6ad49ee8b7
data/Gemfile.lock CHANGED
@@ -1,11 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- neetob (0.5.83)
4
+ neetob (0.5.85)
5
5
  actionview
6
6
  activesupport
7
7
  brakeman (~> 5.0)
8
8
  chronic
9
+ csv
9
10
  dotenv (~> 2.8.1)
10
11
  launchy (~> 2.5.0)
11
12
  neeto-compliance
@@ -172,6 +173,7 @@ GEM
172
173
  unicode (>= 0.4.4.5)
173
174
  css_parser (1.21.0)
174
175
  addressable
176
+ csv (3.3.5)
175
177
  database_cleaner (2.1.0)
176
178
  database_cleaner-active_record (>= 2, < 3)
177
179
  database_cleaner-active_record (2.2.0)
data/README.md CHANGED
@@ -87,7 +87,7 @@ The commands within `neetob` should be used with caution, as improper usage may
87
87
 
88
88
  ## Source of truth
89
89
 
90
- This [list of repos](https://github.com/bigbinary/neeto-compliance/blob/main/data/neeto_repos.json) is used as the "source of truth".
90
+ This [list of repos](https://github.com/neetozone/neeto-compliance/blob/main/data/neeto_repos.json) is used as the "source of truth".
91
91
 
92
92
  ## Passing list of heroku apps as option
93
93
 
@@ -241,7 +241,7 @@ neetob github protect_branch --branch main --path ~/Desktop/branch-protection-ru
241
241
 
242
242
  We can also pass the value `all` to the option `--repos` with the above mentioned command so that the
243
243
  branch protection rules can be updated for
244
- all [neeto repos](https://github.com/bigbinary/neeto-compliance/blob/main/data/neeto_repos.json).
244
+ all [neeto repos](https://github.com/neetozone/neeto-compliance/blob/main/data/neeto_repos.json).
245
245
 
246
246
  ```sh
247
247
  neetob github protect_branch --branch main --path ~/Desktop/branch-protection-rules.json \
data/docs/development.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Clone the repository onto your system using the following command:
4
4
 
5
5
  ```sh
6
- git clone https://github.com/bigbinary/neetob.git
6
+ git clone https://github.com/neetozone/neetob.git
7
7
  ```
8
8
 
9
9
  Navigate to the root of the application directory.
data/docs/testing.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Testing instructions
2
2
 
3
- For testing `github` commands use - [neeto-dummy](https://github.com/bigbinary/neeto-dummy) repo.
3
+ For testing `github` commands use - [neeto-dummy](https://github.com/neetozone/neeto-dummy) repo.
4
4
 
5
5
  For testing `heroku` commands use - [neeto-dummy](https://dashboard.heroku.com/apps/neeto-dummy) app.
6
6
 
@@ -9,7 +9,7 @@ module Neetob
9
9
  class CLI::Base
10
10
  include Utils
11
11
 
12
- NEETO_APPS_LIST_LINK = "https://github.com/bigbinary/neeto-compliance/blob/main/data/neeto_repos.json#L2"
12
+ NEETO_APPS_LIST_LINK = "https://github.com/neetozone/neeto-compliance/blob/main/data/neeto_repos.json#L2"
13
13
 
14
14
  attr_reader :ui
15
15
 
@@ -142,7 +142,7 @@ module Neetob
142
142
  end
143
143
 
144
144
  def testing_apps(platform_name)
145
- [:heroku, :neetodeploy].include?(platform_name) ? ["neeto-dummy"] : ["bigbinary/neeto-dummy"]
145
+ [:heroku, :neetodeploy].include?(platform_name) ? ["neeto-dummy"] : ["neetozone/neeto-dummy"]
146
146
  end
147
147
 
148
148
  def inform_about_current_working_mode(sandbox_mode, quiet = false)
@@ -6,7 +6,7 @@ module Neetob
6
6
  class CLI
7
7
  module Cloudflare
8
8
  class Base < CLI::Base
9
- NEETO_DEPLOY_DOCS = "https://github.com/bigbinary/neetob/#working-with-neetodeploy"
9
+ NEETO_DEPLOY_DOCS = "https://github.com/neetozone/neetob/#working-with-neetodeploy"
10
10
 
11
11
  ZONE_IDS = {
12
12
  "neetoauth.com": "81ade5aa3e075489533f903dc03b1e88",
@@ -41,7 +41,7 @@ module Neetob
41
41
  end
42
42
 
43
43
  def clone_repo(repo_name)
44
- `git clone git@github.com:bigbinary/#{repo_name}.git`
44
+ `git clone git@github.com:neetozone/#{repo_name}.git`
45
45
  if $?.success?
46
46
  puts "------Done cloning #{repo_name}------"
47
47
  else
@@ -23,7 +23,7 @@ module Neetob
23
23
  matching_gems.each do |gem|
24
24
  ui.info("\nWorking on #{gem}\n")
25
25
  begin
26
- shallow_clone_repo_in_tmp_dir!("bigbinary/#{gem}")
26
+ shallow_clone_repo_in_tmp_dir!("neetozone/#{gem}")
27
27
  build_gem(gem)
28
28
  release_gem(gem)
29
29
  if $?.success?
@@ -7,7 +7,7 @@ module Neetob
7
7
  module Github
8
8
  module Issues
9
9
  class CreateProductSubIssues < Base
10
- SOURCE_REPO = "bigbinary/neeto-engineering-web"
10
+ SOURCE_REPO = "neetozone/neeto-engineering-web"
11
11
 
12
12
  def initialize(issue_number, sandbox = false)
13
13
  super()
@@ -10,7 +10,7 @@ module Neetob
10
10
  BRANCH_NAME = "neeto_compliance"
11
11
  PR_TITLE = "Neeto Compliance"
12
12
  REPO_SPECIFIC_COMMANDS_BEFORE_BUNDLE_INSTALL = {
13
- "bigbinary/neeto-deploy-web" => "sed -i '' '/gem \"karafka\"/d' Gemfile"
13
+ "neetozone/neeto-deploy-web" => "sed -i '' '/gem \"karafka\"/d' Gemfile"
14
14
  }
15
15
 
16
16
  attr_accessor :branch_name, :pr_title
@@ -49,7 +49,7 @@ module Neetob
49
49
  all_repos.map! do |repo_config|
50
50
  repo_config.is_a?(Hash) ? repo_config.to_a.map { |values| { values[0] => values[1] } } : repo_config
51
51
  end
52
- all_repos.flatten.map { |repo| (repo.is_a?(Hash) && repo.values[0].dig("semaphore")) ? "bigbinary/#{repo.keys[0]}" : nil }
52
+ all_repos.flatten.map { |repo| (repo.is_a?(Hash) && repo.values[0].dig("semaphore")) ? "neetozone/#{repo.keys[0]}" : nil }
53
53
  end
54
54
  end
55
55
  end
@@ -39,7 +39,7 @@ module Neetob
39
39
  private
40
40
 
41
41
  def run_yarn_audit(repo)
42
- `#{cd_to_repo(repo)} && yarn audit`
42
+ `#{cd_to_repo(repo)} && yarn audit --json --level high 2>&1`
43
43
  end
44
44
  end
45
45
  end
@@ -6,7 +6,7 @@ module Neetob
6
6
  class CLI
7
7
  module Heroku
8
8
  class Base < CLI::Base
9
- NEETO_DEPLOY_DOCS = "https://github.com/bigbinary/neetob/#working-with-neetodeploy"
9
+ NEETO_DEPLOY_DOCS = "https://github.com/neetozone/neetob/#working-with-neetodeploy"
10
10
 
11
11
  def initialize
12
12
  super()
@@ -10,9 +10,10 @@ module Neetob
10
10
  desc "perform", "Perform the audit"
11
11
  option :month, type: :string, aliases: "-m", required: true, desc: "Month. Example: June-2024"
12
12
  option :skip_issue, type: :boolean, default: false, desc: "Skip creating GitHub issues"
13
+ option :verbose, type: :boolean, default: false, desc: "Log issue details (repo, title, assignee) for each issue that would be or is created"
13
14
 
14
15
  def perform
15
- Perform.new(options[:month], options[:sandbox], options[:skip_issue]).run
16
+ Perform.new(options[:month], options[:sandbox], options[:skip_issue], options[:verbose]).run
16
17
  end
17
18
  end
18
19
  end
@@ -11,20 +11,30 @@ class GithubIssueCreation < Neetob::CLI::Github::Base
11
11
  end
12
12
 
13
13
  def create_issue(repo:, title:, description:, labels: "", assignee: nil)
14
- return if Thread.current[:skip_issue]
15
- return if repo.include?("bigbinary-website")
16
-
17
14
  full_title = build_full_title(title)
18
15
  repo_path = "neetozone/#{repo}"
16
+ assignee = Neetob::CLI::Github::Repositories::TeamLeads.team_lead_for(repo)
17
+
18
+ log_verbose_issue(repo_path:, full_title:, assignee:, description:) if Thread.current[:verbose]
19
+
20
+ return if Thread.current[:skip_issue]
21
+ return if repo.include?("bigbinary-website")
19
22
 
20
23
  # Check for existing issue with exact title match
21
24
  existing_issue = find_existing_issue_by_title(repo_path, full_title)
22
- return existing_issue.html_url if existing_issue
25
+ if existing_issue
26
+ ui.info(
27
+ "[verbose] Issue already exists: #{existing_issue.html_url}",
28
+ print_to_audit_log: false) if Thread.current[:verbose]
29
+ return existing_issue.html_url
30
+ end
23
31
 
24
32
  # Close any existing issues of the same type
25
33
  close_existing_issues_of_same_type(repo_path, title)
26
34
 
27
- create_new_issue(repo_path, full_title, description, labels, assignee)
35
+ url = create_new_issue(repo_path, full_title, description, labels, assignee)
36
+ ui.info("[verbose] Created issue: #{url}", print_to_audit_log: false) if Thread.current[:verbose] && url
37
+ url
28
38
  end
29
39
 
30
40
  private
@@ -72,4 +82,16 @@ class GithubIssueCreation < Neetob::CLI::Github::Base
72
82
  def build_issue_body(full_title, description)
73
83
  "## #{full_title}\n\n#{description}"
74
84
  end
85
+
86
+ def log_verbose_issue(repo_path:, full_title:, assignee:, description:)
87
+ ui.info("--------------------------------", print_to_audit_log: false)
88
+ assignee_str = assignee.to_s.strip.empty? || assignee == "NO TL FOUND" ? "(none)" : assignee
89
+ status = Thread.current[:skip_issue] ? "Would create (skipped)" : "Creating"
90
+ ui.info("[verbose] #{status} issue:", print_to_audit_log: false)
91
+ ui.info(" repository: #{repo_path}", print_to_audit_log: false)
92
+ ui.info(" title: #{full_title}", print_to_audit_log: false)
93
+ ui.info(" assignee: #{assignee_str}", print_to_audit_log: false)
94
+ desc_preview = description.to_s.gsub(/\s+/, " ")
95
+ ui.info(" description: #{desc_preview}", print_to_audit_log: false)
96
+ end
75
97
  end
@@ -9,24 +9,29 @@ module Neetob
9
9
  class CLI
10
10
  module MonthlyAudit
11
11
  class Perform < CLI::Base
12
- attr_accessor :sandbox, :month, :skip_issue
12
+ attr_accessor :sandbox, :month, :skip_issue, :verbose
13
13
 
14
- def initialize(month, sandbox = false, skip_issue = false)
14
+ def initialize(month, sandbox = false, skip_issue = false, verbose = false)
15
15
  super()
16
16
  @month = month
17
17
  @sandbox = sandbox
18
18
  @skip_issue = skip_issue
19
+ @verbose = verbose
19
20
  end
20
21
 
21
22
  def run
22
23
  Thread.current[:month] = month
23
24
  Thread.current[:skip_issue] = skip_issue
25
+ Thread.current[:verbose] = verbose
24
26
  Thread.current[:audit_mode] = true
25
27
  markdown_file_name = "audit-report-#{DateTime.now.to_i}.md"
26
28
  Thread.current[:markdown_file_name] = markdown_file_name
27
29
  ui.success("## Starting the audit for #{month}")
28
30
  ui.info "\n"
29
31
  ui.say("## Issue creation is #{skip_issue ? 'disabled' : 'enabled'}")
32
+ ui.say(
33
+ "## Verbose logging is enabled (logs only in terminal, not in report)",
34
+ Thor::Shell::Color::YELLOW) if verbose
30
35
  ui.info "\n"
31
36
  Security::Main.new.run
32
37
  ui.info "\n"
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  require_relative "../../../github/bundle_audit"
4
6
  require_relative "../../github_issue_creation"
7
+
5
8
  module Neetob
6
9
  class CLI
7
10
  module MonthlyAudit
8
11
  module Security
9
12
  module Code
10
13
  class BundleAudit < CLI::Base
14
+ # Path where repos are cloned during audit (used to read Gemfile.common.rb).
15
+ CLONED_REPO_BASE = "/tmp/neetob"
16
+
11
17
  def initialize
12
18
  super()
13
19
  end
@@ -16,42 +22,188 @@ module Neetob
16
22
  ui.success("### 1.1.1. Checking whether running `bundle-audit check` throws any vulnerabilities")
17
23
  repo_data = [["Repository", "Vulnerabilities Found", "Comments", "Audit Passed"]]
18
24
  ui.info "\n"
25
+
26
+ # Track gems that failed bundle-audit and are defined in Gemfile.common.rb (any repo).
27
+ # Used to create a single issue in neeto-commons-backend at the end of the audit.
28
+ common_gems_failed_audit = Set.new
29
+
19
30
  last_comment = nil
31
+
20
32
  products_and_nanos_repos(:backend).each do |repo|
21
33
  ui.info("Checking bundle audit run results for #{repo}", print_to_audit_log: false)
22
34
  bundle_audit_result = Neetob::CLI::Github::BundleAudit.new([repo]).run
35
+
23
36
  vulnerabilities_found = "No"
24
37
  audit_passed = "No"
25
38
  comments = nil
39
+ advisories = nil
40
+
26
41
  if bundle_audit_result.nil? || bundle_audit_result.include?("No vulnerabilities found")
42
+ # 3. No vulnerabilities: audit passed, no issue created.
27
43
  audit_passed = "Yes"
28
44
  else
29
-
30
45
  vulnerabilities_found = "Yes"
31
- formatted_result = bundle_audit_result.gsub("\n", "<br>").gsub("~", "\\~")
32
- comments = formatted_result.split("<br><br>").first(3).map { |comment| parse_advisory_block(comment) }.join("<br><br>") + " ... <br><br> (run `bundle-audit check` in #{repo} to see all issues)"
33
- issue_url = GithubIssueCreation.new.create_issue(
34
- repo:, title: "Fix vulnerabilities reported by bundle-audit",
35
- description: comments)
36
- audit_passed += " #{issue_url}"
46
+ advisories = parse_bundle_audit_advisories(bundle_audit_result)
47
+ common_gem_names = gem_names_from_gemfile_common(repo)
48
+
49
+ # Split advisories into those for gems in Gemfile.common.rb vs app-specific gems.
50
+ common_vuln_gem_names = advisories.map { |a|
51
+ a[:name] }.select { |name| common_gem_names.include?(name) }.to_set
52
+ app_advisories = advisories.reject { |a| common_gem_names.include?(a[:name]) }
53
+
54
+ # Record common gems that failed audit (for neeto-commons-backend issue later).
55
+ common_gems_failed_audit.merge(common_vuln_gem_names)
56
+
57
+ # --- Issue creation logic (per requirements 1, 2, 4) ---
58
+ # Requirement 1: If ALL vulnerabilities are from Gemfile.common.rb gems only:
59
+ # → Flag audit as failed, do NOT create an issue in this app. Track the failed common gems.
60
+ # Requirement 2: If SOME vulnerabilities are from common gems and SOME from other gems:
61
+ # → Flag audit as failed. Create an issue in this app ONLY for the non-common gems.
62
+ # Requirement 4: At the end of the audit, if any common gems failed anywhere,
63
+ # → Create a single issue in neeto-commons-backend listing all such unique gems.
64
+ issue_url = nil
65
+ if app_advisories.empty?
66
+ # Case 1: Every vulnerability is from a gem in Gemfile.common.rb — no app issue.
67
+ common_only_advisories = advisories.select { |a| common_vuln_gem_names.include?(a[:name]) }
68
+ comments = build_advisory_comments_summary(common_only_advisories, repo, common_only: true)
69
+ else
70
+ # Case 2 (mixed) or all app-only: create one issue in this app for app-specific advisories only.
71
+ comments = build_advisory_comments_summary(app_advisories, repo, common_only: false)
72
+ issue_url = GithubIssueCreation.new.create_issue(
73
+ repo:,
74
+ title: "Fix vulnerabilities reported by bundle-audit",
75
+ description: comments
76
+ )
77
+ end
78
+
79
+ # Set audit_passed based only on where the vulnerabilities come from,
80
+ # not on whether we actually created issues (--skip-issue skips creation but report is unchanged).
81
+ if app_advisories.empty? && !common_vuln_gem_names.empty?
82
+ # All vulnerable gems are from Gemfile.common.rb (commons-only case).
83
+ audit_passed = "No (common gems only; see neeto-commons-backend)"
84
+ elsif issue_url
85
+ # App-specific (or mixed) vulnerabilities with an app issue created.
86
+ audit_passed = "No #{issue_url}"
87
+ elsif !app_advisories.empty?
88
+ # App-specific (or mixed) vulnerabilities with issue creation skipped.
89
+ audit_passed = "No (vulnerabilities found; run `bundle-audit check` in #{repo} for details)"
90
+ end
91
+
37
92
  same_as_last_vulnerabilities = comments == last_comment
38
93
  last_comment = comments
39
- if same_as_last_vulnerabilities
40
- comments = "''"
41
- end
94
+ comments = "''" if same_as_last_vulnerabilities
42
95
  end
96
+
43
97
  table_comments = comments.nil? ? nil : comments.split("<br>").first(3).join("<br>") + " ..."
98
+ # When --verbose is enabled, log each advisory as commons vs app-specific (for easier inspection).
99
+ if Thread.current[:verbose] && vulnerabilities_found == "Yes" && advisories && advisories.any?
100
+ common_gem_names = gem_names_from_gemfile_common(repo)
101
+ debug_section = build_debug_commons_vs_app(advisories, common_gem_names)
102
+ table_comments = [table_comments, debug_section].compact.join("<br><br>")
103
+ end
44
104
  repo_data << [repo, vulnerabilities_found, table_comments, audit_passed]
45
105
  end
106
+
107
+ # 4. If any gems from Gemfile.common.rb failed audit, create one issue in neeto-commons-backend.
108
+ # 5. Add that issue as the last row of the bundle-audit table when created.
109
+ if common_gems_failed_audit.any?
110
+ commons_issue_url = create_commons_backend_issue(common_gems_failed_audit)
111
+ commons_table_comments = "Gems from Gemfile.common.rb with vulnerabilities: " \
112
+ "#{common_gems_failed_audit.sort.join(', ')}"
113
+ audit_passed_cell = commons_issue_url ? "No #{commons_issue_url}" : "No (issue creation skipped)"
114
+ repo_data << [
115
+ "neeto-commons-backend",
116
+ "Yes (common gems)",
117
+ commons_table_comments,
118
+ audit_passed_cell
119
+ ]
120
+ end
121
+
46
122
  ui.print_table(repo_data)
47
123
  end
48
124
 
49
125
  private
50
126
 
127
+ # Parses bundle-audit check output into a list of advisories.
128
+ # Each advisory is a hash with :name (gem name), :block_text (raw block for formatting).
129
+ def parse_bundle_audit_advisories(report)
130
+ return [] if report.nil? || report.include?("No vulnerabilities found")
131
+
132
+ blocks = report.split(/\n\n+/)
133
+ advisories = []
134
+ keys = ["Name:", "Version:", "CVE:", "GHSA:", "Criticality:", "URL:", "Title:", "Solution:"]
135
+
136
+ blocks.each do |block|
137
+ next unless block.include?("Name:")
138
+
139
+ name_line = block.lines.find { |l| l.strip.start_with?("Name:") }
140
+ gem_name = name_line&.strip&.sub(/\AName:\s*/, "")&.strip
141
+ next if gem_name.nil? || gem_name.empty?
142
+
143
+ selected = block.lines.select { |line| keys.any? { |key| line.strip.start_with?(key) } }.join("\n")
144
+ advisories << { name: gem_name, block_text: selected }
145
+ end
146
+ advisories
147
+ end
148
+
149
+ # Returns the set of gem names declared in the repo's Gemfile.common.rb (if present).
150
+ # Used to decide whether a vulnerability is "common" (fixed in commons) vs app-specific.
151
+ # Ignores commented-out lines (e.g. # gem 'rails') to avoid false positives.
152
+ def gem_names_from_gemfile_common(repo)
153
+ path = File.join(CLONED_REPO_BASE, repo, "Gemfile.common.rb")
154
+ return Set.new unless File.file?(path)
155
+
156
+ content = File.read(path)
157
+ # Only consider lines that are not blank and do not start with # (after stripping).
158
+ uncommented = content.each_line
159
+ .reject { |line| line.strip.empty? || line.strip.start_with?("#") }
160
+ .join
161
+ # Match gem "name" or gem 'name' (possibly with version/options on same line).
162
+ uncommented.scan(/gem\s+["']([^"']+)["']/).flatten.map(&:strip).to_set
163
+ end
164
+
165
+ # Verbose logging helper: list each advisory with [commons] or [app-specific].
166
+ def build_debug_commons_vs_app(advisories, common_gem_names)
167
+ lines = advisories.map do |a|
168
+ label = common_gem_names.include?(a[:name]) ? "[commons]" : "[app-specific]"
169
+ "Gem: #{a[:name]} → #{label}"
170
+ end
171
+ "[verbose] Commons vs app-specific breakdown:<br>" + lines.join("<br>")
172
+ end
173
+
174
+ # Builds HTML-style comments for issue body and table, from the given advisories.
175
+ def build_advisory_comments_summary(advisories, repo, common_only: false)
176
+ formatted = advisories.map do |a|
177
+ text = a[:block_text].gsub("\n", "<br>").gsub("~", "\\~")
178
+ parse_advisory_block(text)
179
+ end
180
+ suffix = if common_only
181
+ "(All vulnerabilities are from Gemfile.common.rb; no issue created for this app. " \
182
+ "See neeto-commons-backend.)"
183
+ else
184
+ "(run `bundle-audit check` in #{repo} to see all issues)"
185
+ end
186
+ formatted.join("<br><br>") + " ... <br><br> " + suffix
187
+ end
188
+
51
189
  def parse_advisory_block(block)
52
190
  keys = ["Name:", "Version:", "CVE:", "Title:", "Solution:"]
53
191
  block.split("<br>").select { |line| keys.any? { |key| line.strip.start_with?(key) } }.join("<br>")
54
192
  end
193
+
194
+ # Creates a single issue in neeto-commons-backend listing all unique common gems that failed audit.
195
+ def create_commons_backend_issue(common_gems_failed_audit)
196
+ gem_list = common_gems_failed_audit.sort.join(", ")
197
+ title = "Fix bundle-audit vulnerabilities in Gemfile.common.rb"
198
+ description = "The following gems (from Gemfile.common.rb in one or more apps) reported " \
199
+ "vulnerabilities during the monthly bundle-audit check. Please update and release commons so " \
200
+ "apps can pick up the fixes.\n\n**Gems:** #{gem_list}"
201
+ GithubIssueCreation.new.create_issue(
202
+ repo: "neeto-commons-backend",
203
+ title:,
204
+ description:
205
+ )
206
+ end
55
207
  end
56
208
  end
57
209
  end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+ require "net/http"
5
+ require "uri"
6
+
3
7
  require_relative "../../../github/yarn_audit"
8
+ require_relative "../../github_issue_creation"
4
9
 
5
10
  module Neetob
6
11
  class CLI
@@ -8,6 +13,15 @@ module Neetob
8
13
  module Security
9
14
  module Code
10
15
  class YarnAudit < CLI::Base
16
+ # Severities that count as audit failure (issue creation and commons tracking).
17
+ HIGH_OR_CRITICAL = %w[high critical].freeze
18
+
19
+ # Path where repos are cloned during audit (used to read neeto-commons-frontend/package.json if present).
20
+ CLONED_REPO_BASE = "/tmp/neetob"
21
+
22
+ # Fallback: fetch neeto-commons-frontend package.json from main when not cloned yet (common list is loaded before any clone).
23
+ COMMONS_FRONTEND_PACKAGE_JSON_URL = "https://raw.githubusercontent.com/neetozone/neeto-commons-frontend/main/package.json"
24
+
11
25
  def initialize
12
26
  super()
13
27
  end
@@ -16,35 +30,379 @@ module Neetob
16
30
  ui.success("### 1.1.2. Checking whether running `yarn audit` throws any vulnerabilities")
17
31
  repo_data = [["Repository", "Vulnerabilities Found", "Comments", "Audit Passed"]]
18
32
  ui.info "\n"
33
+
34
+ # Package names from package-common.json OR neeto-commons-frontend package.json.
35
+ # "Dependency of" in yarn audit is compared against this set to decide common vs app.
36
+ common_package_names = common_package_names_from_package_common |
37
+ common_package_names_from_commons_frontend_package_json
38
+
39
+ # Track "Dependency of" package names that are in commons and had high/critical vulns.
40
+ # Used to create a single issue in neeto-commons-frontend at the end.
41
+ common_packages_failed_audit = Set.new
42
+
43
+ last_comment = nil
44
+
19
45
  products_and_nanos_repos(:frontend).each do |repo|
20
- ui.info("Checking yarn audit run results for #{repo}", print_to_audit_log: false)
46
+ ui.info(
47
+ "Checking yarn audit run results for #{repo}",
48
+ print_to_audit_log: false) if Thread.current[:verbose]
21
49
  yarn_audit_result = Neetob::CLI::Github::YarnAudit.new([repo]).run
50
+
22
51
  vulnerabilities_found = "No"
23
52
  audit_passed = "No"
24
53
  comments = nil
25
- severity = yarn_audit_result.split("\n").select { |line|
26
- line.include?("Severity:") }.first&.strip&.gsub("|", ",")
27
- if yarn_audit_result && !is_high_critical_vulnerabilities_found?(severity)
54
+
55
+ # Only high/critical findings drive failure and issue creation.
56
+ all_findings = parse_yarn_audit_findings(yarn_audit_result)
57
+ high_critical = all_findings.select { |f| HIGH_OR_CRITICAL.include?(f[:severity]&.to_s&.downcase) }
58
+
59
+ # Fallback: only use the Severity summary line when we couldn't parse any findings.
60
+ # This handles yarn formats where JSON/table parsing fails but a summary is present.
61
+ report_has_high_critical = all_findings.empty? ? report_has_high_or_critical?(yarn_audit_result) : false
62
+ has_high_critical = high_critical.any? || report_has_high_critical
63
+
64
+ if yarn_audit_result.nil? || yarn_audit_result.to_s.strip.empty? || !has_high_critical
28
65
  audit_passed = "Yes"
29
- else
66
+ vulnerabilities_found = (all_findings.any? || report_has_high_critical) ? "Yes" : "No"
67
+ end
68
+
69
+ if has_high_critical
30
70
  vulnerabilities_found = "Yes"
31
- vulnerabilities = yarn_audit_result.split("\n").select { |line|
32
- line.include?("vulnerabilities found") }.first&.strip
33
- comments = "#{vulnerabilities}<br>#{severity}"
34
71
 
35
- issue_url = GithubIssueCreation.new.create_issue(
36
- repo:, title: "Yarn audit: High/Critical vulnerabilities found",
37
- description: comments)
38
- audit_passed += " #{issue_url}"
72
+ # Common if "Dependency of" is in commons OR the vulnerable package itself is in commons (e.g. qs).
73
+ common_high_critical = high_critical.select { |f| finding_common?(f, common_package_names) }
74
+ app_high_critical = high_critical.reject { |f| finding_common?(f, common_package_names) }
75
+
76
+ # Track both dependency_of and vulnerable package when in commons (for neeto-commons-frontend issue).
77
+ common_high_critical.each do |f|
78
+ if f[:dependency_of] && common_package_names.include?(f[:dependency_of])
79
+ common_packages_failed_audit.add(f[:dependency_of])
80
+ end
81
+ if f[:package] && common_package_names.include?(f[:package])
82
+ common_packages_failed_audit.add(f[:package])
83
+ end
84
+ end
85
+
86
+ # --- Issue creation logic (mirror bundle audit) ---
87
+ # When we couldn't parse table blocks, we only have the summary → create one app issue with summary.
88
+ # When we have parsed findings: 1) all common → no app issue; 2) mixed or all app → app issue.
89
+ issue_url = nil
90
+ if high_critical.empty?
91
+ # Parser found no high/critical blocks; report summary says high/critical → use summary for issue.
92
+ comments = build_summary_only_comments(yarn_audit_result, repo)
93
+ issue_url = GithubIssueCreation.new.create_issue(
94
+ repo:,
95
+ title: "Yarn audit: High/Critical vulnerabilities found",
96
+ description: comments
97
+ )
98
+ elsif app_high_critical.empty?
99
+ # All high/critical vulns are from commons packages.
100
+ comments = build_findings_summary(common_high_critical, repo, common_only: true)
101
+ else
102
+ comments = build_findings_summary(app_high_critical, repo, common_only: false)
103
+ issue_url = GithubIssueCreation.new.create_issue(
104
+ repo:,
105
+ title: "Yarn audit: High/Critical vulnerabilities found",
106
+ description: comments
107
+ )
108
+ end
109
+
110
+ # "Common packages only" only when ALL high/critical are from commons (no app-specific).
111
+ if issue_url.nil? && common_high_critical.any? && app_high_critical.empty?
112
+ audit_passed = "No (common packages only; see neeto-commons-frontend)"
113
+ elsif issue_url
114
+ audit_passed = "No #{issue_url}"
115
+ else
116
+ audit_passed = "No (run `yarn audit` in #{repo} for details)"
117
+ end
118
+
119
+ same_as_last = comments == last_comment
120
+ last_comment = comments
121
+ comments = "''" if same_as_last
122
+ end
123
+
124
+ summary_line = yarn_audit_result.to_s.split("\n").find { |l|
125
+ l.include?("vulnerabilities found") }&.strip
126
+ severity_line = yarn_audit_result.to_s.split("\n").find { |l|
127
+ l.include?("Severity:") }&.strip&.gsub("|", ",")
128
+ table_comments = comments.nil? ? nil : (comments.split("<br>").first(3).join("<br>") + " ...")
129
+ if table_comments.to_s.empty?
130
+ table_comments = if summary_line && severity_line
131
+ "#{summary_line}<br>#{severity_line}"
132
+ elsif all_findings.any?
133
+ build_summary_from_findings(all_findings)
134
+ else
135
+ nil
136
+ end
137
+ end
138
+ # When --verbose is enabled, log each high/critical finding as commons vs app-specific (for easier inspection).
139
+ if Thread.current[:verbose] && has_high_critical && high_critical.any?
140
+ debug_section = build_debug_commons_vs_app(high_critical, common_package_names)
141
+ table_comments = [table_comments, debug_section].compact.join("<br><br>")
39
142
  end
40
- repo_data << [repo, vulnerabilities_found, comments, audit_passed]
143
+ repo_data << [repo, vulnerabilities_found, table_comments, audit_passed]
41
144
  end
145
+
146
+ # Create one issue in neeto-commons-frontend and add last row when any common packages failed.
147
+ if common_packages_failed_audit.any?
148
+ commons_issue_url = create_commons_frontend_issue(common_packages_failed_audit)
149
+ commons_table_comments = "Packages from commons "\
150
+ "(Dependency of or vulnerable package) with high/critical vulns: " \
151
+ "#{common_packages_failed_audit.sort.join(', ')}"
152
+ audit_passed_cell = commons_issue_url ? "No #{commons_issue_url}" : "No (issue creation skipped)"
153
+ repo_data << [
154
+ "neeto-commons-frontend",
155
+ "Yes (common packages)",
156
+ commons_table_comments,
157
+ audit_passed_cell
158
+ ]
159
+ end
160
+
42
161
  ui.print_table(repo_data)
43
162
  end
44
163
 
45
- def is_high_critical_vulnerabilities_found?(severity)
46
- severity&.include?("High") || severity&.include?("Critical")
164
+ def is_high_critical_vulnerabilities_found?(severity_or_report)
165
+ return false if severity_or_report.nil? || severity_or_report.to_s.strip.empty?
166
+
167
+ severity_or_report.to_s.include?("High") || severity_or_report.to_s.include?("Critical")
47
168
  end
169
+
170
+ private
171
+
172
+ # Fallback when table parser fails: report has high/critical if Severity line contains them.
173
+ def report_has_high_or_critical?(report)
174
+ return false if report.nil? || report.to_s.strip.empty?
175
+
176
+ severity_line = report.to_s.split("\n").find { |l| l.include?("Severity:") }
177
+ return false unless severity_line
178
+
179
+ # Try to parse counts from a line like:
180
+ # "Severity: 0 Low | 1 Moderate | 0 High | 0 Critical"
181
+ if severity_line =~ /Severity:\s*(\d+)\s+Low.*?(\d+)\s+High.*?(\d+)\s+Critical/i
182
+ high_count = Regexp.last_match(2).to_i
183
+ critical_count = Regexp.last_match(3).to_i
184
+ high_count.positive? || critical_count.positive?
185
+ else
186
+ # Fallback: any mention of High/Critical means high/critical vulns are present.
187
+ severity_line.include?("High") || severity_line.include?("Critical")
188
+ end
189
+ end
190
+
191
+ # Load package-common.json from neeto-commons (same structure as neeto-commons-backend).
192
+ # Returns set of package names from "dependencies" and "devDependencies" keys.
193
+ # Uses NeetoCompliance::NeetoCommons.path.join("common_files/package-common.json").
194
+ def common_package_names_from_package_common
195
+ path = NeetoCompliance::NeetoCommons.path.join("common_files/package-common.json")
196
+ return Set.new unless path.to_s != "" && File.file?(path.to_s)
197
+
198
+ data = read_json_file(path.to_s)
199
+ deps = data["dependencies"] || {}
200
+ dev_deps = data["devDependencies"] || {}
201
+ (deps.keys + dev_deps.keys).to_set
202
+ rescue NameError, Errno::ENOENT
203
+ Set.new
204
+ end
205
+
206
+ # Load package.json from neeto-commons-frontend (local clone or GitHub raw).
207
+ # Common list is built before we iterate repos, so the clone often isn't there yet — fetch from main if needed.
208
+ # Returns set of package names from "dependencies", "devDependencies", and "peerDependencies".
209
+ def common_package_names_from_commons_frontend_package_json
210
+ data = commons_frontend_package_json_data
211
+ return Set.new unless data.is_a?(Hash)
212
+
213
+ deps = data["dependencies"] || {}
214
+ dev_deps = data["devDependencies"] || {}
215
+ peer_deps = data["peerDependencies"] || {}
216
+ (deps.keys + dev_deps.keys + peer_deps.keys).to_set
217
+ rescue JSON::ParserError
218
+ Set.new
219
+ end
220
+
221
+ # Returns parsed package.json from neeto-commons-frontend: local file if present, else fetch from main.
222
+ def commons_frontend_package_json_data
223
+ path = File.join(CLONED_REPO_BASE, "neeto-commons-frontend", "package.json")
224
+ return read_json_file(path) if File.file?(path)
225
+
226
+ uri = URI(COMMONS_FRONTEND_PACKAGE_JSON_URL)
227
+ response = Net::HTTP.get_response(uri)
228
+ return {} unless response.is_a?(Net::HTTPSuccess)
229
+
230
+ JSON.parse(response.body)
231
+ rescue Errno::ENOENT, JSON::ParserError, SocketError, Net::OpenTimeout
232
+ {}
233
+ end
234
+
235
+ # Prefer JSON output (yarn audit --json) for accurate "Dependency of" → common vs app.
236
+ # Fall back to table parsing if report is not JSON lines.
237
+ def parse_yarn_audit_findings(report)
238
+ return [] if report.nil? || report.to_s.strip.empty?
239
+
240
+ findings = parse_yarn_audit_json(report)
241
+ return findings if findings.any?
242
+
243
+ parse_yarn_audit_table(report)
244
+ end
245
+
246
+ # Yarn 1: yarn audit --json outputs one JSON object per line (auditAdvisory, auditSummary).
247
+ # data.advisory.severity, data.advisory.module_name; data.resolution.path = "dep@ver > vuln@ver" → first segment = Dependency of.
248
+ def parse_yarn_audit_json(report)
249
+ findings = []
250
+ report.to_s.each_line do |line|
251
+ line = line.strip
252
+ next if line.empty?
253
+
254
+ obj = JSON.parse(line)
255
+ next unless obj["type"] == "auditAdvisory"
256
+
257
+ data = obj["data"] || {}
258
+ advisory = data["advisory"] || {}
259
+ resolution = data["resolution"] || {}
260
+ severity = (advisory["severity"] || "").to_s.downcase
261
+ package = (advisory["module_name"] || "").to_s.strip
262
+ path = (resolution["path"] || "").to_s.strip
263
+ path_segments = path.split(" > ").map { |segment| segment.sub(/@[^@]*\z/, "") }.reject(&:empty?)
264
+ # First segment of path is the direct dependency ("Dependency of" in human table).
265
+ dependency_of = path.split(" > ").first.to_s.strip
266
+ dependency_of = dependency_of.sub(/@[^@]*\z/, "") if dependency_of.include?("@")
267
+
268
+ next if package.empty?
269
+
270
+ block_text = [
271
+ "Package: #{package}",
272
+ "Severity: #{severity}",
273
+ "Dependency of: #{dependency_of}",
274
+ "Path: #{path}",
275
+ advisory["url"] ? "More info: #{advisory['url']}" : nil
276
+ ].compact.join("\n")
277
+
278
+ findings << {
279
+ severity:,
280
+ package:,
281
+ dependency_of:,
282
+ path:,
283
+ path_segments:,
284
+ block_text:
285
+ }
286
+ rescue JSON::ParserError
287
+ next
288
+ end
289
+ findings
290
+ end
291
+
292
+ # Parse human-readable table (box-drawing); used when --json not available.
293
+ def parse_yarn_audit_table(report)
294
+ return [] if report.nil? || report.to_s.strip.empty?
295
+
296
+ blocks = report.split(/\n(?=┌─)/)
297
+ findings = []
298
+
299
+ blocks.each do |block|
300
+ next unless block.include?("Package") && block.include?("Dependency of")
301
+
302
+ row_key = nil
303
+ parsed = {}
304
+
305
+ block.each_line do |line|
306
+ next unless line.include?("│")
307
+
308
+ parts = line.split("│").map(&:strip)
309
+ key = parts[1].to_s.strip
310
+ value = parts[2].to_s.strip
311
+
312
+ if key != ""
313
+ row_key = key
314
+ parsed[row_key] = value
315
+ elsif row_key && value != ""
316
+ parsed[row_key] = "#{parsed[row_key]} #{value}"
317
+ end
318
+ end
319
+
320
+ severity = parsed.keys.find { |k|
321
+ %w[low moderate high critical].include?(k.to_s.downcase) }&.to_s&.downcase
322
+ package = parsed["Package"]
323
+ dependency_of = parsed["Dependency of"]
324
+ next if package.to_s.empty?
325
+
326
+ keys = ["Package", "Patched in", "Dependency of", "Path", "More info"]
327
+ block_text = parsed.slice(*keys).map { |k, v| "#{k}: #{v}" }.join("\n")
328
+
329
+ findings << {
330
+ severity:,
331
+ package:,
332
+ dependency_of:,
333
+ block_text:
334
+ }
335
+ end
336
+
337
+ findings
338
+ end
339
+
340
+ def build_findings_summary(findings, repo, common_only: false)
341
+ lines = findings.first(5).map { |f| f[:block_text].gsub("\n", "<br>").gsub("|", "\\|") }
342
+ suffix = if common_only
343
+ "(All high/critical vulnerabilities are from packages in package-common.json; " \
344
+ "no issue created for this app. See neeto-commons-frontend.)"
345
+ else
346
+ "(run `yarn audit` in #{repo} to see all issues)"
347
+ end
348
+ lines.join("<br><br>") + " ... <br><br> " + suffix
349
+ end
350
+
351
+ # Build summary line from parsed findings (e.g. when report was JSON and had no human summary).
352
+ def build_summary_from_findings(findings)
353
+ return nil if findings.nil? || findings.empty?
354
+
355
+ by_sev = findings.group_by { |f| f[:severity]&.to_s&.downcase || "unknown" }
356
+ counts = { "low" => 0, "moderate" => 0, "high" => 0, "critical" => 0 }
357
+ by_sev.each { |sev, list| counts[sev] = list.size if counts.key?(sev) }
358
+ parts = %w[low moderate high critical].map { |s|
359
+ "#{counts[s]} #{s.capitalize}" if counts[s].positive? }.compact
360
+ "#{findings.size} vulnerabilities found<br>Severity: #{parts.join(' , ')}"
361
+ end
362
+
363
+ # A finding is commons if "Dependency of" is in common set OR the vulnerable package is in common set.
364
+ def finding_common?(finding, common_package_names)
365
+ dependency_of = finding[:dependency_of]
366
+ package = finding[:package]
367
+ path_segments = finding[:path_segments] || []
368
+
369
+ common_package_names.include?(dependency_of) ||
370
+ common_package_names.include?(package) ||
371
+ path_segments.any? { |name| common_package_names.include?(name) }
372
+ end
373
+
374
+ # Verbose logging helper: list each high/critical finding with [commons] or [app-specific].
375
+ def build_debug_commons_vs_app(high_critical, common_package_names)
376
+ lines = high_critical.map do |f|
377
+ label = finding_common?(f, common_package_names) ? "[commons]" : "[app-specific]"
378
+ "Package: #{f[:package]} | Severity: #{f[:severity]} | Dependency of: #{f[:dependency_of]} → #{label}"
379
+ end
380
+ "[verbose] Commons vs app-specific breakdown:<br>" + lines.join("<br>")
381
+ end
382
+
383
+ # Used when we detected high/critical from summary line but couldn't parse table blocks.
384
+ def build_summary_only_comments(report, repo)
385
+ summary = report.to_s.split("\n").find { |l| l.include?("vulnerabilities found") }&.strip
386
+ severity = report.to_s.split("\n").find { |l| l.include?("Severity:") }&.strip&.gsub("|", ",")
387
+ parts = [summary, severity].compact
388
+ parts << "(run `yarn audit` in #{repo} to see full details)"
389
+ parts.join("<br>")
390
+ end
391
+
392
+ def create_commons_frontend_issue(common_packages_failed_audit)
393
+ package_list = common_packages_failed_audit.sort.join(", ")
394
+ title = "Fix yarn audit high/critical vulnerabilities in package-common.json"
395
+ description = "The following packages (from package-common.json or" \
396
+ " neeto-commons-frontend package.json, " \
397
+ "as 'Dependency of' or vulnerable package in yarn audit) reported high or critical vulnerabilities " \
398
+ "during the monthly yarn audit. Please update and release commons so apps can pick up the fixes." \
399
+ "\n\n**Packages:** #{package_list}"
400
+ GithubIssueCreation.new.create_issue(
401
+ repo: "neeto-commons-frontend",
402
+ title:,
403
+ description:
404
+ )
405
+ end
48
406
  end
49
407
  end
50
408
  end
@@ -60,7 +60,7 @@ module Neetob
60
60
  end
61
61
 
62
62
  def check_envs_neetodeploy(app)
63
- # TODO: Optimize once github.com/bigbinary/neeto-deploy-web/issues/3745 is done
63
+ # TODO: Optimize once github.com/neetozone/neeto-deploy-web/issues/3745 is done
64
64
  begin
65
65
  env_table = `bundle exec neetodeploy env list -a #{app}`
66
66
  json_parse_result = JSON.parse(env_table) rescue nil
@@ -105,7 +105,7 @@ module Neetob
105
105
  end
106
106
 
107
107
  def clean_repo_name(repo)
108
- repo.gsub("bigbinary/", "")
108
+ repo.gsub("neetozone/", "")
109
109
  end
110
110
 
111
111
  def invalid_email?(email)
@@ -67,7 +67,7 @@ module Neetob
67
67
  def html_table_row(app, commit_id, email, time, date, title)
68
68
  starting_7_characters_of_commit = commit_id[0..6]
69
69
  "<tr>
70
- <td class='commit'><a href='https://github.com/bigbinary/#{app}/commit/#{commit_id}'
70
+ <td class='commit'><a href='https://github.com/neetozone/#{app}/commit/#{commit_id}'
71
71
  target='_blank'>#{starting_7_characters_of_commit}</a></td>
72
72
  <td class='project'>#{app}</td>
73
73
  <td class='time'>#{time}</td>
@@ -36,7 +36,7 @@ module Neetob
36
36
  " Use \"neetob github login\" command to update or set the token.")
37
37
  when Octokit::Forbidden
38
38
  ui.error(
39
- "You don't have enough permissions to access 'bigbinary' organization."\
39
+ "You don't have enough permissions to access 'neetozone' organization."\
40
40
  " Please grant access to this org while authorizing via the browser.")
41
41
  when Errno::ENOENT
42
42
  ui.error(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Neetob
4
- VERSION = "0.5.83"
4
+ VERSION = "0.5.85"
5
5
  end
data/neetob.gemspec CHANGED
@@ -10,13 +10,13 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = "Provides a set of helper scripts for Github and Heroku."
12
12
  spec.description = "This gem gives different commands for interacting with Github and Heroku instances of existing neeto repos."
13
- spec.homepage = "https://github.com/bigbinary/neetob"
13
+ spec.homepage = "https://github.com/neetozone/neetob"
14
14
  spec.license = "MIT"
15
15
  spec.required_ruby_version = ">= 3.1.3"
16
16
 
17
17
  spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = "https://github.com/bigbinary/neetob"
19
- spec.metadata["changelog_uri"] = "https://github.com/bigbinary/neetob/blob/main/CHANGELOG.md"
18
+ spec.metadata["source_code_uri"] = "https://github.com/neetozone/neetob"
19
+ spec.metadata["changelog_uri"] = "https://github.com/neetozone/neetob/blob/main/CHANGELOG.md"
20
20
 
21
21
  # Specify which files should be added to the gem when it is released.
22
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -42,6 +42,7 @@ Gem::Specification.new do |spec|
42
42
  spec.add_dependency "actionview" # for helpers
43
43
  spec.add_dependency "activesupport" # for helpers
44
44
  spec.add_dependency "pry" # for debugging
45
+ spec.add_dependency "csv" # required by httparty; extracted from stdlib in Ruby 3.4+
45
46
 
46
47
  # To add the files from submodules
47
48
  `git submodule --quiet foreach pwd`.split($\).each do |submodule_path|
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.83
4
+ version: 0.5.85
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-02-09 00:00:00.000000000 Z
11
+ date: 2026-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: csv
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
181
195
  description: This gem gives different commands for interacting with Github and Heroku
182
196
  instances of existing neeto repos.
183
197
  email:
@@ -378,13 +392,13 @@ files:
378
392
  - scripts/workflows/sparkpost.ts
379
393
  - tsconfig.json
380
394
  - yarn.lock
381
- homepage: https://github.com/bigbinary/neetob
395
+ homepage: https://github.com/neetozone/neetob
382
396
  licenses:
383
397
  - MIT
384
398
  metadata:
385
- homepage_uri: https://github.com/bigbinary/neetob
386
- source_code_uri: https://github.com/bigbinary/neetob
387
- changelog_uri: https://github.com/bigbinary/neetob/blob/main/CHANGELOG.md
399
+ homepage_uri: https://github.com/neetozone/neetob
400
+ source_code_uri: https://github.com/neetozone/neetob
401
+ changelog_uri: https://github.com/neetozone/neetob/blob/main/CHANGELOG.md
388
402
  post_install_message:
389
403
  rdoc_options: []
390
404
  require_paths: