Skip to content

Commit

Permalink
Merge branch 'main' of github.com:clj-holmes/clj-watson
Browse files Browse the repository at this point in the history
  • Loading branch information
mthbernardes committed Dec 15, 2022
2 parents 92c0397 + 93ca7dd commit 25e873a
Show file tree
Hide file tree
Showing 16 changed files with 240 additions and 37 deletions.
3 changes: 3 additions & 0 deletions .clj-kondo/config.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{:linters {:unresolved-symbol {:exclude []}
:refer-all {:exclude [clojure.test]}}
:output {:show-rule-name-in-message true}}
2 changes: 2 additions & 0 deletions .github/workflows/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
patreon: mthbernardes
ko_fi: mthbernardes
34 changes: 33 additions & 1 deletion .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,38 @@ on:
workflow_dispatch:

jobs:
tests:
strategy:
matrix:
namespace: [ unit ]
operating-system: [ubuntu-latest]

runs-on: ${{ matrix.operating-system }}

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Prepare java
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '11'

- name: Install clojure tools-deps
uses: DeLaGuardo/setup-clojure@master
with:
tools-deps: 1.11.1.1113

- name: Cache Maven packages
uses: actions/[email protected]
with:
path: ~/.m2
key: ${{ runner.os }}-m2-tests-${{ hashFiles('**/deps.edn') }}
restore-keys: ${{ runner.os }}-m2-pr

- name: Execute ${{ matrix.namespace }} Test
run: clojure -M:test ${{ matrix.namespace }}

check-lint:

