about summary refs log tree commit diff
path: root/users/sterni/nixpkgs-crate-holes/default.nix
diff options
context:
space:
mode:
Diffstat (limited to 'users/sterni/nixpkgs-crate-holes/default.nix')
-rw-r--r--users/sterni/nixpkgs-crate-holes/default.nix284
1 files changed, 284 insertions, 0 deletions
diff --git a/users/sterni/nixpkgs-crate-holes/default.nix b/users/sterni/nixpkgs-crate-holes/default.nix
new file mode 100644
index 000000000000..a022568dc941
--- /dev/null
+++ b/users/sterni/nixpkgs-crate-holes/default.nix
@@ -0,0 +1,284 @@
+{ 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;
+  };
+
+  # list of maintainers we may @mention on GitHub
+  maintainerWhitelist = builtins.attrValues {
+    inherit (lib.maintainers)
+      sternenseemann
+      qyliss
+      jk
+      symphorien
+      erictapen
+      expipiplus1
+    ;
+  };
+
+  # 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<drv>
+  */
+  # 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<str>;
+               lock :: option<drv>;
+               maintainers :: list<maintainer>;
+             }
+  */
+  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, maintainers ? [] }: let
+    # naïve attribute path to Nix syntax conversion
+    strAttr = lib.concatStringsSep "." attr;
+    strMaintainers = lib.concatMapStringsSep " " (m: "@${m.github}") (
+      builtins.filter (x: builtins.elem x maintainerWhitelist) maintainers
+    );
+  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
+      "--arg" "maintainers" strMaintainers
+    ];
+
+  # 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 = ''
+    <details>
+    <summary>
+    Generating Cargo.lock vulnerability reports
+
+    </summary>
+
+    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" ]'
+    ```
+
+    </details>
+  '';
+
+  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" ];
+}