From b17e1b9bcf88c6e1d1233996df247cade3087972 Mon Sep 17 00:00:00 2001 From: lread Date: Mon, 26 Aug 2024 11:30:19 -0400 Subject: [PATCH] Show short summary of findings Include a 2-line summary of findings that reports the number of dependencies scanned, vulnerabilities found, and vulnerabilities broken down by severity. The break down by severity makes no effort to distinguish between CVSS2, CVSS3 and CVSS4 scores. For example, CVSS2 has no Critical severity, so a High CVSS2 could be classified as a Critical CVSS3/CVSS4. For a summary, I think this is fine. Accounts for possibility that data might have unspecified or unrecognized severity values. I think this is less likely for dependency-check (at least today as I've looked at the downloaded db), but have less of an idea of what values github-advisory might return. Some minor cleanups in touched code: - de-duplicated shared scan logic in entrypoint ns - moved logging setup to logging-config ns - change kaocha test reporter to show tests being run Closes #87 --- CHANGELOG.md | 1 + deps.edn | 2 +- .../controller/dependency_check/scanner.clj | 27 ++++--- .../controller/github/vulnerability.clj | 7 +- src/clj_watson/controller/output.clj | 29 +++++++- src/clj_watson/entrypoint.clj | 74 +++++++++---------- src/clj_watson/logging_config.clj | 2 + 7 files changed, 87 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df2ad20..53ad7f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,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) + * Show short summary of findings [#87](https://github.com/clj-holmes/clj-watson/issues/87) * 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) diff --git a/deps.edn b/deps.edn index 488d1aa..0a83859 100644 --- a/deps.edn +++ b/deps.edn @@ -25,7 +25,7 @@ :main-opts ["-m" "clojure-lsp.main"]} :test {:extra-paths ["test"] :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}} - :main-opts ["-m" "kaocha.runner"]} + :main-opts ["-m" "kaocha.runner" "--reporter" "documentation"]} ;; for dev: so we can run the recommended command from the README: :clj-watson {:replace-deps {io.github.clj-holmes/clj-watson {:local/root "."}} diff --git a/src/clj_watson/controller/dependency_check/scanner.clj b/src/clj_watson/controller/dependency_check/scanner.clj index 096fba1..f2b9e83 100644 --- a/src/clj_watson/controller/dependency_check/scanner.clj +++ b/src/clj_watson/controller/dependency_check/scanner.clj @@ -96,16 +96,20 @@ (defn ^:private build-engine [settings] (Engine. settings)) -(defn ^:private clojure-file? [dependency-path] +(defn ^:private jar-file? [dependency-path] (string/ends-with? dependency-path ".jar")) -(defn ^:private scan-jars [engine dependencies] +(defn ^:private deps->jars [dependencies] (->> dependencies + ;; as far as I understand, a dep will only ever point to a single jar + ;; but we exclude non-jar deps and perhaps local paths (map :paths) (apply concat) - (filter clojure-file?) - (map io/file) - (.scan engine)) + (filter jar-file?) + (map io/file))) + +(defn ^:private scan-jars [engine jars] + (.scan engine jars) (.analyzeDependencies engine) engine) @@ -114,8 +118,11 @@ (let [settings (create-settings dependency-check-properties clj-watson-properties)] (when-let [{:keys [exit]} (validate-settings settings opts)] (System/exit exit)) - (with-open [engine (build-engine settings)] - (-> engine - (scan-jars dependencies) - (.getDependencies) - (Arrays/asList))))) + (let [jars (deps->jars dependencies) + vulnerable-jars (with-open [engine (build-engine settings)] + (-> engine + (scan-jars jars) + (.getDependencies) + (Arrays/asList)))] + {:deps-scanned (count jars) + :findings vulnerable-jars}))) diff --git a/src/clj_watson/controller/github/vulnerability.clj b/src/clj_watson/controller/github/vulnerability.clj index a508d35..14fe934 100644 --- a/src/clj_watson/controller/github/vulnerability.clj +++ b/src/clj_watson/controller/github/vulnerability.clj @@ -59,9 +59,10 @@ (defn scan-dependencies [dependencies repositories allow-list] - (->> dependencies - (pmap (partial scan-dependency repositories allow-list)) - (filterv :vulnerabilities))) + {:deps-scanned (count dependencies) + :findings (->> dependencies + (pmap (partial scan-dependency repositories allow-list)) + (filterv :vulnerabilities))}) (comment (def repositories {:mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"} diff --git a/src/clj_watson/controller/output.clj b/src/clj_watson/controller/output.clj index bb7ee7e..318a97a 100644 --- a/src/clj_watson/controller/output.clj +++ b/src/clj_watson/controller/output.clj @@ -4,7 +4,8 @@ [clj-watson.logic.sarif :as logic.sarif] [clj-watson.logic.template :as logic.template] [clojure.java.io :as io] - [clojure.pprint :as pprint])) + [clojure.pprint :as pprint] + [clojure.string :as str])) (defmulti ^:private generate* (fn [_ _ kind] kind)) @@ -27,3 +28,29 @@ (defn generate [dependencies deps-edn-path kind] (generate* dependencies deps-edn-path kind)) + +(defn final-summary + "See `clj-waston.logic.summarize/summary` for description" + [{:keys [cnt-deps-scanned cnt-deps-vulnerable + cnt-deps-severities cnt-deps-unexpected-severities cnt-deps-unspecified-severity]}] + (let [details (->> [(when (seq cnt-deps-severities) + (format "(%s)" + (->> cnt-deps-severities + (mapv (fn [[severity count]] (format "%d %s" count severity))) + (str/join ", ")))) + (when (seq cnt-deps-unexpected-severities) + (format "Unrecognized severities: (%s)" + (->> cnt-deps-unexpected-severities + (mapv (fn [[severity count]] (format "%d %s" count severity))) + (str/join ", ")))) + (when (and cnt-deps-unspecified-severity (> cnt-deps-unspecified-severity 0)) + (format "Unspecified severities: %d" cnt-deps-unspecified-severity))] + (keep identity))] + ;; Dependencies scanned: 151 + ;; Vulnerable dependencies found: 9 (4 Critical, 1 High, 2 Medium), Unrecognize d severities: (2 Foobar), Unspecified severities: 3 + (println (format "Dependencies scanned: %d" cnt-deps-scanned)) + (println (format "Vulnerable dependencies found: %d%s" + cnt-deps-vulnerable + (if (seq details) + (str " " (str/join ", " details)) + ""))))) diff --git a/src/clj_watson/entrypoint.clj b/src/clj_watson/entrypoint.clj index f6089c3..3308b91 100644 --- a/src/clj_watson/entrypoint.clj +++ b/src/clj_watson/entrypoint.clj @@ -9,51 +9,42 @@ [clj-watson.controller.output :as controller.output] [clj-watson.controller.remediate :as controller.remediate] [clj-watson.logging-config :as logging-config] + [clj-watson.logic.summarize :as summarize] [clojure.java.io :as io] [clojure.tools.reader.edn :as edn])) (defmulti scan* (fn [{:keys [database-strategy]}] database-strategy)) - -(defmethod scan* :github-advisory [{:keys [deps-edn-path suggest-fix aliases]}] - (let [{:keys [deps dependencies]} (controller.deps/parse deps-edn-path aliases) - repositories (select-keys deps [:mvn/repos]) - config (when-let [config-file (io/resource "clj-watson-config.edn")] +(defmethod scan* :github-advisory [{:keys [dependencies repositories]}] + (let [config (when-let [config-file (io/resource "clj-watson-config.edn")] (edn/read-string (slurp config-file))) - allow-list (adapter.config/config->allow-config-map config) - vulnerable-dependencies (controller.gh.vulnerability/scan-dependencies dependencies repositories allow-list)] - (if suggest-fix - (controller.remediate/scan vulnerable-dependencies deps) - vulnerable-dependencies))) + allow-list (adapter.config/config->allow-config-map config)] + (controller.gh.vulnerability/scan-dependencies dependencies repositories allow-list))) -(defmethod scan* :dependency-check [{:keys [deps-edn-path suggest-fix aliases - dependency-check-properties clj-watson-properties] :as opts}] - ;; dependency-check uses Apache Commons JCS, ask it to use log4j2 to allow us to configure its noisy logging - (System/setProperty "jcs.logSystem" "log4j2") - (let [{:keys [deps dependencies]} (controller.deps/parse deps-edn-path aliases) - repositories (select-keys deps [:mvn/repos]) - scanned-dependencies (controller.dc.scanner/start! dependencies - dependency-check-properties - clj-watson-properties - opts) - vulnerable-dependencies (controller.dc.vulnerability/extract scanned-dependencies dependencies repositories)] - (if suggest-fix - (controller.remediate/scan vulnerable-dependencies deps) - vulnerable-dependencies))) +(defmethod scan* :dependency-check [{:keys [dependency-check-properties clj-watson-properties + dependencies repositories] :as opts}] + (let [{:keys [findings] :as result} + (controller.dc.scanner/start! dependencies + dependency-check-properties + clj-watson-properties + opts)] + (assoc result :findings (controller.dc.vulnerability/extract findings dependencies repositories)))) (defmethod scan* :default [opts] - (scan* (assoc opts :database-strategy "dependency-check"))) + (scan* (assoc opts :database-strategy :dependency-check))) -(defn do-scan - "Indirect entry point for -M usage." - [opts] +(defn do-scan [{:keys [fail-on-result output deps-edn-path aliases suggest-fix] :as opts}] (logging-config/init) - (let [{:keys [fail-on-result output deps-edn-path]} opts - vulnerabilities (scan* opts) - contains-vulnerabilities? (->> vulnerabilities - (map (comp empty? :vulnerabilities)) - (some false?))] - (controller.output/generate vulnerabilities deps-edn-path output) - (if (and contains-vulnerabilities? fail-on-result) + (let [{:keys [deps dependencies]} (controller.deps/parse deps-edn-path aliases) + repositories (select-keys deps [:mvn/repos]) + {:keys [findings] :as result} (scan* (assoc opts + :dependencies dependencies + :repositories repositories)) + findings (if suggest-fix + (controller.remediate/scan findings deps) + findings)] + (controller.output/generate findings deps-edn-path output) + (-> result summarize/summarize controller.output/final-summary) + (if (and (seq findings) fail-on-result) (System/exit 1) (System/exit 0)))) @@ -64,11 +55,14 @@ (do-scan (cli-spec/validate-tool-opts opts))) (comment - (def vulnerabilities (scan* {:deps-edn-path "resources/vulnerable-deps.edn" - :database-strategy "dependency-check" - :suggest-fix true})) + (def vulnerabilities (do-scan {:deps-edn-path "resources/vulnerable-deps.edn" + :database-strategy :dependency-check + :suggest-fix true})) (def vulnerabilities (scan* {:deps-edn-path "resources/vulnerable-deps.edn" - :database-strategy "github-advisory"})) + :database-strategy :github-advisory})) + (controller.output/generate vulnerabilities "deps.edn" "sarif") - (controller.output/generate vulnerabilities "deps.edn" "stdout-simple")) + (controller.output/generate vulnerabilities "deps.edn" "stdout-simple") + + :eoc) diff --git a/src/clj_watson/logging_config.clj b/src/clj_watson/logging_config.clj index 183a157..3e59da4 100644 --- a/src/clj_watson/logging_config.clj +++ b/src/clj_watson/logging_config.clj @@ -34,6 +34,8 @@ (defn init "Complement `resources/logaback.xml` with some customizations" [] + ;; dependency-check uses Apache Commons JCS, ask it to use log4j2 to allow us to configure its noisy logging + (System/setProperty "jcs.logSystem" "log4j2") (.addTurboFilter (LoggerFactory/getILoggerFactory) (create-custom-filter))) (comment