Expand Down Expand Up @@ -65,4 +97,4 @@ jobs:
uses: clj-holmes/clj-holmes-action@main
with:
output-type: 'stdout'
fail-on-result: 'true'
fail-on-result: 'true'
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ pom.xml.asc
.idea/
.cpcache/
*.iml
.clj-kondo/
.clj-kondo/*/
target/
.lsp/
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ But there's a requirements to use it, it's necessary to generate a [Github PAT (
Another important thing is that the api has a limit of 5K requests per hour/per PAT.
If you create a PAT or uses the github action token just set it in as an environment variabe named `GITHUB_TOKEN` to clj-watson be able to use it.

#### Allow Listing Known CVE's

Sometimes the dependency tree is not under your control and overrides are not possible,
but you can allways allow a CVE for a limited period by adding a config file at `resources/clj-watson-config.edn`:

```clojure
{:allow-list {:cves [{:cve-label "CVE-0000"
:expires "2000-01-01"}
{:cve-label "CVE-00000"
:expires "2000-01-01"}]}}
```

## Remediation suggestion
#### The big difference from clj-watson to other tools.
Since fixing the found vulnerabilities manually could be truly frustrating `clj-watson` provides a way to suggest a remediation.
Expand All @@ -38,18 +50,18 @@ In order to get the auto remediate suggestion it's necessary to provide a `--sug
# Installation
It's possible to install clj-watson as a clojure tool and invoke it.
```bash
$ clojure -Ttools install io.github.clj-holmes/clj-watson '{:git/tag "v4.0.0" :git/sha "9972a33"}' :as clj-watson
$ clojure -Ttools install io.github.clj-holmes/clj-watson '{:git/tag "v4.1.1" :git/sha "efa3420"}' :as clj-watson
$ clojure -Tclj-watson scan '{:output "stdout" :dependency-check-properties nil :fail-on-result true :deps-edn-path "deps.edn" :suggest-fix true :aliases ["*"] :database-strategy "dependency-check"}'
```
It can also be called directly.
```bash
$ clojure -Sdeps '{:deps {io.github.clj-holmes/clj-watson {:git/tag "v4.0.0" :git/sha "9972a33"}}}' -M -m clj-watson.cli scan -p deps.edn
$ clojure -Sdeps '{:deps {io.github.clj-holmes/clj-watson {:git/tag "v4.1.1" :git/sha "efa3420"}}}' -M -m clj-watson.cli scan -p deps.edn
```
Or you can just add it to your project `deps.edn`
```clojure
{:deps {}
:aliases
{:clj-watson {:extra-deps {io.github.clj-holmes/clj-watson {:git/tag "v4.0.0" :git/sha "9972a33"}}
{:clj-watson {:extra-deps {io.github.clj-holmes/clj-watson {:git/tag "v4.1.1" :git/sha "efa3420"}}
:main-opts ["-m" "clj-watson.cli" "scan"]}}}
```

Expand Down
28 changes: 17 additions & 11 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{:deps {org.clojure/clojure {:mvn/version "1.11.0-beta1"}
{:deps {org.clojure/clojure {:mvn/version "1.11.1"}
version-clj/version-clj {:mvn/version "2.0.2"}
clj-http/clj-http {:mvn/version "3.12.3"}
cheshire/cheshire {:mvn/version "5.10.2"}
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 "7.0.0"}
org.apache.maven.resolver/maven-resolver-transport-http {:mvn/version "1.7.3"}}

cheshire/cheshire {:mvn/version "5.11.0"}
cli-matic/cli-matic {:mvn/version "0.5.4"}
selmer/selmer {:mvn/version "1.12.55"}
org.slf4j/slf4j-nop {:mvn/version "2.0.6"}
borkdude/edamame {:mvn/version "1.0.16"}
org.clojure/tools.deps.alpha {:mvn/version "0.15.1254"}
org.owasp/dependency-check-core {:mvn/version "7.4.1"}
org.apache.maven.resolver/maven-resolver-transport-http {:mvn/version "1.9.2"}}
:mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"}
"clojars" {:url "https://repo.clojars.org/"}}

Expand All @@ -26,4 +26,10 @@
:outdated {:replace-deps {olical/depot {:mvn/version "2.3.0"}}
:main-opts ["-m" "depot.outdated.main"]}
:clojure-lsp {:replace-deps {com.github.clojure-lsp/clojure-lsp-standalone {:mvn/version "2022.02.01-20.02.32"}}
:main-opts ["-m" "clojure-lsp.main"]}}}
:main-opts ["-m" "clojure-lsp.main"]}
:test {:extra-paths ["test"]
:extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}
lambdaisland/kaocha {:mvn/version "1.67.1055"}
nubank/mockfn {:mvn/version "0.7.0"}
nubank/state-flow {:mvn/version "5.14.1"}}
:main-opts ["-m" "kaocha.runner"]}}}
2 changes: 2 additions & 0 deletions resources/clj-watson-config.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{:allow-list {:cves [{:cve-label "CVE-0000"
:expires "2000-01-01"}]}}
17 changes: 17 additions & 0 deletions src/clj_watson/adapter/config.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
(ns clj-watson.adapter.config
(:require
[clj-time.format :as time.format]))

(def time-parser (time.format/formatters :date))

(defn ->allow-config
[{:keys [cve-label expires]}]
{cve-label (time.format/parse time-parser expires)})

(defn config->allow-config-map
[config]
(->> config
:allow-list
:cves
(map ->allow-config)
(into {})))
2 changes: 1 addition & 1 deletion src/clj_watson/controller/dependency_check/scanner.clj
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@
(defn start! [dependencies dependency-check-properties]
(let [engine (scan-jars dependencies dependency-check-properties)
scanned-dependencies (->> engine .getDependencies Arrays/asList)]
scanned-dependencies))
scanned-dependencies))
26 changes: 16 additions & 10 deletions src/clj_watson/controller/github/vulnerability.clj
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
(ns clj-watson.controller.github.vulnerability
(:require
[clj-time.core :as time]
[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.github.vulnerability :as logic.gh.vulnerability]
[clj-watson.logic.rules.allowlist :as logic.rules.allowlist]))

(def ^:private dependency-rename
{'org.jdom/jdom2 'org.jdom/jdom})

(defn ^:private latest-dependency-version [dependency all-dependency-vulnerabilities repositories]
(defn ^:private latest-dependency-version
[dependency all-dependency-vulnerabilities repositories]
(let [latest-version (diplomat.dependency/get-latest-version! dependency repositories)
vulnerabilities (filterv #(logic.gh.vulnerability/is-version-vulnerable? latest-version %) all-dependency-vulnerabilities)]
vulnerabilities (filterv (partial logic.gh.vulnerability/is-version-vulnerable? latest-version) all-dependency-vulnerabilities)]
(when (not (seq vulnerabilities))
latest-version)))

(defn ^:private scan-dependency [{:keys [dependency] :as dependency-info} repositories]
(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)
filtered-vulnerabilities (filterv #(logic.gh.vulnerability/is-version-vulnerable? dependency-info %) all-dependency-vulnerabilities)
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 (time/today)) reported-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)
dependency-info)))

(defn scan-dependencies [dependencies repositories]
(defn scan-dependencies
[dependencies repositories allow-list]
(->> dependencies
(pmap #(scan-dependency % repositories))
(pmap (partial scan-dependency repositories allow-list))
(filterv :vulnerabilities)))

(comment
(def repositories {:mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"}
"clojars" {:url "https://repo.clojars.org/"}}})

(scan-dependencies [{:dependency 'org.jdom/jdom2 :mvn/version "2.0.6"}] repositories)
(scan-dependencies [{:dependency 'org.jdom/jdom2 :mvn/version "2.0.6"}] 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 {}))
26 changes: 16 additions & 10 deletions src/clj_watson/entrypoint.clj
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
(ns clj-watson.entrypoint
(:require
[clj-watson.adapter.config :as adapter.config]
[clj-watson.controller.dependency-check.scanner :as controller.dc.scanner]
[clj-watson.controller.dependency-check.vulnerability :as controller.dc.vulnerability]
[clj-watson.controller.deps :as controller.deps]
[clj-watson.controller.github.vulnerability :as controller.gh.vulnerability]
[clj-watson.controller.output :as controller.output]
[clj-watson.controller.remediate :as controller.remediate]))
[clj-watson.controller.remediate :as controller.remediate]
[clojure.java.io :as io]
[clojure.tools.reader.edn :as edn]))

(defmulti scan* (fn [{:keys [database-strategy]}] (keyword 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])
vulnerable-dependencies (controller.gh.vulnerability/scan-dependencies dependencies repositories)]
config (edn/read-string (slurp (io/resource "clj-watson-config.edn")))
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)))
Expand All @@ -30,20 +35,21 @@
(scan* (assoc opts :database-strategy "dependency-check")))

(defn scan [{:keys [fail-on-result output deps-edn-path] :as opts}]
(let [vulnerabilities (scan* opts)]
(let [vulnerabilities (scan* opts)
contains-vulnerabilities? (->> vulnerabilities
(map (comp empty? :vulnerabilities))
(some false?))]
(controller.output/generate vulnerabilities deps-edn-path output)
(if (and (-> vulnerabilities count (> 0)) fail-on-result)
(if (and contains-vulnerabilities? fail-on-result)
(System/exit 1)
(System/exit 0))))

(comment
(def vulnerabilities (scan* {:deps-edn-path "resources/vulnerable-deps.edn"
(def vulnerabilities (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"
:suggest-fix true}))
:suggest-fix true}))

(def vulnerabilities (scan* {:deps-edn-path "resources/vulnerable-deps.edn"
:database-strategy "github-advisory"}))
(controller.output/generate vulnerabilities "deps.edn" "sarif")
(controller.output/generate vulnerabilities "deps.edn" "stdout-simple"))
22 changes: 22 additions & 0 deletions src/clj_watson/logic/rules/allowlist.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
(ns clj-watson.logic.rules.allowlist
(:require
[clj-time.core :as time]))

(defn match-cve?
([allowed-cves as-of]
(partial match-cve? allowed-cves as-of))
([allowed-cves
as-of
{identifier :value}]
(when-let [expire-date (allowed-cves identifier)]
(time/after? expire-date as-of))))

(defn by-pass?
[allowed-cves
as-of
vulnerability]
(let [allowed? (comp seq (partial filter (match-cve? allowed-cves as-of)) :identifiers :advisory)]
(->> vulnerability
:vulnerabilities
(remove allowed?)
empty?)))
19 changes: 19 additions & 0 deletions test/clj_watson/unit/adapter/config_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
(ns clj-watson.unit.adapter.config-test
(:require
[clj-time.core :as time]
[clj-watson.adapter.config :as adapter.config]
[clojure.test :refer :all]))

(deftest ->allow-config
(testing "Allow Parsing"
(is (= {"CVE-1234" (time/date-time 2021 5 12)}
(adapter.config/->allow-config {:cve-label "CVE-1234" :expires "2021-05-12"})))
(is (thrown? IllegalArgumentException
(adapter.config/->allow-config {:cve-label "CVE-1234" :expires "wrong date"})))))

(deftest config->allow-list
(testing "Configs Parsing"
(is (= {"CVE-1234" (time/date-time 2021 5 12)
"CVE-5678" (time/date-time 2025 5 12)}
(adapter.config/config->allow-config-map {:allow-list {:cves [{:cve-label "CVE-1234" :expires "2021-05-12"}
{:cve-label "CVE-5678" :expires "2025-05-12"}]}})))))
63 changes: 63 additions & 0 deletions test/clj_watson/unit/logic/allowlist_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
(ns clj-watson.unit.logic.allowlist-test
(:require
[clj-time.core :as time]
[clj-watson.logic.rules.allowlist :as logic.rules.allowlist]
[clojure.test :refer :all]))

(deftest by-pass?
(let [expired-date (time/local-date 2020 2 1)
as-of (time/local-date 2022 7 12)
valid-date (time/local-date 2022 7 14)]
(testing "matching CVEs"
(is (= true (logic.rules.allowlist/by-pass? {"CVE-2022-2047" valid-date}
as-of
{:vulnerabilities
[{:advisory
{:identifiers
[{:value "GHSA-cj7v-27pg-wf7q"}
{:value "CVE-2022-2047"}]}}]})))
(is (= false (logic.rules.allowlist/by-pass? {"CVE-2022-2042" valid-date}
as-of
{:vulnerabilities
[{:advisory
{:identifiers
[{:value "GHSA-cj7v-27pg-wf7q"}
{:value "CVE-DO-NOT-BYPASS"}]}}]}))))
(testing "Multiple vulnerabilities on a single report"
(testing "all CVEs must be allowed"
(is (= true (logic.rules.allowlist/by-pass? {"CVE-2022-2047" valid-date
"CVE-1234-56789" valid-date}
as-of
{:vulnerabilities
[{:advisory
{:identifiers
[{:value "CVE-1234-56789"}]}}
{:advisory
{:identifiers
[{:value "GHSA-cj7v-27pg-wf7q"}
{:value "CVE-2022-2047"}]}}]})))
(is (= false (logic.rules.allowlist/by-pass? {"CVE-2022-2047" valid-date}
as-of
{:vulnerabilities
[{:advisory
{:identifiers
[{:value "CVE-1234-56789"}]}}
{:advisory
{:identifiers
[{:value "GHSA-cj7v-27pg-wf7q"}
{:value "CVE-2022-2047"}]}}]})))))
(testing "expired allowlist"
(is (= false (logic.rules.allowlist/by-pass? {"CVE-2022-2047" expired-date}
as-of
{:vulnerabilities
[{:advisory
{:identifiers
[{:value "GHSA-cj7v-27pg-wf7q"}
{:value "CVE-2022-2047"}]}}]})))
(is (= false (logic.rules.allowlist/by-pass? {"CVE-2022-2042" expired-date}
as-of
{:vulnerabilities
[{:advisory
{:identifiers
[{:value "GHSA-cj7v-27pg-wf7q"}
{:value "CVE-DO-NOT-BYPASS"}]}}]}))))))
Loading

0 comments on commit 25e873a

Please sign in to comment.