Skip to content
This repository was archived by the owner on Jan 22, 2026. It is now read-only.

Commit 37e790a

Browse files
committed
Enhance vulnerability history output to include withdrawn vulnerabilities and their statuses
1 parent 7eeff5d commit 37e790a

File tree

2 files changed

+162
-12
lines changed

2 files changed

+162
-12
lines changed

lib/git/pkgs/commands/vulns/history.rb

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -209,37 +209,48 @@ def run_package_history(package_name, repo)
209209

210210
changes.each do |change|
211211
affected_vulns = vuln_pkgs.select do |vp|
212-
next false if vp.vulnerability&.withdrawn?
213212
change.requirement && vp.affects_version?(change.requirement)
214213
end
215214

216-
vuln_info = if affected_vulns.any?
217-
"(vulnerable to #{affected_vulns.map { |vp| vp.vulnerability_id }.join(", ")})"
218-
else
219-
""
220-
end
215+
active_vulns = affected_vulns.reject { |vp| vp.vulnerability&.withdrawn? }
216+
withdrawn_vulns = affected_vulns.select { |vp| vp.vulnerability&.withdrawn? }
217+
218+
vuln_parts = []
219+
vuln_parts << "vulnerable to #{active_vulns.map(&:vulnerability_id).join(", ")}" if active_vulns.any?
220+
vuln_parts << "#{withdrawn_vulns.map(&:vulnerability_id).join(", ")} withdrawn" if withdrawn_vulns.any?
221+
vuln_info = vuln_parts.any? ? "(#{vuln_parts.join("; ")})" : ""
221222

222223
event = {
223224
date: change.commit.committed_at,
224225
event_type: change.change_type.to_sym,
225226
description: "#{change.change_type.capitalize} #{package_name} #{change.requirement} #{vuln_info}".strip,
226227
version: change.requirement,
227228
commit: format_commit_info(change.commit),
228-
affected_vulns: affected_vulns.map(&:vulnerability_id)
229+
affected_vulns: active_vulns.map(&:vulnerability_id),
230+
withdrawn_vulns: withdrawn_vulns.map(&:vulnerability_id)
229231
}
230232

231233
timeline << event
232234
end
233235

234236
vuln_pkgs.each do |vp|
235-
next unless vp.vulnerability&.published_at
236-
next if vp.vulnerability.withdrawn?
237+
vuln = vp.vulnerability
238+
next unless vuln&.published_at
237239

240+
withdrawn_note = vuln.withdrawn? ? " [withdrawn]" : ""
238241
timeline << {
239-
date: vp.vulnerability.published_at,
242+
date: vuln.published_at,
240243
event_type: :cve_published,
241-
description: "#{vp.vulnerability_id} published (#{vp.vulnerability.severity || "unknown"} severity)"
244+
description: "#{vp.vulnerability_id} published (#{vuln.severity || "unknown"} severity)#{withdrawn_note}"
242245
}
246+
247+
if vuln.withdrawn? && vuln.withdrawn_at
248+
timeline << {
249+
date: vuln.withdrawn_at,
250+
event_type: :cve_withdrawn,
251+
description: "#{vp.vulnerability_id} withdrawn"
252+
}
253+
end
243254
end
244255

245256
timeline = filter_timeline_by_date(timeline)
@@ -316,6 +327,7 @@ def output_package_timeline(package_name, timeline)
316327

317328
colored_line = case event[:event_type]
318329
when :cve_published then Color.yellow(line)
330+
when :cve_withdrawn then Color.cyan(line)
319331
when :added
320332
event[:affected_vulns]&.any? ? Color.red(line) : line
321333
when :modified

