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

add sarif output support #19

Merged
merged 19 commits into from
Mar 14, 2022
Merged
1 change: 1 addition & 0 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
cli-matic/cli-matic {:mvn/version "0.4.3"}
selmer/selmer {:mvn/version "1.12.50"}
org.slf4j/slf4j-nop {:mvn/version "2.0.0-alpha6"}
borkdude/edamame {:mvn/version "0.0.19"}
org.clojure/tools.deps.alpha {:mvn/version "0.12.1148"}
org.owasp/dependency-check-core {:mvn/version "6.5.3"}
org.apache.maven.resolver/maven-resolver-transport-http {:mvn/version "1.7.3"}}
Expand Down
2 changes: 2 additions & 0 deletions resources/github/query-package-vulnerabilities
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ query Vulnerabilities {
nodes {
vulnerableVersionRange
advisory {
description
summary
severity
cvss {
score
Expand Down
22 changes: 22 additions & 0 deletions resources/sarif-help.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Dependency
## Information
Name: {{vulnerable-dependency.dependency}}
Version: {{vulnerable-dependency.mvn/version}}

## Vulnerabilities
{% for identifier in identifiers %}
- {{identifier.value}}
{% endfor %}

## Location
{% if vulnerable-dependency.parents|empty? %}
Impossible to find dependency in tree.
It could be a jar inside some project.

{% else %}
{{vulnerable-dependency.parents|build-tree}}
{% endif %}
# Fix suggestion
```clojure
{{vulnerable-dependency.remediate-suggestion|safe}}
```
2 changes: 1 addition & 1 deletion src/clj_watson/cli.clj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
:default :present
:as "path of deps.edn to scan."}
{:option "output" :short "o"
:type #{"json" "edn" "stdout" "stdout-simple"} ; keep stdout type to avoid break current automations
:type #{"json" "edn" "stdout" "stdout-simple" "sarif"} ; keep stdout type to avoid break current automations
:default "stdout"
:as "Output type."}
{:option "aliases" :short "a"
Expand Down
33 changes: 29 additions & 4 deletions src/clj_watson/controller/deps.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
(ns clj-watson.controller.deps
(:require
[clojure.set :refer [rename-keys]]
[clojure.tools.deps.alpha :as deps]
[clojure.tools.deps.alpha.util.maven :as maven])
[clojure.tools.deps.alpha.util.maven :as maven]
[edamame.core :refer [parse-string]])
(:import
(java.io File)))

Expand All @@ -11,18 +13,41 @@
(coll? aliases) (map keyword aliases)
:else []))

(defn ^:private dependencies-map->dependencies-vector [dependencies]
(defn ^:private dependencies-map->dependencies-vector [dependencies dependencies-physical-location]
(reduce (fn [dependency-vector [dependency-name dependency-info]]
(conj dependency-vector (assoc dependency-info :dependency dependency-name)))
(let [dependency-physical-location (or (get dependencies-physical-location dependency-name)
(->> dependency-info
:parents
ffirst
(get dependencies-physical-location)))]
(->> (assoc dependency-info :dependency dependency-name :physical-location dependency-physical-location)
(conj dependency-vector))))
[] dependencies))

(defn ^:private rename-location-keys [location]
(rename-keys location {:row :startLine :end-row :endLine :col :startColumn :end-col :endColumn}))

(defn ^:private deps->dependencies-location [deps-path]
(let [deps (-> deps-path slurp parse-string)]
(->> (tree-seq coll? identity deps)
(filter symbol?)
(reduce (fn [locations element]
(if-let [location (some-> element meta rename-location-keys)]
(assoc locations element location)
locations))
{}))))

(defn parse [^String deps-path aliases]
(let [project-deps (-> deps-path File. deps/slurp-deps (update :mvn/repos merge maven/standard-repos))
aliases (build-aliases project-deps aliases)
dependencies-physical-location (deps->dependencies-location deps-path)
aliases-resolver {:resolve-args (deps/combine-aliases project-deps aliases)
:classpath-args (deps/combine-aliases project-deps aliases)}]
{:deps project-deps
:dependencies (-> project-deps (deps/calc-basis aliases-resolver) :libs dependencies-map->dependencies-vector)}))
:dependencies (-> project-deps
(deps/calc-basis aliases-resolver)
:libs
(dependencies-map->dependencies-vector dependencies-physical-location))}))

