From 3a2fd6e275580616b86190c9959654521760abe4 Mon Sep 17 00:00:00 2001 From: sterni Date: Sun, 10 Oct 2021 21:31:53 +0200 Subject: feat(nixpkgs-crate-holes): report vulnerable crates in cargoDeps nixpkgs-crate-holes can build a markdown report detailing all vulnerable crates pinned in cargoDeps vendors in nixpkgs according to RustSec's advisory db. This report is intended to be pasted into a GitHub issue. The report is produced by a derivation and can be obtained like this: nix-build -A users.sterni.nixpkgs-crate-holes.full \ --argstr nixpkgsPath /path/to/nixpkgs Example output: https://gist.github.com/sternenseemann/27509eece93d6eff35cd4b8ce75423b5 Additionally, you can obtain a more verbose report for a single attribute of nixpkgs, in HTML format since we just reuse the command line output of cargo-audit and convert it to HTML using ansi2html: nix-build -A users.sterni.nixpkgs-crate-holes.single \ --argstr nixpkgsPath /path/to/nixpkgs --argstr attr ripgrep Change-Id: Ic1c029ab67770fc41ba521b2acb798628357f9b2 Reviewed-on: https://cl.tvl.fyi/c/depot/+/3715 Tested-by: BuildkiteCI Reviewed-by: sterni --- users/sterni/nixpkgs-crate-holes/default.nix | 267 +++++++++++++++++++++ .../nixpkgs-crate-holes/format-audit-result.jq | 59 +++++ 2 files changed, 326 insertions(+) create mode 100644 users/sterni/nixpkgs-crate-holes/default.nix create mode 100644 users/sterni/nixpkgs-crate-holes/format-audit-result.jq diff --git a/users/sterni/nixpkgs-crate-holes/default.nix b/users/sterni/nixpkgs-crate-holes/default.nix new file mode 100644 index 0000000000..9ca72e5463 --- /dev/null +++ b/users/sterni/nixpkgs-crate-holes/default.nix @@ -0,0 +1,267 @@ +{ depot, pkgs, lib, ... }: + +let + # dependency imports + + inherit (depot.nix) getBins; + inherit (depot.third_party) rustsec-advisory-db; + + bins = getBins pkgs.jq [ + "jq" + ] // getBins pkgs.coreutils [ + "cat" + "printf" + "tee" + "test" + "wc" + ] // getBins pkgs.gnugrep [ + "grep" + ] // getBins pkgs.cargo-audit [ + "cargo-audit" + ] // getBins pkgs.ansi2html [ + "ansi2html" + ] // { + eprintf = depot.tools.eprintf; + }; + + # buildRustPackage handling + + /* Predicate by which we identify rust packages we are interested in, + i. e. built using `buildRustPackage`. + + Type :: drv -> bool + */ + isRustPackage = v: v ? cargoDeps; + + /* Takes a buildRustPackage derivation and returns a derivation which + builds extracts the `Cargo.lock` of its `cargoDeps` derivation or + `null` if it has none. + + Type: drv -> option + */ + # TODO(sterni): support cargoVendorDir? + extractCargoLock = drv: + if !(drv ? cargoDeps.outPath) + then null + else pkgs.runCommandNoCC "${drv.name}-Cargo.lock" {} '' + if test -d "${drv.cargoDeps}"; then + cp "${drv.cargoDeps}/Cargo.lock" "$out" + fi + + if test -f "${drv.cargoDeps}"; then + tar -xO \ + --no-wildcards-match-slash --wildcards \ + -f "${drv.cargoDeps}" \ + '*/Cargo.lock' \ + > "$out" + fi + ''; + + # nixpkgs traversal + + # Condition for us to recurse: Either at top-level or recurseForDerivation. + recurseInto = path: x: path == [] || + (lib.isAttrs x && (x.recurseForDerivations or false)); + + # Returns the value or false if an eval error occurs. + tryEvalOrFalse = v: (builtins.tryEval v).value; + + /* Traverses nixpkgs as instructed by `recurseInto` and collects + the attribute and lockfile derivation of every rust package it + encounters into a list. + + Type :: attrs + -> list { + attr :: list; + lock :: option; + maintainers :: list; + } + */ + allLockFiles = + let + go = path: x: + let + isDrv = tryEvalOrFalse (lib.isDerivation x); + doRec = tryEvalOrFalse (recurseInto path x); + isRust = tryEvalOrFalse (isRustPackage x); + in + if doRec then lib.concatLists ( + lib.mapAttrsToList (n: go (path ++ [ n ])) x + ) else if isDrv && isRust then [ + { + attr = path; + lock = extractCargoLock x; + maintainers = x.meta.maintainers or []; + } + ] else []; + in go []; + + # Report generation and formatting + + reportFor = { attr, lock, ... }: let + # naïve attribute path to Nix syntax conversion + strAttr = lib.concatStringsSep "." attr; + in + if lock == null + then pkgs.emptyFile + else depot.nix.runExecline "${strAttr}-vulnerability-report" {} [ + "pipeline" [ + bins.cargo-audit + "audit" "--json" + "-n" "--db" rustsec-advisory-db + "-f" lock + ] + "importas" "out" "out" + "redirfd" "-w" "1" "$out" + bins.jq "-rj" "-f" ./format-audit-result.jq "--arg" "attr" strAttr + ]; + + # GHMF in issues splits paragraphs on newlines + description = lib.concatMapStringsSep "\n\n" ( + builtins.replaceStrings [ "\n" ] [ " " ] + ) [ + '' + The vulnerability report below was generated by + [nixpkgs-crate-holes](https://code.tvl.fyi/tree/users/sterni/nixpkgs-crate-holes) + which extracts the `Cargo.lock` file of each package in nixpkgs with a + `cargoDeps` attribute and passes it to + [cargo-audit](https://github.com/RustSec/rustsec/tree/main/cargo-audit) + using RustSec's + [advisory-db at ${builtins.substring 0 7 rustsec-advisory-db.rev}](https://github.com/RustSec/advisory-db/tree/${rustsec-advisory-db.rev}/). + '' + '' + Feel free to report any problems or suggest improvements (I have an email + address on my profile and hang out on Matrix/libera.chat as sterni)! + Tick off any reports that have been fixed in the meantime. + '' + '' + Note: A vulnerability in a dependency does not necessarily mean the dependent + package is vulnerable, e. g. when a vulnerable function isn't used. + '' + ]; + + runInstructions = '' +
+ + Generating Cargo.lock vulnerability reports + + + + If you have a checkout of [depot](https://code.tvl.fyi/about/), you can generate this report using: + + ``` + nix-build -A users.sterni.nixpkgs-crate-holes.full \ + --argstr nixpkgsPath /path/to/nixpkgs + ``` + + If you want a more detailed report for a single attribute of nixpkgs, use: + + ``` + nix-build -A users.sterni.nixpkgs-crate-holes.single \ + --argstr nixpkgsPath /path/to/nixpkgs --arg attr '[ "ripgrep" ]' + ``` + +
+ ''; + + defaultNixpkgsArgs = { allowBroken = false; }; + + reportForNixpkgs = + { nixpkgsPath + , nixpkgsArgs ? defaultNixpkgsArgs + }@args: + + let + reports = builtins.map reportFor ( + allLockFiles (import nixpkgsPath nixpkgsArgs) + ); + in + + depot.nix.runExecline "nixpkgs-rust-pkgs-vulnerability-report.md" { + stdin = lib.concatMapStrings (report: "${report}\n") reports; + } [ + "importas" "out" "out" + "redirfd" "-w" "1" "$out" + # Print introduction paragraph for the issue + "if" [ bins.printf "%s\n\n" description ] + # Print all reports + "foreground" [ + "forstdin" "-E" "report" bins.cat "$report" + ] + # Print stats at the end (mostly as a gimmick), we already know how many + # attributes there are and count the attributes with vulnerability by + # finding the number of checkable list entries in the output. + "backtick" "-E" "vulnerableCount" [ + "pipeline" [ + bins.grep "^- \\[ \\]" "$out" + ] + bins.wc "-l" + ] + "if" [ + bins.printf + "\n%s of %s checked attributes have vulnerable dependencies.\n\n" + "$vulnerableCount" + (toString (builtins.length reports)) + ] + "if" [ + bins.printf "%s\n\n" runInstructions + ] + ]; + + singleReport = + { # Attribute to check: string or list of strings (attr path) + attr + # Path to importable nixpkgs checkout + , nixpkgsPath + # Arguments to pass to nixpkgs + , nixpkgsArgs ? defaultNixpkgsArgs + }: + + let + attr' = if builtins.isString attr then [ attr ] else attr; + drv = lib.getAttrFromPath attr' (import nixpkgsPath nixpkgsArgs); + lockFile = extractCargoLock drv; + strAttr = lib.concatStringsSep "." attr'; + in + + depot.nix.runExecline "${strAttr}-report.html" {} [ + "importas" "out" "out" + "backtick" "-I" "-E" "-N" "report" [ + bins.cargo-audit "audit" + "--quiet" + "-n" "--db" rustsec-advisory-db + "-f" lockFile + ] + "pipeline" [ + "ifte" [ + bins.printf "%s" "$report" + ] [ + bins.printf "%s\n" "No vulnerabilities found" + ] + bins.test "-n" "$report" + ] + "pipeline" [ + bins.tee "/dev/stderr" + ] + "redirfd" "-w" "1" "$out" + bins.ansi2html + ]; + +in { + full = reportForNixpkgs; + single = singleReport; + + inherit + extractCargoLock + allLockFiles + ; + + # simple sanity check, doesn't cover everything, but testing the full report + # is quite expensive in terms of evaluation. + testSingle = singleReport { + nixpkgsPath = depot.third_party.nixpkgs.path; + attr = [ "ripgrep" ]; + }; + + meta.targets = [ "testSingle" ]; +} diff --git a/users/sterni/nixpkgs-crate-holes/format-audit-result.jq b/users/sterni/nixpkgs-crate-holes/format-audit-result.jq new file mode 100644 index 0000000000..c527bc4da9 --- /dev/null +++ b/users/sterni/nixpkgs-crate-holes/format-audit-result.jq @@ -0,0 +1,59 @@ +# Link to human-readable advisory info for a given vulnerability +def link: + [ "https://rustsec.org/advisories/", .advisory.id, ".html" ] | add; + +# Format a list of version constraints +def version_list: + [ .[] | "`" + . + "`" ] | join("; "); + +# show paths to fixing this vulnerability: +# +# - if there are patched releases, show them (the version we are using presumably +# predates the vulnerability discovery, so we likely want to upgrade to a +# patched release). +# - if there are no patched releases, show the unaffected versions (in case we +# want to downgrade). +# - otherwise we state that no unaffected versions are available at this time. +# +# This logic should be useful, but is slightly dumber than cargo-audit's +# suggestion when using the non-JSON output. +def patched: + if .versions.patched == [] then + if .versions.unaffected != [] then + "unaffected: " + (.versions.unaffected | version_list) + else + "no unaffected version available" + end + else + "patched: " + (.versions.patched | version_list) + end; + +# if the vulnerability has aliases (like CVE-*) emit them in parens +def aliases: + if .advisory.aliases == [] then + "" + else + [ " (", (.advisory.aliases | join(", ")), ")" ] | add + end; + +# each vulnerability is rendered as a (normal) sublist item +def format_vulnerability: + [ " - " + , .package.name, " ", .package.version, ": " + , "[", .advisory.id, "](", link, ")" + , aliases + , ", ", patched + , "\n" + ] | add; + +# be quiet if no found vulnerabilities, otherwise render a GHFM checklist item +if .vulnerabilities.found | not then + "" +else + ([ "- [ ] " + , "`", $attr, "`: " + , (.vulnerabilities.count | tostring) + , " vulnerabilities in Cargo.lock\n" + ] + (.vulnerabilities.list | map(format_vulnerability)) + ) | add +end -- cgit 1.4.1