From 6546e3a5a5c3929c79a2e68ceec1f3602d1a5bb2 Mon Sep 17 00:00:00 2001 From: lread Date: Sat, 31 Aug 2024 13:30:04 -0400 Subject: [PATCH] Add missing files for final summary Did someone forget to add his new files? Yes, indeed, he did. Addendum for #87 --- src/clj_watson/logic/summarize.clj | 100 ++++++++++++++++++ .../unit/controller/output_test.clj | 29 +++++ test/clj_watson/unit/logic/summarize_test.clj | 72 +++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 src/clj_watson/logic/summarize.clj create mode 100644 test/clj_watson/unit/controller/output_test.clj create mode 100644 test/clj_watson/unit/logic/summarize_test.clj diff --git a/src/clj_watson/logic/summarize.clj b/src/clj_watson/logic/summarize.clj new file mode 100644 index 0000000..4ab114f --- /dev/null +++ b/src/clj_watson/logic/summarize.clj @@ -0,0 +1,100 @@ +(ns clj-watson.logic.summarize + (:require + [clojure.string :as str])) + +(def ^:private known-severities ["Critical" "High" "Medium" "Low"]) + +(def ^:private known-severities-sort-order + (reduce (fn [acc [ndx k]] + (assoc acc k ndx)) + {} + (map-indexed vector known-severities))) + +(defn- sort-by-severity + "Sort by: + - recognized severity sort order + - unrecognized severity by cvss score" + [vulnerabilities] + (sort-by (fn [{:keys [advisory]}] + (let [severity (:severity advisory) + severity-order (get known-severities-sort-order severity) + score (some-> advisory :cvss :score -) + unknown-severity (when-not severity-order + severity)] + [(or severity-order 999) + (or score 999) + unknown-severity])) + vulnerabilities)) + +(defn- highest-severity [finding] + (->> finding + :vulnerabilities + sort-by-severity + first + :advisory + :severity)) + +(defn summarize + "Account for fact that data is coming from external sources that do not guarantee sensible values. + Separate expected severities from potentially unrecognized and potentially unspecified. + + A dep can have multiple advisories, we choose the one with the highest severity. + + This summary makes no attempt to distinguish CVSS2 vs CVSS3 vs CVSS4. + CVSS3 and CVSS4 are similar but CVSS2, for example, has no Critical severity." + [{:keys [deps-scanned findings]}] + ;; Dependencies scanned: 151 + ;; Vulnerable dependencies found: 9 (4 Critical, 1 High, 2 Medium), Unrecognized severities: (2 Foobar), Unspecified severities: 3 + (let [findings (mapv #(assoc % :highest-severity (highest-severity %)) findings) + severity-counts (->> findings + (mapv #(some-> % :highest-severity str/capitalize)) + (frequencies)) + {:keys [expected unexpected unspecified]} (reduce-kv (fn [m k v] + (cond + (nil? k) (assoc m :unspecified v) + (get known-severities-sort-order k) (assoc-in m [:expected k] v) + :else (assoc-in m [:unexpected k] v))) + {:unspecified 0} + severity-counts) + expected (->> expected + (into []) + (sort-by #(->> % first (get known-severities-sort-order)))) + unexpected (->> unexpected + (into []) + (sort-by first))] + {:cnt-deps-scanned deps-scanned + :cnt-deps-vulnerable (count findings) + :cnt-deps-severities expected + :cnt-deps-unexpected-severities unexpected + :cnt-deps-unspecified-severity unspecified})) + +(comment + (sort-by-severity [{:advisory {:severity "Critical"}} + {:advisory {:severity "Foo"}} + {:advisory {:severity "Medium"}} + {:advisory {:severity "Aba"}} + {:advisory {:severity "Blah" :cvss {:score 8.0}}} + {:advisory {:severity "Bah" :cvss {:score 9.0}}} + {:advisory {:severity "Low"}}]) + ;; => ({:advisory {:severity "Critical"}} + ;; {:advisory {:severity "Medium"}} + ;; {:advisory {:severity "Low"}} + ;; {:advisory {:severity "Bah", :cvss {:score 9.0}}} + ;; {:advisory {:severity "Blah", :cvss {:score 8.0}}} + ;; {:advisory {:severity "Aba"}} + ;; {:advisory {:severity "Foo"}}) + + (sort-by-severity [{:advisory {:severity "Foo"}} + {:advisory {:severity "Aba"}} + {:advisory {:severity "Bar" :cvss {:score 9.9}}} + {:advisory {:severity "Alpha" :cvss {:score 9.8}}}]) + ;; => ({:advisory {:severity "Bar", :cvss {:score 9.9}}} + ;; {:advisory {:severity "Alpha", :cvss {:score 9.8}}} + ;; {:advisory {:severity "Aba"}} + ;; {:advisory {:severity "Foo"}}) + + (sort-by-severity [{:advisory {:severity "Foo"}} + {:advisory {:severity "Bar"}}]) + ;; => ({:advisory {:severity "Bar"}} {:advisory {:severity "Foo"}}) + + :eoc) diff --git a/test/clj_watson/unit/controller/output_test.clj b/test/clj_watson/unit/controller/output_test.clj new file mode 100644 index 0000000..c20797b --- /dev/null +++ b/test/clj_watson/unit/controller/output_test.clj @@ -0,0 +1,29 @@ +(ns clj-watson.unit.controller.output-test + (:require + [clj-watson.controller.output :as output] + [clojure.test :refer [deftest is]])) + +(deftest all-good-test + (is (= (str "Dependencies scanned: 72\n" + "Vulnerable dependencies found: 0\n") + (with-out-str (output/final-summary {:cnt-deps-scanned 72 + :cnt-deps-vulnerable 0}))))) + +(deftest expected-severities-test + (is (= (str "Dependencies scanned: 72\n" + "Vulnerable dependencies found: 10 (3 Critical, 2 High, 1 Medium, 4 Low)\n") + (with-out-str (output/final-summary {:cnt-deps-scanned 72 + :cnt-deps-vulnerable 10 + :cnt-deps-unspecified-severity 0 + :cnt-deps-severities [["Critical" 3] + ["High" 2] + ["Medium" 1] + ["Low" 4]]}))))) +(deftest unexpected-and-unspecified-severities-test + (is (= (str "Dependencies scanned: 72\n" + "Vulnerable dependencies found: 11 (7 Critical), Unrecognized severities: (2 Foo, 7 Bar), Unspecified severities: 7\n") + (with-out-str (output/final-summary {:cnt-deps-scanned 72 + :cnt-deps-vulnerable 11 ;; garbage in, garbage out + :cnt-deps-unspecified-severity 7 + :cnt-deps-unexpected-severities [["Foo" 2] ["Bar" 7]] + :cnt-deps-severities [["Critical" 7]]}))))) diff --git a/test/clj_watson/unit/logic/summarize_test.clj b/test/clj_watson/unit/logic/summarize_test.clj new file mode 100644 index 0000000..3bd1086 --- /dev/null +++ b/test/clj_watson/unit/logic/summarize_test.clj @@ -0,0 +1,72 @@ +(ns clj-watson.unit.logic.summarize-test + (:require + [clj-watson.logic.summarize :as summarize] + [clojure.test :refer [deftest is]])) + +(deftest all-good-test + (is (= {:cnt-deps-scanned 42 + :cnt-deps-vulnerable 0 + :cnt-deps-severities [] + :cnt-deps-unexpected-severities [] + :cnt-deps-unspecified-severity 0} + (summarize/summarize {:deps-scanned 42 :findings []})))) + +(deftest expected-severities-test + (is (= {:cnt-deps-scanned 97 + :cnt-deps-vulnerable 7 + :cnt-deps-severities [["Critical" 1] ["High" 3] ["Medium" 2] ["Low" 1]] + :cnt-deps-unexpected-severities [] + :cnt-deps-unspecified-severity 0} + (summarize/summarize + {:deps-scanned 97 + :findings [{:vulnerabilities [{:advisory {:severity "High"}}]} ;; high wins + {:vulnerabilities [{:advisory {:severity "Wtf"}} + {:advisory {:severity "Low"}} + {:advisory {:severity "High"}}]} ;; high wins + {:vulnerabilities [{:advisory {:severity "High"}} ;; high wins + {:advisory {:severity "Medium"}} + {:advisory {:severity "Wtf"}}]} + {:vulnerabilities [{:advisory {:severity "Foo"}} + {:advisory {:severity "Low"}}]} ;; low wins + {:vulnerabilities [{:advisory {:severity "High"}} + {:advisory {:severity "Low"}} + {:advisory {:severity "Medium"}} + {:advisory {:severity "Critical"}} + {:advisory {:severity "Wtf"}}]} ;; critical wins + {:vulnerabilities [{:advisory {:severity "Medium"}}]} ;; medium wins + {:vulnerabilities [{:advisory {:severity "Low"}} + {:advisory {:severity "Medium"}}]} ;; medium wins ]} + ]})))) + +(deftest unexpected-and-missing-severities-test + (is (= {:cnt-deps-scanned 7123 + :cnt-deps-vulnerable 10 + :cnt-deps-severities [["Critical" 1] ["High" 2] ["Medium" 1]] + :cnt-deps-unexpected-severities [["Bar" 2] ["Foo" 1]] + :cnt-deps-unspecified-severity 3} + (summarize/summarize + {:deps-scanned 7123 + :findings [{:vulnerabilities [{:advisory {:severity "High"}}]} ;; high wins + {:vulnerabilities [{:advisory {:severity "Wtf"}} + {:advisory {:severity "Low"}} + {:advisory {:severity "High"}} + {:advisory {:severity "High"}}]} ;; high wins + {:vulnerabilities [{:advisory {:severity "Foo"}} + {:advisory {:severity "Bar"}}]} ;; bar wins + {:vulnerabilities [{:advisory {:severity "Foo"}}]} ;; foo wins + {} ;; unspecified + {:vulnerabilities []} ;; unspecified + {:vulnerabilities [{:advisory {:severity nil}}]} ;; unspecified + {:vulnerabilities [{:advisory {:severity "High"}} + {:advisory {:severity "Low"}} + {:advisory {:severity "Low"}} + {:advisory {:severity "Medium"}} + {:advisory {:severity "Low"}} + {:advisory {:severity "Medium"}} + {:advisory {:severity "Critical"}} ;; critical wins + {:advisory {:severity "Wtf"}}]} + {:vulnerabilities [{:advisory {:severity "Medium"}}]} ;; medium wins + {:vulnerabilities [{:advisory {:severity "Foo"}} + {:advisory {:severity "Aba"}} + {:advisory {:severity "Bar" :cvss {:score 9.9}}} ;; bar wins + {:advisory {:severity "Alpha" :cvss {:score 9.8}}}]}]}))))