Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Review CVSS score handling & reporting #118

Merged
merged 2 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the old code, each of these chains of calls is wrapped in try / catch -- is there any danger that the call chains can actually throw or was the old code overly defensive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My feeling was the old code was hiding potential issues.
I don't see why we should normally encounter an exception here (and didn't see a rationale in the git history).
If we learn otherwise, we can adapt and address.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I wasn't sure whether those methods were declared to throw exceptions.

{: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))))