From 81835f6d1d8c3acf9f74717e60c6ecdc784f0688 Mon Sep 17 00:00:00 2001 From: Moritz Heidkamp Date: Tue, 7 Oct 2025 13:36:57 +0200 Subject: [PATCH 1/7] Scan all classpath entries This allows using e.g. the [Node Audit Analyzer](https://dependency-check.github.io/DependencyCheck/analyzers/node-audit-analyzer.html) (when enabled via the `nvd.analyzer.node-audit-enabled` config option) by passing `package-lock.json` as part of `classpath`. Note that the filtering in `-main` still takes care of removing directories and non-existing files. The comment there is updated to reflect the new behavior. --- src/nvd/task/check.clj | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/nvd/task/check.clj b/src/nvd/task/check.clj index 384f8c2..d3f0188 100644 --- a/src/nvd/task/check.clj +++ b/src/nvd/task/check.clj @@ -37,17 +37,13 @@ (delay {:nvd-clojure (get-version "nvd-clojure" "nvd-clojure") :dependency-check (.getImplementationVersion (.getPackage Engine))})) -(defn jar? [^String filename] - (.endsWith filename ".jar")) - (defn absolute-path ^String [file] (s/replace-first file #"^~" (System/getProperty "user.home"))) (defn- scan-and-analyze [project] (let [^Engine engine (:engine project)] (doseq [p (:classpath project)] - (when (jar? p) - (.scan engine (absolute-path p)))) + (.scan engine (absolute-path p))) (try (.analyzeDependencies engine) (catch ExceptionCollection e @@ -105,10 +101,9 @@ Older usages are deprecated." {}))) (let [classpath (s/split classpath-string classpath-separator-re) classpath (into [] (remove (fn [^String s] - ;; Only .jar (and perhaps .zip) files are relevant. - ;; source paths such as `src`, while are part of the classpath, - ;; won't be meaningfully analyzed by dependency-check-core. - ;; Keeping only .jars facilitates various usage patterns. + ;; Source paths such as `src`, while part of the classpath, won't + ;; be meaningfully analyzed by dependency-check-core. Thus, skip + ;; directories in general as well as non-existing files. (let [file (io/file s)] (or (.isDirectory file) (not (.exists file)))))) From 53f6e30e9e5d53e562587cebdda06570eb04af4b Mon Sep 17 00:00:00 2001 From: Moritz Heidkamp Date: Tue, 7 Oct 2025 15:02:33 +0200 Subject: [PATCH 2/7] Extract `parse-classpath` for clarity and improve commentary --- src/nvd/task/check.clj | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/nvd/task/check.clj b/src/nvd/task/check.clj index d3f0188..056a42e 100644 --- a/src/nvd/task/check.clj +++ b/src/nvd/task/check.clj @@ -37,11 +37,31 @@ (delay {:nvd-clojure (get-version "nvd-clojure" "nvd-clojure") :dependency-check (.getImplementationVersion (.getPackage Engine))})) +(def classpath-separator-re + (re-pattern (str File/pathSeparatorChar))) + (defn absolute-path ^String [file] (s/replace-first file #"^~" (System/getProperty "user.home"))) +(defn parse-classpath + "Accepts a classpath string (i.e. colon-separated paths) and returns a sequence of analyzable + absolute paths. + + In particular, source paths such as `src`, while part of the classpath, won't be meaningfully + analyzed by dependency-check-core. We only care about regular files (e.g. *.jar or + package-lock.json). Thus, skip directories in general as well as non-existing files." + [classpath-string] + (into [] + (comp (remove (fn [^String s] + (let [file (io/file s)] + (or (.isDirectory file) + (not (.exists file)))))) + (map absolute-path)) + (s/split classpath-string classpath-separator-re))) + (defn- scan-and-analyze [project] (let [^Engine engine (:engine project)] + ;; See `parse-classpath` for details on which classpath entries are considered here. (doseq [p (:classpath project)] (.scan engine (absolute-path p))) (try @@ -90,24 +110,12 @@ fail-build? conditional-exit))) -(def classpath-separator-re - (re-pattern (str File/pathSeparatorChar))) - (defn -main [& [config-filename ^String classpath-string]] (when (s/blank? classpath-string) (throw (ex-info "nvd-clojure requires a classpath value to be explicitly passed as a CLI argument. Older usages are deprecated." {}))) - (let [classpath (s/split classpath-string classpath-separator-re) - classpath (into [] - (remove (fn [^String s] - ;; Source paths such as `src`, while part of the classpath, won't - ;; be meaningfully analyzed by dependency-check-core. Thus, skip - ;; directories in general as well as non-existing files. - (let [file (io/file s)] - (or (.isDirectory file) - (not (.exists file)))))) - classpath)] + (let [classpath (parse-classpath classpath-string)] (when-not (System/getProperty "nvd-clojure.internal.skip-self-check") (when-let [bad-entry (->> classpath From 0b6bf8726f59a3832899c4d7e3656f20c4d6aabf Mon Sep 17 00:00:00 2001 From: Moritz Heidkamp Date: Tue, 7 Oct 2025 15:55:32 +0200 Subject: [PATCH 3/7] Reinstate classpath sanity check --- src/nvd/task/check.clj | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/nvd/task/check.clj b/src/nvd/task/check.clj index 056a42e..5e81964 100644 --- a/src/nvd/task/check.clj +++ b/src/nvd/task/check.clj @@ -117,6 +117,12 @@ Older usages are deprecated." {}))) (let [classpath (parse-classpath classpath-string)] + (when (empty? classpath) + (throw (ex-info "No entries in given classpath qualify for analysis. + +Note that only regular files (non-directories) are considered." + {:classpath classpath-string}))) + (when-not (System/getProperty "nvd-clojure.internal.skip-self-check") (when-let [bad-entry (->> classpath (some (fn [^String entry] @@ -130,19 +136,6 @@ Please refer to the project's README for recommended usages." {:bad-entry bad-entry :classpath classpath-string})))) - ;; perform some sanity checks for ensuring the calculated classpath has the expected format: - (let [f (-> classpath ^String (first) File.)] - (when-not (.exists f) - (throw (ex-info (str "The classpath variable should be a vector of simple strings denoting existing files: " - (pr-str f)) - {})))) - - (let [f (-> classpath ^String (last) File.)] - (when-not (.exists f) - (throw (ex-info (str "The classpath variable should be a vector of simple strings denoting existing files: " - (pr-str f)) - {})))) - ;; specifically handle blank strings (in addition to nil) ;; so that CLI callers can skip the first argument by simply passing an empty string: (let [config-filename (if (s/blank? config-filename) From 67361775fb6b2b4eaeedd019fabd55576dad0eba Mon Sep 17 00:00:00 2001 From: Moritz Heidkamp Date: Wed, 8 Oct 2025 12:06:28 +0200 Subject: [PATCH 4/7] Expand `~` in classpath entries *before* filtering for existence Otherwise, `~` would be interpreted literally and the existence check would always remove such entries. --- src/nvd/task/check.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nvd/task/check.clj b/src/nvd/task/check.clj index 5e81964..037f8a9 100644 --- a/src/nvd/task/check.clj +++ b/src/nvd/task/check.clj @@ -52,11 +52,11 @@ package-lock.json). Thus, skip directories in general as well as non-existing files." [classpath-string] (into [] - (comp (remove (fn [^String s] + (comp (map absolute-path) + (remove (fn [^String s] (let [file (io/file s)] (or (.isDirectory file) - (not (.exists file)))))) - (map absolute-path)) + (not (.exists file))))))) (s/split classpath-string classpath-separator-re))) (defn- scan-and-analyze [project] From c349827f605fcc07930aac015fcd34cec8b48348 Mon Sep 17 00:00:00 2001 From: Moritz Heidkamp Date: Wed, 8 Oct 2025 12:07:47 +0200 Subject: [PATCH 5/7] Only expand `~` in classpath entries when followed by `/` or `$` Otherwise, entries like `~foo` would expand to something like `/home/userfoo`. --- src/nvd/task/check.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nvd/task/check.clj b/src/nvd/task/check.clj index 037f8a9..2ebb0d3 100644 --- a/src/nvd/task/check.clj +++ b/src/nvd/task/check.clj @@ -41,7 +41,7 @@ (re-pattern (str File/pathSeparatorChar))) (defn absolute-path ^String [file] - (s/replace-first file #"^~" (System/getProperty "user.home"))) + (s/replace-first file #"^~(?=$|/)" (System/getProperty "user.home"))) (defn parse-classpath "Accepts a classpath string (i.e. colon-separated paths) and returns a sequence of analyzable From 8b647642ff97ac391d06984723f98dcd03477605 Mon Sep 17 00:00:00 2001 From: Moritz Heidkamp Date: Wed, 8 Oct 2025 15:07:23 +0200 Subject: [PATCH 6/7] Add integration test for non-default analyzer (node audit) --- .github/integration_test.sh | 19 ++- .github/nvd-node-audit-config.edn | 3 + example/package-lock.json | 225 ++++++++++++++++++++++++++++++ example/package.json | 8 ++ 4 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 .github/nvd-node-audit-config.edn create mode 100644 example/package-lock.json create mode 100644 example/package.json diff --git a/.github/integration_test.sh b/.github/integration_test.sh index e86b3e2..630a4f0 100755 --- a/.github/integration_test.sh +++ b/.github/integration_test.sh @@ -16,13 +16,14 @@ CONFIG_FILE_USING_DEFAULT_FILENAME="$PROJECT_DIR/nvd-clojure.edn" DOGFOODING_CONFIG_FILE="$PROJECT_DIR/.github/nvd-dogfooding-config.edn" TOOLS_CONFIG_FILE="$PROJECT_DIR/.github/nvd-tool-config.edn" DATAFEED_CONFIG_FILE="$PROJECT_DIR/.github/nvd-datafeed-config.edn" +NODE_AUDIT_CONFIG_FILE="$PROJECT_DIR/.github/nvd-node-audit-config.edn" JSON_CONFIG_FILE="$PROJECT_DIR/.github/nvd-config.json" JSON_DOGFOODING_CONFIG_FILE="$PROJECT_DIR/.github/nvd-dogfooding-config.json" JSON_TOOLS_CONFIG_FILE="$PROJECT_DIR/.github/nvd-tool-config.json" A_CUSTOM_CHANGE=":a-custom-change" -SUCCESS_REGEX="[1-9][0-9] vulnerabilities detected\. Severity: " +SUCCESS_REGEX="[1-9][0-9]* vulnerabilities detected\. Severity: " if ! lein with-profile -user,-dev,+ci install; then exit 1 @@ -123,6 +124,22 @@ if ! grep --silent "$SUCCESS_REGEX" test-output; then exit 1 fi +# 1.5 - Exercise `main` program (non-default analyzer) + +step_name=">>> [Step 1.5 lein & non-default analyzer]" + +echo "$step_name starting..." + +if lein with-profile -user,-dev,+ci run -m nvd.task.check "$NODE_AUDIT_CONFIG_FILE" example/package-lock.json > test-output; then + echo "$step_name Should have failed with non-zero code!" + exit 1 +fi + +if ! grep --silent "$SUCCESS_REGEX" test-output; then + echo "$step_name Should have found vulnerabilities!" + exit 1 +fi + # cd to the root dir, so that one runs `defproject nvd-clojure` which is the most clean and realistic way to run `main`: cd "$PROJECT_DIR" || exit 1 diff --git a/.github/nvd-node-audit-config.edn b/.github/nvd-node-audit-config.edn new file mode 100644 index 0000000..a4a2309 --- /dev/null +++ b/.github/nvd-node-audit-config.edn @@ -0,0 +1,3 @@ +{:suppression-file ".github/example_nvd_suppressions.xml" + :analyzer {:ossindex-warn-only-on-remote-errors true + :node-audit-enabled true}} diff --git a/example/package-lock.json b/example/package-lock.json new file mode 100644 index 0000000..06865f7 --- /dev/null +++ b/example/package-lock.json @@ -0,0 +1,225 @@ +{ + "name": "example-with-known-vulnerabilities", + "version": "1.4.17", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "example-with-known-vulnerabilities", + "version": "1.4.17", + "dependencies": { + "tar-fs": "2.1.3" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..53c3e5f --- /dev/null +++ b/example/package.json @@ -0,0 +1,8 @@ +{ + "name": "example-with-known-vulnerabilities", + "version": "1.4.17", + "private": true, + "dependencies": { + "tar-fs": "2.1.3" + } +} From 826ec576755728f441ce2a59dfb9eeeb863dbab7 Mon Sep 17 00:00:00 2001 From: Moritz Heidkamp Date: Thu, 9 Oct 2025 16:38:52 +0200 Subject: [PATCH 7/7] Regex-quote path separator for prudence In practice, only ":" and ";" are used but quoting is prudent to not give readers pause. --- src/nvd/task/check.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nvd/task/check.clj b/src/nvd/task/check.clj index 2ebb0d3..7ecd5a7 100644 --- a/src/nvd/task/check.clj +++ b/src/nvd/task/check.clj @@ -30,6 +30,7 @@ [trptcolin.versioneer.core :refer [get-version]]) (:import (java.io File) + (java.util.regex Pattern) (org.owasp.dependencycheck Engine) (org.owasp.dependencycheck.exception ExceptionCollection))) @@ -38,7 +39,7 @@ :dependency-check (.getImplementationVersion (.getPackage Engine))})) (def classpath-separator-re - (re-pattern (str File/pathSeparatorChar))) + (re-pattern (Pattern/quote File/pathSeparator))) (defn absolute-path ^String [file] (s/replace-first file #"^~(?=$|/)" (System/getProperty "user.home")))