Skip to content

Commit

Permalink
Review CVSS score handling & reporting (#118)
Browse files Browse the repository at this point in the history
* Review CVSS score handling & reporting

For dependency-check:

Clj-watson now recognizes that multiple CVSS versions can be populated
for a single CVE. We now:
- to be cautious, choose the highest base score across all CVSS versions
- include the CVSS version with the score

For github-advisory:

The github-advisory only contains a single CVSS entry.
Clj-watson now extracts the CVSS revision from the CVSS "vectorString",
when available.

For reports:

- `json` & `edn` - now include the CVSS `:version` under `:cvss`
- `stdout` - now includes version after score: `CVSS: <score> (version <cvss version>)`
- `sarif`
  - added `cvss` with its `score`, `version` and `severity` under `properties`,
this duplicates existing the (unfortunately named) `security-severity`
which also holds the `score`
- reworded slightly awkward summary message, ex:
  - old: Vulnerability identified as CVE-2022-4244 of score 7.5 and severity HIGH found.
  - new: Vulnerability CVE-2022-4244 with a score of 7.5 and severity of HIGH found.

Out of scope:

This change does not include support for deriving a CVSS score when it
missing. This will be handled when we need it for decision making, like
in #114.

Closes #112

* docs: update changelog
  • Loading branch information
lread authored Aug 25, 2024
1 parent 4d44cb4 commit 98b040a
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 28 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* Unreleased
* Fix: `--output json` now renders correctly & JSON output now pretty-printed [#116](https://github.com/clj-holmes/clj-watson/issues/116)
* Recognize CVSS2 and CVSS4 scores when available [#112](https://github.com/clj-holmes/clj-watson/issues/112)

* v6.0.0 cb02879 -- 2024-08-20
* Fix: show score and severity in dependency-check findings [#58](https://github.com/clj-holmes/clj-watson/issues/58)
Expand Down
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,6 @@ clojure -M:clj-watson -p deps.edn
```
```
...
Downloading/Updating database.
Download/Update completed.
...
Dependency Information
-----------------------------------------------------
Expand All @@ -408,7 +405,7 @@ Vulnerabilities
SEVERITY: Information not available.
IDENTIFIERS: CVE-2022-1000000
CVSS: 7.5
CVSS: 7.5 (version 3.1)
PATCHED VERSION: 1.55
SEVERITY: Information not available.
Expand All @@ -418,6 +415,28 @@ PATCHED VERSION: 1.55
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
```

# CVSS Scores & Severities

A Common Vulnerability Scoring System (CVSS) score is a number from `0.0` to `10.0` that conveys the severity of a vulnerability.
There are multiple different scores available, but `clj-watson` will always only report and use the base score.

Over the years, CVSS has been revised a number of times.
As of this writing, you can expect to see versions `2.0`, `3.0`, `3.1`, and `4.0`.
Sometimes, a single vulnerability will specify scores from multiple CVSS versions.
To err on the side of caution, `clj-watson` will always use and report the highest base score.

If you are curious about other scores, you can always bring up the CVE on the NVD NIST website, for an arbitrary example: https://nvd.nist.gov/vuln/detail/CVE-2022-21724.

A severity is `low`, `medium`, `high`, or `critical`, and is based on the CVSS score.
See the [NVD NIST website description for details](https://nvd.nist.gov/vuln-metrics/cvss).

> [!TIP]
> The experimental `github-advisory` strategy has some differences:
> - In addition to `medium` can return a severity of `moderate` which is equivalent to `medium`.
`clj-watson` will always convert `moderate` to `medium` for `github-advisory`.
> - It only populates scores from a single CVSS version.
> - It does not always populate the CVSS score, or populates it with `0.0`.
# Output & Logging

`clj-watson` uses [SLFJ4](https://www.slf4j.org/) and [Logback](https://logback.qos.ch) to collect and filter meaningful log output from its dependencies.
Expand Down
2 changes: 1 addition & 1 deletion resources/full-report.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Vulnerabilities
{% for vulnerability in vulnerable-dependency.vulnerabilities %}
SEVERITY: {{vulnerability.advisory.severity|default:"Information not available."}}
IDENTIFIERS: {% for identifier in vulnerability.advisory.identifiers %}{{identifier.value}} {% endfor %}
CVSS: {{vulnerability.advisory.cvss.score|default:"Information not available."}}
CVSS: {{vulnerability.advisory.cvss.score|default:"Information not available."}} (version {{vulnerability.advisory.cvss.version|default:"Unavailable"}})
PATCHED VERSION: {{vulnerability.firstPatchedVersion.identifier|default:"Information not available."}}
{% endfor %}
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Expand Down
3 changes: 2 additions & 1 deletion resources/github/query-package-vulnerabilities
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ query Vulnerabilities {
severity
cvss {
score
vectorString
}
identifiers {
value
Expand All @@ -23,4 +24,4 @@ query Vulnerabilities {
}
totalCount
}
}
}
39 changes: 35 additions & 4 deletions src/clj_watson/controller/github/vulnerability.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
[clj-watson.diplomat.dependency :as diplomat.dependency]
[clj-watson.diplomat.github.advisory :as diplomat.gh.advisory]
[clj-watson.logic.github.vulnerability :as logic.gh.vulnerability]
[clj-watson.logic.rules.allowlist :as logic.rules.allowlist])
[clj-watson.logic.rules.allowlist :as logic.rules.allowlist]
[clojure.string :as str])
(:import
(java.time ZoneOffset ZonedDateTime)))

Expand All @@ -17,16 +18,43 @@
(when (not (seq vulnerabilities))
latest-version)))

(defn ^:private enrich
"Normalize and enrich the response from GitHub.
- Normalize MODERATE `severity` to MEDIUM
- Extract CVSS version from `vectorString`"
[{:keys [advisory] :as vulnerability}]
(let [{:keys [cvss severity]} advisory
{:keys [vectorString]} cvss
cvss-version (when vectorString
(second (re-find #"^CVSS:(.*?)/" vectorString)))]
(cond-> vulnerability
(= "MODERATE" (str/upper-case severity))
(assoc-in [:advisory :severity] "MEDIUM")

cvss-version
(assoc-in [:advisory :cvss :version] cvss-version)

:always ;; we don't currently include vectorString in our findings
(update-in [:advisory :cvss] dissoc :vectorString))))

(comment
(re-find #"^CVSS:(.*?)/" "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H")
;; => ["CVSS:3.1/" "3.1"]

:eoc)

(defn ^:private scan-dependency
[repositories allow-list {:keys [dependency] :as dependency-info}]
(let [dependency-name-for-github (or (get dependency-rename dependency) dependency)
all-dependency-vulnerabilities (diplomat.gh.advisory/vulnerabilities-by-package dependency-name-for-github)
reported-vulnerabilities (filterv (partial logic.gh.vulnerability/is-version-vulnerable? dependency-info) all-dependency-vulnerabilities)
; not sure how to use it here and avoid always recommend the latest version (logic.gh.vulnerability/version-not-vulnerable all-dependency-vulnerabilities)
filtered-vulnerabilities (remove (partial logic.rules.allowlist/by-pass? allow-list (ZonedDateTime/now ZoneOffset/UTC)) reported-vulnerabilities)
enriched-vulnerabilities (mapv enrich filtered-vulnerabilities)
latest-secure-version (latest-dependency-version dependency all-dependency-vulnerabilities repositories)]
(if (seq filtered-vulnerabilities)
(assoc dependency-info :vulnerabilities filtered-vulnerabilities :secure-version latest-secure-version)
(if (seq enriched-vulnerabilities)
(assoc dependency-info :vulnerabilities enriched-vulnerabilities :secure-version latest-secure-version)
dependency-info)))

(defn scan-dependencies
Expand All @@ -39,6 +67,9 @@
(def repositories {:mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"}
"clojars" {:url "https://repo.clojars.org/"}}})

;; assumes GITHUB_TOKEN is set in your REPL env
(scan-dependencies [{:dependency 'org.apache.commons/commons-compress :mvn/version "1.21"}] repositories {})

(scan-dependencies [{:dependency 'org.postgresql/postgresql :mvn/version "42.2.10"}] repositories {}))
(scan-dependencies [{:dependency 'org.postgresql/postgresql :mvn/version "42.2.10"}] repositories {})

:eoc)
99 changes: 83 additions & 16 deletions src/clj_watson/logic/dependency_check/vulnerability.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@
(:import
(org.owasp.dependencycheck.dependency Vulnerability)))

(defn ^:private cvssv3 [vulnerability]
(try
(some-> vulnerability .getCvssV3 .getCvssData .getBaseScore)
(catch Exception _
nil)))

(defn ^:private severity [vulnerability]
(try
(some-> vulnerability .getCvssV3 .getCvssData .getBaseSeverity str)
(catch Exception _
nil)))
(defn ^:private base-score [cvss]
{:score (.getBaseScore cvss)
:severity (-> cvss .getBaseSeverity str)
:version (-> cvss .getVersion str)})

(defn ^:private cvss-base-scores [vulnerability]
(->> [(some-> vulnerability .getCvssV2 .getCvssData base-score)
(some-> vulnerability .getCvssV3 .getCvssData base-score)
(some-> vulnerability .getCvssV4 .getCvssData base-score)]
(keep identity)))

(defn ^:private highest-cvss-base-score
"To be cautious, we take the highest cvss base score we can find across all cvss versions."
[vulnerability]
(->> (cvss-base-scores vulnerability)
(sort-by (juxt :score :version))
last))

(defn ^:private versions [vulnerability]
(let [vulnerable-software (.getMatchedVulnerableSoftware vulnerability)]
Expand All @@ -25,12 +31,14 @@

(defn ^:private build-vulnerability-map [vulnerability safe-versions]
(let [vulnerability-identifier (.getName vulnerability)
vulnerability-cvss (cvssv3 vulnerability)
vulnerability-severity (severity vulnerability)
summary (format "Vulnerability identified as %s of score %s and severity %s found." vulnerability-identifier vulnerability-cvss vulnerability-severity)]
cvss (highest-cvss-base-score vulnerability)
summary (format "Vulnerability %s with a score of %s and severity of %s found."
vulnerability-identifier
(or (:score cvss) "<unavailable>")
(or (:severity cvss) "<unavailable>"))]
(-> (assoc-in {:advisory {:identifiers []}} [:advisory :identifiers 0 :value] vulnerability-identifier)
(assoc-in [:advisory :cvss :score] vulnerability-cvss)
(assoc-in [:advisory :severity] vulnerability-severity)
(assoc-in [:advisory :cvss] (dissoc cvss :severity))
(assoc-in [:advisory :severity] (:severity cvss))
(assoc-in [:advisory :description] (.getDescription vulnerability))
(assoc-in [:advisory :summary] summary)
(assoc-in [:firstPatchedVersion :identifier] (-> safe-versions first vals first))
Expand All @@ -44,3 +52,62 @@
(->> all-versions
(filter (partial logic.version/newer-and-not-vulnerable-version? cpe-version versions current-version))
(build-vulnerability-map vulnerability)))))

(comment
;; assuming you have an nvd db downloaded...
(import [org.owasp.dependencycheck.data.nvdcve CveDB]
[org.owasp.dependencycheck.utils Settings])

(defn get-vulnerability [cve-id]
(let [cve-db (CveDB. (Settings.))]
(try
(.open cve-db)
(.getVulnerability cve-db cve-id)
(finally
(.close cve-db)))))

(cvss-base-scores (get-vulnerability "CVE-2014-3577"))
;; => ({:score 5.8, :severity "MEDIUM", :version "2.0"})

(cvss-base-scores (get-vulnerability "CVE-2020-8903"))
;; => ({:score 6.9, :severity "MEDIUM", :version "2.0"}
;; {:score 7.8, :severity "HIGH", :version "3.1"}
;; {:score 7.3, :severity "HIGH", :version "4.0"})

(cvss-base-scores (get-vulnerability "CVE-2023-38524"))
;; => ({:score 7.8, :severity "HIGH", :version "3.1"}
;; {:score 2.0, :severity "LOW", :version "4.0"})

(cvss-base-scores (get-vulnerability "CVE-2024-7666"))
;; => ({:score 6.5, :severity "MEDIUM", :version "2.0"}
;; {:score 5.3, :severity "MEDIUM", :version "3.1"}
;; {:score 5.3, :severity "MEDIUM", :version "4.0"})

(cvss-base-scores (get-vulnerability "CVE-2022-32170"))
;; => ()

(build-vulnerability-map (get-vulnerability "CVE-2022-32170") [])
;; => {:advisory
;; {:identifiers [{:value "CVE-2022-32170"}],
;; :cvss nil,
;; :severity nil,
;; :description
;; "The “Bytebase” application does not restrict low privilege user to access admin “projects“ for which an unauthorized user can view the “projects“ created by “Admin” and the affected endpoint is “/api/project?user=${userId}”.",
;; :summary
;; "Vulnerability CVE-2022-32170 with a score of <unavailable> and severity of <unavailable> has been found."},
;; :firstPatchedVersion {:identifier nil},
;; :safe-versions []}

(build-vulnerability-map (get-vulnerability "CVE-2020-8903") [])
;; => {:advisory
;; {:identifiers [{:value "CVE-2020-8903"}],
;; :cvss {:score 7.8, :version "3.1"},
;; :severity "HIGH",
;; :description
;; "A vulnerability in Google Cloud Platform's guest-oslogin versions between 20190304 and 20200507 allows a user that is only granted the role \"roles/compute.osLogin\" to escalate privileges to root. Using their membership to the \"adm\" group, users with this role are able to read the DHCP XID from the systemd journal. Using the DHCP XID, it is then possible to set the IP address and hostname of the instance to any value, which is then stored in /etc/hosts. An attacker can then point metadata.google.internal to an arbitrary IP address and impersonate the GCE metadata server which make it is possible to instruct the OS Login PAM module to grant administrative privileges. All images created after 2020-May-07 (20200507) are fixed, and if you cannot update, we recommend you edit /etc/group/security.conf and remove the \"adm\" user from the OS Login entry.",
;; :summary
;; "Vulnerability CVE-2020-8903 with a score of 7.8 and severity of HIGH has been found."},
;; :firstPatchedVersion {:identifier nil},
;; :safe-versions []}

:eoc)
5 changes: 3 additions & 2 deletions src/clj_watson/logic/sarif.clj
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
:help {:text help-text
:markdown help-text}
:helpUri (format "https://github.com/advisories/%s" identifier)
:properties {:security-severity (-> cvss :score str)}
:properties {:security-severity (some-> cvss :score str)
:cvss cvss}
:defaultConfiguration {:level "error"}}]))

(defn ^:private dependencies->sarif-rules [dependencies]
Expand Down Expand Up @@ -55,4 +56,4 @@
results (dependencies->sarif-results dependencies deps-edn-path)]
(-> sarif-boilerplate
(assoc-in [:runs 0 :tool :driver :rules] rules)
(assoc-in [:runs 0 :results] results))))
(assoc-in [:runs 0 :results] results))))

0 comments on commit 98b040a

Please sign in to comment.