(comment
(parse "resources/vulnerable-deps.edn" nil))
24 changes: 14 additions & 10 deletions src/clj_watson/controller/output.clj
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
(ns clj-watson.controller.output
(:require
[cheshire.core :as json]
[clj-watson.logic.stdout :as logic.stdout]
[clj-watson.logic.sarif :as logic.sarif]
[clj-watson.logic.template :as logic.template]
[clojure.java.io :as io]
[clojure.pprint :as pprint]))

(defmulti ^:private generate* (fn [_ kind] (keyword kind)))
(defmulti ^:private generate* (fn [_ _ kind] (keyword kind)))

(defmethod ^:private generate* :stdout-simple [dependencies _]
(defmethod ^:private generate* :stdout-simple [dependencies & _]
(let [template (-> "simple-report.mustache" io/resource slurp)]
(println (logic.stdout/generate dependencies template))))
(println (logic.template/generate {:vulnerable-dependencies dependencies} template))))

(defmethod ^:private generate* :stdout [dependencies _]
(defmethod ^:private generate* :stdout [dependencies & _]
(let [template (-> "full-report.mustache" io/resource slurp)]
(println (logic.stdout/generate dependencies template))))
(println (logic.template/generate {:vulnerable-dependencies dependencies} template))))

(defmethod ^:private generate* :json [dependencies _]
(defmethod ^:private generate* :json [dependencies & _]
(-> dependencies json/generate-string pprint/pprint))

(defmethod ^:private generate* :edn [dependencies _]
(defmethod ^:private generate* :edn [dependencies & _]
(pprint/pprint dependencies))

(defn generate [dependencies kind]
(generate* dependencies kind))
(defmethod ^:private generate* :sarif [dependencies deps-edn-path & _]
(-> dependencies (logic.sarif/generate deps-edn-path) json/generate-string println))

(defn generate [dependencies deps-edn-path kind]
(generate* dependencies deps-edn-path kind))
8 changes: 4 additions & 4 deletions src/clj_watson/entrypoint.clj
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
(defmethod scan* :default [opts]
(scan* (assoc opts :database-strategy "dependency-check")))

(defn scan [{:keys [fail-on-result output] :as opts}]
(defn scan [{:keys [fail-on-result output deps-edn-path] :as opts}]
(let [vulnerabilities (scan* opts)]
(controller.output/generate vulnerabilities output)
(controller.output/generate vulnerabilities deps-edn-path output)
(if (and (-> vulnerabilities count (> 0)) fail-on-result)
(System/exit 1)
(System/exit 0))))
Expand All @@ -45,5 +45,5 @@
:database-strategy "github-advisory"
:suggest-fix true}))

(controller.output/generate vulnerabilities "stdout")
(controller.output/generate vulnerabilities "stdout-simple"))
(controller.output/generate vulnerabilities "deps.edn" "sarif")
(controller.output/generate vulnerabilities "deps.edn" "stdout-simple"))
16 changes: 11 additions & 5 deletions src/clj_watson/logic/dependency_check/vulnerability.clj
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@
:version-start-including (.getVersionStartIncluding vulnerable-software)}))

(defn ^:private build-vulnerability-map [vulnerability safe-versions]
(-> (assoc-in {} [:advisory :identifiers] [{:value (.getName vulnerability)}])
(assoc-in [:advisory :cvss :score] (cvssv3 vulnerability))
(assoc-in [:advisory :severity] (severity vulnerability))
(assoc-in [:firstPatchedVersion :identifier] (-> safe-versions first vals first))
(assoc :safe-versions 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)]
(-> (assoc-in {} [:advisory :identifiers 0 :value] vulnerability-identifier)
(assoc-in [:advisory :cvss :score] vulnerability-cvss)
(assoc-in [:advisory :severity] vulnerability-severity)
(assoc-in [:advisory :description] (.getDescription vulnerability))
(assoc-in [:advisory :summary] summary)
(assoc-in [:firstPatchedVersion :identifier] (-> safe-versions first vals first))
(assoc :safe-versions safe-versions))))

