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