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 <sternenseemann@systemli.org>
This commit is contained in:
parent
14282370e9
commit
3a2fd6e275
2 changed files with 326 additions and 0 deletions
267
users/sterni/nixpkgs-crate-holes/default.nix
Normal file
267
users/sterni/nixpkgs-crate-holes/default.nix
Normal file
|
@ -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<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, ... }: 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 = ''
|
||||
<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" ];
|
||||
}
|
59
users/sterni/nixpkgs-crate-holes/format-audit-result.jq
Normal file
59
users/sterni/nixpkgs-crate-holes/format-audit-result.jq
Normal file
|
@ -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
|
Loading…
Reference in a new issue