Module: StillActive::VulnerabilityHelper
- Extended by:
- VulnerabilityHelper
- Included in:
- VulnerabilityHelper
- Defined in:
- lib/helpers/vulnerability_helper.rb
Constant Summary collapse
- SEVERITY_ORDER =
["low", "medium", "high", "critical"].freeze
- OSV_LABELS =
OSV's GHSA severity labels, mapped to our order. deps.dev stores only CVSS 3.x, so a CVSS-4-only advisory returns cvss3Score 0 (unscored); OSV's
database_specific.severityreads correctly for it and rescues a real HIGH from fail-closed noise. GitHub uses MODERATE where we use medium. { "critical" => "critical", "high" => "high", "moderate" => "medium", "medium" => "medium", "low" => "low" }.freeze
Instance Method Summary collapse
-
#advisory_severity(vulnerability) ⇒ Object
One advisory's severity label ("low".."critical"), or nil when genuinely unscored.
-
#effective_score(vulnerability) ⇒ Object
The best real CVSS score for an advisory, or nil when none is usable.
- #highest_severity(vulnerabilities) ⇒ Object
-
#merge_advisories(deps_dev:, ruby_advisory_db:) ⇒ Object
Combines advisories from deps.dev and ruby-advisory-db (via bundler-audit), deduplicating on shared identifiers.
-
#no_fix_available?(vulnerabilities) ⇒ Boolean
True when at least one advisory has no fixed version available -- the gem can't be upgraded out of the vulnerability.
- #severity_at_or_above?(vulnerabilities, threshold) ⇒ Boolean
-
#unknown_severity?(vulnerability) ⇒ Boolean
Unknown = no CVSS score AND no OSV label.
Instance Method Details
#advisory_severity(vulnerability) ⇒ Object
One advisory's severity label ("low".."critical"), or nil when genuinely unscored. The authoritative GHSA/OSV label is a FLOOR: a real CVSS number sharpens precision and can RAISE the band, but never lowers it below the label. This matters because the OSV-derived score is cvss-suite's overall_score, which folds in any threat/environmental metrics a vector carries (and can diverge from GHSA at a band edge) -- on a fail-closed, no-false-positive tool a computed number must not silently demote a GHSA-HIGH finding out of a severity gate. With only one of the two present, that one stands.
31 32 33 34 35 36 37 |
# File 'lib/helpers/vulnerability_helper.rb', line 31 def advisory_severity(vulnerability) from_score = (score = effective_score(vulnerability)) ? severity_label(score) : nil from_label = OSV_LABELS[vulnerability[:osv_severity].to_s.downcase] return from_score || from_label if from_score.nil? || from_label.nil? [from_score, from_label].max_by { |label| SEVERITY_ORDER.index(label) } end |
#effective_score(vulnerability) ⇒ Object
The best real CVSS score for an advisory, or nil when none is usable. Prefers deps.dev's v3/v2, then the score OSV enrichment computed from a v4 (or v3) vector. A score of 0 is treated as absent: deps.dev only stores CVSS 3.x, so a CVSS-4-only advisory (e.g. two HIGH protobuf GHSAs) comes back with cvss3Score 0 -- its "no 3.x score" sentinel, not a genuine severity of zero (which published advisories never carry) -- and the OSV v4 score fills that gap. Every consumer of a numeric score (severity labels, SARIF level, CycloneDX rating) routes through here so a 0 can't masquerade as "low" and silently clear a gate.
65 66 67 |
# File 'lib/helpers/vulnerability_helper.rb', line 65 def effective_score(vulnerability) [vulnerability[:cvss3_score], vulnerability[:cvss2_score], vulnerability[:osv_cvss_score]].find { |score| score&.positive? } end |
#highest_severity(vulnerabilities) ⇒ Object
15 16 17 18 19 20 21 |
# File 'lib/helpers/vulnerability_helper.rb', line 15 def highest_severity(vulnerabilities) return if vulnerabilities.nil? || vulnerabilities.empty? vulnerabilities .filter_map { |v| advisory_severity(v) } .max_by { |label| SEVERITY_ORDER.index(label) } end |
#merge_advisories(deps_dev:, ruby_advisory_db:) ⇒ Object
Combines advisories from deps.dev and ruby-advisory-db (via bundler-audit), deduplicating on shared identifiers. deps.dev is preferred for CVSS/title/url (it carries the vector string); ruby-advisory-db fills gaps. Advisories present in both sources are tagged source: "merged"; otherwise the per-source tag is kept.
86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/helpers/vulnerability_helper.rb', line 86 def merge_advisories(deps_dev:, ruby_advisory_db:) merged = deps_dev.map(&:dup) ruby_advisory_db.each do |advisory| existing = merged.find { |m| identifiers(m).intersect?(identifiers(advisory)) } if existing combine!(existing, advisory) else merged << advisory end end merged end |
#no_fix_available?(vulnerabilities) ⇒ Boolean
True when at least one advisory has no fixed version available -- the gem can't be upgraded out of the vulnerability. Only ruby-advisory-db sets this today; a deps.dev-only advisory leaves it nil (unknown), which reads false.
53 54 55 |
# File 'lib/helpers/vulnerability_helper.rb', line 53 def no_fix_available?(vulnerabilities) Array(vulnerabilities).any? { |vulnerability| vulnerability[:no_fix_available] } end |
#severity_at_or_above?(vulnerabilities, threshold) ⇒ Boolean
69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/helpers/vulnerability_helper.rb', line 69 def severity_at_or_above?(vulnerabilities, threshold) return false if vulnerabilities.nil? || vulnerabilities.empty? # Fail closed on an unscored advisory: a confirmed advisory we can't score # could be anything up to critical (and fresh CVEs commonly lack a score), # so passing it as "below threshold" would silently clear a severity gate on # a real finding. The user accepts a specific advisory via a suppression if # they've reviewed it. Only when every advisory is scored do we compare. return true if vulnerabilities.any? { |vulnerability| unknown_severity?(vulnerability) } SEVERITY_ORDER.index(highest_severity(vulnerabilities)) >= SEVERITY_ORDER.index(threshold) end |
#unknown_severity?(vulnerability) ⇒ Boolean
Unknown = no CVSS score AND no OSV label. A freshly disclosed CVE often has neither yet, and the cross-ecosystem lens emits a minimal advisory when detail-fetch fails. The CVSS-4-only case (deps.dev returns cvss3Score 0, its "no 3.x score" sentinel, for two HIGH protobuf GHSAs scored CVSS 4.0) is no longer unknown once OSV enrichment attaches the GHSA severity label -- that's the whole point of the enrichment: a real HIGH stops failing closed as unscored. A truly unknown advisory still fails closed on a gate.
46 47 48 |
# File 'lib/helpers/vulnerability_helper.rb', line 46 def unknown_severity?(vulnerability) advisory_severity(vulnerability).nil? end |