test/git/pkgs/test_vulns_commands.rb

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,8 @@ def test_scan_sarif_output_format
341341
# Validate against SARIF 2.1.0 schema
342342
require "json_schemer"
343343
schema_path = File.join(File.dirname(__FILE__), "../../fixtures/sarif-schema-2.1.0.json")
344-
schema = JSONSchemer.schema(Pathname.new(schema_path))
344+
schema_content = JSON.parse(File.read(schema_path))
345+
schema = JSONSchemer.schema(schema_content, ref_resolver: proc { |uri| schema_content })
345346
errors = schema.validate(sarif).to_a
346347
assert_empty errors, "SARIF schema validation failed: #{errors.map { |e| e["error"] }.join(", ")}"
347348
ensure
@@ -549,3 +550,140 @@ def test_lockfile_dependencies_filter
549550
assert_equal "7.0.0", result.first[:requirement]
550551
end
551552
end
553+
554+
class Git::Pkgs::TestVulnsHistory < Minitest::Test
555+
include TestHelpers
556+
557+
def setup
558+
Git::Pkgs::Database.disconnect
559+
create_test_repo
560+
561+
add_file("package.json", '{"dependencies": {"lodash": "4.17.0"}}')
562+
commit("Add lodash")
563+
564+
@git_dir = File.join(@test_dir, ".git")
565+
Git::Pkgs::Database.connect(@git_dir)
566+
Git::Pkgs::Database.create_schema
567+
568+
Git::Pkgs.git_dir = @git_dir
569+
capture_stdout { Git::Pkgs::Commands::Init.new(["--no-hooks", "--force"]).run }
570+
571+
# Mark package as synced to avoid OSV API calls
572+
Git::Pkgs::Models::Package.create(
573+
purl: "pkg:npm/lodash",
574+
ecosystem: "npm",
575+
name: "lodash",
576+
vulns_synced_at: Time.now
577+
)
578+
end
579+
580+
def teardown
581+
Git::Pkgs.git_dir = nil
582+
cleanup_test_repo
583+
end
584+
585+
def capture_stdout
586+
original = $stdout
587+
$stdout = StringIO.new
588+
yield
589+
$stdout.string
590+
ensure
591+
$stdout = original
592+
end
593+
594+
def test_history_shows_withdrawn_vulns_in_timeline
595+
# Create an active vulnerability
596+
active_vuln = Git::Pkgs::Models::Vulnerability.create(
597+
id: "GHSA-active",
598+
summary: "Active vulnerability",
599+
severity: "high",
600+
published_at: Time.now - 86400,
601+
fetched_at: Time.now
602+
)
603+
Git::Pkgs::Models::VulnerabilityPackage.create(
604+
vulnerability_id: active_vuln.id,
605+
ecosystem: "npm",
606+
package_name: "lodash",
607+
vulnerable_range: "< 4.17.21"
608+
)
609+
610+
# Create a withdrawn vulnerability
611+
withdrawn_vuln = Git::Pkgs::Models::Vulnerability.create(
612+
id: "GHSA-withdrawn",
613+
summary: "Withdrawn vulnerability",
614+
severity: "medium",
615+
published_at: Time.now - 172800,
616+
withdrawn_at: Time.now - 86400,
617+
fetched_at: Time.now
618+
)
619+
Git::Pkgs::Models::VulnerabilityPackage.create(
620+
vulnerability_id: withdrawn_vuln.id,
621+
ecosystem: "npm",
622+
package_name: "lodash",
623+
vulnerable_range: "< 4.17.21"
624+
)
625+
626+
output = capture_stdout do
627+
Git::Pkgs::Commands::Vulns::History.new(["lodash"]).run
628+
end
629+
630+
assert_includes output, "GHSA-active"
631+
assert_includes output, "GHSA-withdrawn"
632+
assert_includes output, "withdrawn"
633+
end
634+
635+
def test_history_json_includes_withdrawn_vulns
636+
withdrawn_vuln = Git::Pkgs::Models::Vulnerability.create(
637+
id: "GHSA-withdrawn-json",
638+
summary: "Withdrawn vulnerability",
639+
severity: "low",
640+
published_at: Time.now - 172800,
641+
withdrawn_at: Time.now - 86400,
642+
fetched_at: Time.now
643+
)
644+
Git::Pkgs::Models::VulnerabilityPackage.create(
645+
vulnerability_id: withdrawn_vuln.id,
646+
ecosystem: "npm",
647+
package_name: "lodash",
648+
vulnerable_range: "< 4.17.21"
649+
)
650+
651+
output = capture_stdout do
652+
Git::Pkgs::Commands::Vulns::History.new(["lodash", "-f", "json"]).run
653+
end
654+
655+
json = JSON.parse(output)
656+
assert_equal "lodash", json["package"]
657+
658+
events = json["timeline"]
659+
withdrawn_events = events.select { |e| e["event_type"] == "cve_withdrawn" }
660+
assert withdrawn_events.any?, "Expected withdrawn event in timeline"
661+
662+
published_events = events.select { |e| e["description"]&.include?("[withdrawn]") }
663+
assert published_events.any?, "Expected [withdrawn] annotation on published event"
664+
end
665+
666+
def test_history_shows_withdrawn_event_with_date
667+
withdrawn_time = Time.now - 86400
668+
withdrawn_vuln = Git::Pkgs::Models::Vulnerability.create(
669+
id: "GHSA-with-date",
670+
summary: "Withdrawn with date",
671+
severity: "high",
672+
published_at: Time.now - 172800,
673+
withdrawn_at: withdrawn_time,
674+
fetched_at: Time.now
675+
)
676+
Git::Pkgs::Models::VulnerabilityPackage.create(
677+
vulnerability_id: withdrawn_vuln.id,
678+
ecosystem: "npm",
679+
package_name: "lodash",
680+
vulnerable_range: "< 4.17.21"
681+
)
682+
683+
output = capture_stdout do
684+
Git::Pkgs::Commands::Vulns::History.new(["lodash"]).run
685+
end
686+
687+
assert_includes output, "GHSA-with-date withdrawn"
688+
end
689+
end

0 commit comments

Comments
 (0)