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" + } +} diff --git a/src/nvd/task/check.clj b/src/nvd/task/check.clj index 384f8c2..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))) @@ -37,17 +38,33 @@ (delay {:nvd-clojure (get-version "nvd-clojure" "nvd-clojure") :dependency-check (.getImplementationVersion (.getPackage Engine))})) -(defn jar? [^String filename] - (.endsWith filename ".jar")) +(def classpath-separator-re + (re-pattern (Pattern/quote File/pathSeparator))) (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 + 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 (map absolute-path) + (remove (fn [^String s] + (let [file (io/file s)] + (or (.isDirectory file) + (not (.exists file))))))) + (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)] - (when (jar? p) - (.scan engine (absolute-path p)))) + (.scan engine (absolute-path p))) (try (.analyzeDependencies engine) (catch ExceptionCollection e @@ -94,25 +111,18 @@ 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] - ;; 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. - (let [file (io/file s)] - (or (.isDirectory file) - (not (.exists file)))))) - classpath)] + (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 @@ -127,19 +137,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)