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 +4 -4
- data/Gemfile.lock +3 -1
- data/README.md +2 -2
- data/docs/development.md +1 -1
- data/docs/testing.md +1 -1
- data/lib/neetob/cli/base.rb +2 -2
- data/lib/neetob/cli/cloudflare/base.rb +1 -1
- data/lib/neetob/cli/fetchorupdate_repos/execute.rb +1 -1
- data/lib/neetob/cli/github/gems/release.rb +1 -1
- data/lib/neetob/cli/github/issues/create_product_sub_issues.rb +1 -1
- data/lib/neetob/cli/github/make_pr/base.rb +1 -1
- data/lib/neetob/cli/github/protect_branch.rb +1 -1
- data/lib/neetob/cli/github/yarn_audit.rb +1 -1
- data/lib/neetob/cli/heroku/base.rb +1 -1
- data/lib/neetob/cli/monthly_audit/commands.rb +2 -1
- data/lib/neetob/cli/monthly_audit/github_issue_creation.rb +27 -5
- data/lib/neetob/cli/monthly_audit/perform.rb +7 -2
- data/lib/neetob/cli/monthly_audit/security/code/bundle_audit.rb +162 -10
- data/lib/neetob/cli/monthly_audit/security/code/yarn_audit.rb +373 -15
- data/lib/neetob/cli/sre/check_essential_env.rb +1 -1
- data/lib/neetob/cli/users/audit.rb +1 -1
- data/lib/neetob/cli/users/commits.rb +1 -1
- data/lib/neetob/exception_handler.rb +1 -1
- data/lib/neetob/version.rb +1 -1
- data/neetob.gemspec +4 -3
- metadata +20 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16306ac8fba41db1469e0138632f44a7ff12013c144817937924635899f7f00b
|
|
4
|
+
data.tar.gz: 3fd69cacef27a582301e59efd916fdc35a4a1217ea5c4411f42e4bcb4415f19c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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/
|
|
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/
|
|
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
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/
|
|
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
|
|
data/lib/neetob/cli/base.rb
CHANGED
|
@@ -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/
|
|
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"] : ["
|
|
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/
|
|
9
|
+
NEETO_DEPLOY_DOCS = "https://github.com/neetozone/neetob/#working-with-neetodeploy"
|
|
10
10
|
|
|
11
11
|
ZONE_IDS = {
|
|
12
12
|
"neetoauth.com": "81ade5aa3e075489533f903dc03b1e88",
|
|
@@ -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
|
-
"
|
|
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")) ? "
|
|
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
|
|
@@ -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/
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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,
|
|
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?(
|
|
46
|
-
|
|
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/
|
|
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
|
|
@@ -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/
|
|
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 '
|
|
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(
|
data/lib/neetob/version.rb
CHANGED
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/
|
|
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/
|
|
19
|
-
spec.metadata["changelog_uri"] = "https://github.com/
|
|
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.
|
|
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-
|
|
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/
|
|
395
|
+
homepage: https://github.com/neetozone/neetob
|
|
382
396
|
licenses:
|
|
383
397
|
- MIT
|
|
384
398
|
metadata:
|
|
385
|
-
homepage_uri: https://github.com/
|
|
386
|
-
source_code_uri: https://github.com/
|
|
387
|
-
changelog_uri: https://github.com/
|
|
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:
|