(defn get-information
[current-version all-versions ^Vulnerability vulnerability]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
(ns clj-watson.logic.stdout
(:require
[selmer.filters :refer [add-filter!]]
[selmer.parser :refer [render]]))
(ns clj-watson.logic.formatter)

(defn ^:private dependencies-hierarchy-to-tree* [tree]
(loop [text ""
Expand All @@ -14,15 +11,10 @@
new-text (format "%s%s[%s]\n" text tabs dependency)]
(recur new-text (inc count) (next dependencies))))))

(defn ^:private dependencies-hierarchy-to-tree [trees]
(defn dependencies-hierarchy-to-tree [trees]
(if (and (= 1 (count trees)) (every? empty? trees))
"Direct dependency."
(->> trees
reverse
(map dependencies-hierarchy-to-tree*)
(reduce #(str %1 "\n" %2)))))

(add-filter! :build-tree dependencies-hierarchy-to-tree)

(defn generate [dependencies template]
(render template {:vulnerable-dependencies dependencies}))
(reduce #(str %1 "\n" %2)))))
58 changes: 58 additions & 0 deletions src/clj_watson/logic/sarif.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
(ns clj-watson.logic.sarif
(:require
[clj-watson.logic.template :as logic.template]
[clojure.java.io :as io]
[clojure.string :as string]))

(def ^:private sarif-boilerplate
{:$schema "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json"
:version "2.1.0"
:runs [{:tool
{:driver {:name "clj-watson"
:informationUri "https://github.com/clj-holmes/clj-watson"
:version "3.0.2"}}}]})

(defn ^:private advisory->sarif-rule [dependency dependency-info {{:keys [description summary identifiers cvss]} :advisory}]
(let [identifier (-> identifiers first :value)
; needs to remove it from here
template (-> "sarif-help.mustache" io/resource slurp)
help-text (logic.template/generate {:vulnerable-dependency dependency-info :identifiers identifiers} template)]
[{:id identifier
:name (format "VulnerableDependency%s" (-> dependency name string/capitalize))
:shortDescription {:text summary}
:fullDescription {:text description}
:help {:text help-text
:markdown help-text}
:helpUri (format "https://github.com/advisories/%s" identifier)
:properties {:security-severity (-> cvss :score str)}
:defaultConfiguration {:level "error"}}]))

(defn ^:private dependencies->sarif-rules [dependencies]
(->> dependencies
(map (fn [{:keys [dependency vulnerabilities] :as dependency-info}]
(->> vulnerabilities
(map #(advisory->sarif-rule dependency dependency-info %))
(reduce concat))))
(reduce concat)))

(defn ^:private advisory->sarif-result
[filename physical-location dependency {{:keys [identifiers]} :advisory}]
{:ruleId (-> identifiers first :value)
:message {:text (format "Vulnerability found in direct dependency %s" dependency)}
:locations [{:physicalLocation
{:artifactLocation {:uri filename}
:region physical-location}}]})

(defn ^:private dependencies->sarif-results [dependencies deps-edn-path]
(->> dependencies
(map (fn [{:keys [dependency vulnerabilities physical-location]}]
(->> vulnerabilities
(map #(advisory->sarif-result deps-edn-path physical-location dependency %)))))
(reduce concat)))

(defn generate [dependencies deps-edn-path]
(let [rules (dependencies->sarif-rules dependencies)
results (dependencies->sarif-results dependencies deps-edn-path)]
(-> sarif-boilerplate
(assoc-in [:runs 0 :tool :driver :rules] rules)
(assoc-in [:runs 0 :results] results))))
10 changes: 10 additions & 0 deletions src/clj_watson/logic/template.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
(ns clj-watson.logic.template
(:require
[clj-watson.logic.formatter :refer [dependencies-hierarchy-to-tree]]
[selmer.filters :refer [add-filter!]]
[selmer.parser :refer [render]]))

(add-filter! :build-tree dependencies-hierarchy-to-tree)

(defn generate [dependencies template]
(render template dependencies))