From 8b46c36ffe6386873790697ff61bb30c8decd258 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Thu, 22 Oct 2026 14:06:31 -0400 Subject: [PATCH 1/3] refactor(cli)!: coherent flag names and grouped help; v0.4.0 "all" meant two different things on the command line: --all (every pass) and the --*-cves-all detail flags (every item within the CVE pass). And the "show me more" flags were four unrelated idioms (-v, --license-paths, and two --*-cves-all), most of them human-view-only, with no stated rule. Split the two meanings and name the detail family consistently: --component-cves-all -> --component-cves-full --kernel-cves-all -> --kernel-cves-full "all" now means only "all passes"; "full" means "all detail within a pass." Add --full as one switch for maximum human detail (verbose + license-paths + both -cves-full). No aliases for the old names: this is a breaking change, hence the major-ish bump to 0.4.0. Regroup --help into PASSES / OUTPUT / CVE MIRROR / OTHER, and state once that JSON (-j) is always complete so the detail flags only expand the human view. Drop the trailing "Pipeline:"/"Docs:" footer lines. Document that -C writes sbom.cdx.json and sbom.spdx.json alongside mithril-report.json. Add the previously-undocumented --verbose entry to the man page. Renames flow through the code comments, the in-output "run --X" hints (json.cpp, human.cpp), the man page, README, docs/cve-join.md, and the test_cve / test_kernelfeed integration tests. Full suite green. --- README.md | 8 +++--- VERSION | 2 +- docs/cve-join.md | 14 +++++------ mithril.1 | 21 +++++++++++----- src/cve.cpp | 2 +- src/cve.hpp | 4 +-- src/cveupdate.cpp | 2 +- src/cveupdate.hpp | 2 +- src/dbpath.hpp | 2 +- src/human.cpp | 10 ++++---- src/json.cpp | 4 +-- src/kernelcve.hpp | 2 +- src/kernelfeed.hpp | 2 +- src/main.cpp | 54 ++++++++++++++++++++++++---------------- src/report.hpp | 6 ++--- tests/test_cve.py | 12 ++++----- tests/test_kernelfeed.py | 20 +++++++-------- 17 files changed, 93 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index bf24d57..0074ba8 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ mithril -j # JSON, for tools and agents mithril --secrets # secrets only mithril --sbom -C out/ # SBOM only -> out/sbom.cdx.json + out/sbom.spdx.json mithril --cve # match components against the local mirror (offline) -mithril --cve --kernel-cves-all # also list every kernel.org CVE for the kernel version (opt-in) -mithril --cve --component-cves-all # human view: list every component CVE, not just high-signal (opt-in) +mithril --cve --kernel-cves-full # also list every kernel.org CVE for the kernel version (opt-in) +mithril --cve --component-cves-full # human view: list every component CVE, not just high-signal (opt-in) mithril --licenses # licenses only mithril --license-paths # licenses, with each license's file locations listed mithril --boot # boot-security intel: U-Boot env, device tree, FIT/AVB, UEFI Secure Boot @@ -69,7 +69,7 @@ Naming any of `--secrets` / `--sbom` / `--cve` / `--licenses` / `--boot` / `--ke - **`mithril --fetch-db`** downloads a prebuilt index (rebuilt weekly) and verifies every file against a published SHA-256 before installing. It does no upstream scraping: it just fetches the finished index. Needs `curl`. Point it at a mirror with `$MITHRIL_DB_URL`. - **`mithril --update-db`** rebuilds the index from the upstream feeds yourself. Slower, needs `curl` and `unzip`, and is the authoritative path if you want to control exactly what goes in. Set `$NVD_API_KEY` to raise the NVD rate limit (optional; it works without one, just slower). -Add **`--with-kernel-feed`** to either command to also get the optional full **kernel.org (Linux CNA) CVE feed** that backs [`--kernel-cves-all`](docs/cve-join.md). By default the kernel is triaged by a curated high-signal checklist; `--kernel-cves-all` lists every kernel.org CVE for the detected version instead, matched branch-aware (a bug fixed in 6.6.17 is reported for 6.6.10 but not 6.6.110). It is a separate asset so the base mirror stays lean; building it needs `tar`. +Add **`--with-kernel-feed`** to either command to also get the optional full **kernel.org (Linux CNA) CVE feed** that backs [`--kernel-cves-full`](docs/cve-join.md). By default the kernel is triaged by a curated high-signal checklist; `--kernel-cves-full` lists every kernel.org CVE for the detected version instead, matched branch-aware (a bug fixed in 6.6.17 is reported for 6.6.10 but not 6.6.110). It is a separate asset so the base mirror stays lean; building it needs `tar`. Build it once either way; every scan after that is offline. See `docs/cve-join.md` and `docs/cve-index-format.md`. @@ -77,7 +77,7 @@ Build it once either way; every scan after that is offline. See `docs/cve-join.m - **secrets**: credential and key material in file content (cloud keys, tokens, JWTs, private keys, high-entropy `KEY=…` values). `/etc/shadow` and `htpasswd` hashes are classified with weak-algorithm and empty-password flags, and credential/crypto/config files are flagged by path. - **sbom**: components and versions from package databases, language manifests, binary version strings, libc filenames, and the kernel banner, keyed on purl (with CPE co-derived), emitted as CycloneDX and SPDX. -- **cve**: the SBOM joined against the local OSV + NVD/CPE mirror, plus the curated kernel-CVE checklist, annotated with KEV and EPSS. The default human view shows only foothold-worthy component CVEs (on CISA KEV, or Critical, or a hard High/Critical floor of CVSS >= 7.0 with EPSS traction, low attack complexity, and real impact or a trending remote DoS), sorted worst-first; it drops Mediums, local DoS, and the high-complexity crypto class. `--component-cves-all` lists them all. JSON always carries the complete set plus a `component_cve_scan` summary. +- **cve**: the SBOM joined against the local OSV + NVD/CPE mirror, plus the curated kernel-CVE checklist, annotated with KEV and EPSS. The default human view shows only foothold-worthy component CVEs (on CISA KEV, or Critical, or a hard High/Critical floor of CVSS >= 7.0 with EPSS traction, low attack complexity, and real impact or a trending remote DoS), sorted worst-first; it drops Mediums, local DoS, and the high-complexity crypto class. `--component-cves-full` lists them all. JSON always carries the complete set plus a `component_cve_scan` summary. - **licenses**: SPDX identification from `SPDX-License-Identifier` tags and LICENSE/COPYING/NOTICE text. Human output summarizes by id and count; `--license-paths` lists each license's file locations, and JSON always carries the full `paths` array. File-type identification, extraction, and embedded key/certificate *file* signatures are moria's job; mithril reads content, it does not unpack. How discovery works is documented in `docs/engine-design.md`. diff --git a/VERSION b/VERSION index 0d91a54..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 +0.4.0 diff --git a/docs/cve-join.md b/docs/cve-join.md index 9ee4cf1..0035386 100644 --- a/docs/cve-join.md +++ b/docs/cve-join.md @@ -131,11 +131,11 @@ a `kernel_cve_scan` object (`method`, `exhaustive: false`, and the `curated_tota `applicable` / `undetermined` / `ruled_out` counts) so a tool caller never infers exhaustiveness from an empty array. Second, the curated ranges reach only as far as the entries have been maintained (currently up to the ~6.8 fix window), so a newer kernel legitimately matches nothing here. For a complete, -version-matched enumeration use `--kernel-cves-all` (below). +version-matched enumeration use `--kernel-cves-full` (below). -## The full kernel CVE feed (`--kernel-cves-all`, opt-in) +## The full kernel CVE feed (`--kernel-cves-full`, opt-in) -The curated table is high-signal but partial. `--kernel-cves-all` adds the complete set: every Linux-kernel +The curated table is high-signal but partial. `--kernel-cves-full` adds the complete set: every Linux-kernel CVE published by the **kernel.org CNA**, joined against the detected kernel version with branch-aware precision. It is off by default (it is large and noisy — hundreds of matches on a mature branch — and most operators want the curated triage set first) and it implies `--cve`. @@ -168,7 +168,7 @@ LPEs) regardless of assigner. **Distribution.** The feed is a **separate, optional asset**, so the base mirror stays lean. Build it with `--update-db --with-kernel-feed` (needs `curl` + `tar`; downloads the vulns snapshot) or fetch the prebuilt -`kernel-cve-index.json` with `--fetch-db --with-kernel-feed`. Absent, `--kernel-cves-all` fails loud with a +`kernel-cve-index.json` with `--fetch-db --with-kernel-feed`. Absent, `--kernel-cves-full` fails loud with a scan-level error naming the fetch command; the curated verdicts still print. ## Exploit annotations: KEV + EPSS (value-add only, never gates) @@ -185,7 +185,7 @@ IT and misses kernel LPEs, so it must not be a gate. The human view tags a row ` The default human output is meant for a manual pentester triaging by impact, not a compliance dump. A firmware image pins old C libraries, so a flat join returns dozens to hundreds of component CVEs of every severity (busybox alone brought 20 on one sample) with no ranking. The default human view therefore shows -only **high-signal** component CVEs and sorts them worst-first; `--component-cves-all` prints the full +only **high-signal** component CVEs and sorts them worst-first; `--component-cves-full` prints the full list. A component CVE is high-signal if: @@ -247,6 +247,6 @@ matches stay unclassified → kept). Deferred until the vector heuristic proves nothing is dropped — and a `component_cve_scan` object (`total` / `high_signal` / `hidden` / `gate`) makes the human triage set machine-readable. The hard floor is deliberately blunt: a manual pentester triages Highs and Criticals, and anything below 7.0 (Terrapin CVE-2023-48795 at 5.9 included) goes to -`--component-cves-all` or the JSON, not the default view. KEV is the one override that ignores the floor, +`--component-cves-full` or the JSON, not the default view. KEV is the one override that ignores the floor, so an actively-exploited Medium still shows. Kernel CVEs are gated separately (curated vs. -`--kernel-cves-all` feed) and are unaffected by this component gate. +`--kernel-cves-full` feed) and are unaffected by this component gate. diff --git a/mithril.1 b/mithril.1 index 7dd16d3..b32bfd1 100644 --- a/mithril.1 +++ b/mithril.1 @@ -1,4 +1,4 @@ -.TH MITHRIL 1 "2026-10-05" "mithril 0.3.0" "User Commands" +.TH MITHRIL 1 "2026-10-22" "mithril 0.4.0" "User Commands" .SH NAME mithril \- analyze firmware contents for secrets, SBOM, CVEs, and licenses .SH SYNOPSIS @@ -90,26 +90,35 @@ Run every pass. This is the default when no pass is named. .TP .BR \-j ", " \-\-json Emit a single JSON object instead of the human-readable view. JSON always -carries the complete result set. Use this when a tool or model will parse the -output. +carries the complete result set, so the detail options below only affect the +human view. Use this when a tool or model will parse the output. +.TP +.BR \-v ", " \-\-verbose +Human view: show full values instead of tail-truncating long ones with +.RB \(lq ... \(rq. .TP .B \-\-license\-paths List the file locations of each license, not just a count (human output). Implies .BR \-\-licenses . .TP -.B \-\-component\-cves\-all +.B \-\-component\-cves\-full Human view: list every component CVE, not just the high-signal set (on CISA KEV, or CVSS 9.0 and above, or EPSS 0.10 and above). Implies .BR \-\-cve . .TP -.B \-\-kernel\-cves\-all +.B \-\-kernel\-cves\-full List every CVE for the detected kernel version, matched branch-aware, instead of the curated high-signal checklist. Needs the kernel feed (see .BR \-\-with\-kernel\-feed ). Implies .BR \-\-cve . .TP +.B \-\-full +Maximum human detail in one switch: equivalent to +.BR \-\-verbose ", " \-\-license\-paths ", " \-\-component\-cves\-full ", and " \-\-kernel\-cves\-full +together. No effect on JSON, which is always complete. +.TP .B \-\-dump\-kconfig Print the recovered kernel .I .config @@ -138,7 +147,7 @@ Given with or .BR \-\-update\-db , also fetch the optional full kernel.org (Linux CNA) CVE feed that backs -.BR \-\-kernel\-cves\-all . +.BR \-\-kernel\-cves\-full . .TP .B \-\-no\-fetch Suppress the interactive offer to download the CVE mirror. When the diff --git a/src/cve.cpp b/src/cve.cpp index 7e7a790..6559a12 100644 --- a/src/cve.cpp +++ b/src/cve.cpp @@ -236,7 +236,7 @@ bool cve_is_high_signal(const CveMatch& m) { // A vendor-fork component matched only by an unbounded-below range: we cannot // confirm the fork's base carries the vulnerable code (e.g. a 0.8.x_rtw hostapd // predates SAE), so drop it from the default view. Still listed under - // --component-cves-all with the "verify" basis. KEV above overrides this. + // --component-cves-full with the "verify" basis. KEV above overrides this. if (m.fork && m.unbounded_below) return false; double score = cvss_base_score(m.severity); if (score >= kHighSignalCvss) return true; // Critical, any shape diff --git a/src/cve.hpp b/src/cve.hpp index 635ecf0..2a7bc36 100644 --- a/src/cve.hpp +++ b/src/cve.hpp @@ -106,7 +106,7 @@ double cvss_base_score(const std::string& vector); // - it is on the CISA KEV catalog (exploited in the wild) -- unconditional; OR // - (it is NOT a vendor-fork component matched only by an unbounded-below range: // such a match cannot be confirmed to apply -- a fork's base may predate the -// vulnerable code -- so it is demoted to the --component-cves-all list); AND +// vulnerable code -- so it is demoted to the --component-cves-full list); AND // - its CVSS base score is >= 9.0 (Critical) -- any shape; OR // - it clears a hard High/Critical floor (base score >= 7.0) AND has EPSS // traction (>= 0.10) AND is low-complexity (AC:L) AND EITHER @@ -118,7 +118,7 @@ double cvss_base_score(const std::string& vector); // but low-yield crypto/DoS CVEs old libraries accumulate (SWEET32, DROWN, padding // oracles). A pure DoS cannot reach CVSS 9.0, so the Critical arm needs no test. // Scores are computed for CVSS v2 and v3 alike so the floor judges old CVEs -// instead of hiding them. --component-cves-all bypasses the whole gate; kernel +// instead of hiding them. --component-cves-full bypasses the whole gate; kernel // CVEs gate separately. constexpr double kHighSignalCvss = 9.0; // Critical: always shown constexpr double kHighFloorCvss = 7.0; // hard floor: High/Critical only below Critical diff --git a/src/cveupdate.cpp b/src/cveupdate.cpp index 42fbb4a..069a8d1 100644 --- a/src/cveupdate.cpp +++ b/src/cveupdate.cpp @@ -537,7 +537,7 @@ int epss_update(std::string& err) { } // Build the full kernel.org (Linux CNA) CVE feed index from the vulns snapshot. -// A large, optional asset (only --kernel-cves-all reads it): fetch the cgit +// A large, optional asset (only --kernel-cves-full reads it): fetch the cgit // tarball, extract with tar, reconstruct one entry per published CVE record, // write kernel-cve-index.json. Needs curl + tar. int kernel_feed_update(std::string& err) { diff --git a/src/cveupdate.hpp b/src/cveupdate.hpp index a19de48..3a9916e 100644 --- a/src/cveupdate.hpp +++ b/src/cveupdate.hpp @@ -15,7 +15,7 @@ namespace ft { // success; on failure returns non-zero and sets `err`. Progress to stderr. // This is the authoritative rebuild path (scrapes OSV + NVD at the source). // `with_kernel_feed` additionally builds the full kernel.org CVE feed (a large -// extra download; off by default — it only serves --kernel-cves-all). +// extra download; off by default — it only serves --kernel-cves-full). int cve_update(std::string& err, bool with_kernel_feed = false); // Download a prebuilt index from the rolling release (or $MITHRIL_DB_URL) and diff --git a/src/dbpath.hpp b/src/dbpath.hpp index fd8d2eb..e103a67 100644 --- a/src/dbpath.hpp +++ b/src/dbpath.hpp @@ -24,7 +24,7 @@ std::string kev_index_path(); std::string epss_index_path(); // The full kernel.org (Linux CNA) CVE feed index, consulted only by -// --kernel-cves-all. An optional asset: fetched/built out-of-band, absent by +// --kernel-cves-full. An optional asset: fetched/built out-of-band, absent by // default so the base mirror stays lean. std::string kernel_feed_path(); diff --git a/src/human.cpp b/src/human.cpp index 7fb7c21..75b9b74 100644 --- a/src/human.cpp +++ b/src/human.cpp @@ -165,7 +165,7 @@ std::string emit_report_human(const Report& rep, const Passes& passes, const std // ---- CVE matches ---- // The default view is gated to high-signal component CVEs (KEV / CVSS>=9.0 / // EPSS>=0.10) and sorted worst-first, so a manual pentester triages by impact - // instead of reading a flat dump of every range match. --component-cves-all + // instead of reading a flat dump of every range match. --component-cves-full // shows the full list; JSON (-j) is always complete regardless of the gate. if (passes.cve) { if (passes.secrets || passes.sbom) o += "\n"; @@ -194,7 +194,7 @@ std::string emit_report_human(const Report& rep, const Passes& passes, const std } o += "\n"; - // Rows to render: high-signal only (default) or all (--component-cves-all). + // Rows to render: high-signal only (default) or all (--component-cves-full). std::vector show; for (const auto& r : rows) if (rep.component_cves_all || r.high) show.push_back(r); @@ -259,7 +259,7 @@ std::string emit_report_human(const Report& rep, const Passes& passes, const std o += a.dim(); o += "\n Gated to high-signal (KEV, or High/Critical CVSS>=7.0 with EPSS " "traction, real impact or trending remote DoS).\n" - " Run --component-cves-all for every component CVE in range.\n"; + " Run --component-cves-full for every component CVE in range.\n"; o += a.reset(); } } @@ -404,7 +404,7 @@ std::string emit_report_human(const Report& rep, const Passes& passes, const std o += "\n"; } } - // --kernel-cves-all: the full kernel.org feed, version-matched. Can be + // --kernel-cves-full: the full kernel.org feed, version-matched. Can be // hundreds of lines, so cap the human list and point at -j for the rest. if (rep.kernel_cves_full) { o += "\n"; @@ -459,7 +459,7 @@ std::string emit_report_human(const Report& rep, const Passes& passes, const std o += rep.kernel_cves_full ? " Curated entries are kconfig-gated; feed entries are version-matched " "(confirm the backport)." - : " Curated high-signal set. Run --kernel-cves-all for every kernel CVE in range."; + : " Curated high-signal set. Run --kernel-cves-full for every kernel CVE in range."; o += a.reset(); o += "\n"; } diff --git a/src/json.cpp b/src/json.cpp index 934ab65..63d9f40 100644 --- a/src/json.cpp +++ b/src/json.cpp @@ -227,7 +227,7 @@ std::string emit_report_json(const Report& rep, const Passes& passes) { "(real-C/I-impact OR remote-DoS with epss>=0.70))", sf); kv_str(o, "note", "the \"cves\" array is complete; \"high_signal\" is the count shown in " - "the default human view; run --component-cves-all to list all in human output", + "the default human view; run --component-cves-full to list all in human output", sf); o += "}"; } @@ -293,7 +293,7 @@ std::string emit_report_json(const Report& rep, const Passes& passes) { ? "curated entries are kconfig-gated; feed entries (source " "\"kernel.org\") are version-matched, confirm the backport" : "curated high-signal set of widely-exploited kernel bugs, not every " - "CVE affecting this version; run --kernel-cves-all for the full feed", + "CVE affecting this version; run --kernel-cves-full for the full feed", sf); o += "}"; } diff --git a/src/kernelcve.hpp b/src/kernelcve.hpp index 4e422a0..4b032f1 100644 --- a/src/kernelcve.hpp +++ b/src/kernelcve.hpp @@ -36,7 +36,7 @@ struct KernelCveResult { bool applicable = true; // == (state == Applicable); kept for existing callers std::string reason; // why applicable/ruled-out/unknown, with the evidence source // Where this result came from: "curated" (the built-in high-signal table) or - // "kernel.org" (the full version-matched feed, only with --kernel-cves-all). + // "kernel.org" (the full version-matched feed, only with --kernel-cves-full). std::string source = "curated"; // Value-add annotations (never affect applicability): bool kev = false; // on the CISA Known-Exploited catalog diff --git a/src/kernelfeed.hpp b/src/kernelfeed.hpp index dfdbf35..3a96c01 100644 --- a/src/kernelfeed.hpp +++ b/src/kernelfeed.hpp @@ -1,4 +1,4 @@ -// kernelfeed.hpp — the full kernel.org (Linux CNA) CVE feed, for --kernel-cves-all. +// kernelfeed.hpp — the full kernel.org (Linux CNA) CVE feed, for --kernel-cves-full. // // The curated table (kernelcve.hpp) is the high-signal default: a hand-picked set // of widely-exploited kernel bugs, kconfig-gated. It is deliberately partial and diff --git a/src/main.cpp b/src/main.cpp index 19e2e23..08e55c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -46,30 +46,38 @@ void print_help(std::FILE* out, const char* prog, bool color) { std::fprintf(out, "Analyze software contents: secrets, components (SBOM), CVEs, licenses.\n\n"); std::fprintf(out, "%sUSAGE%s\n", B, R); std::fprintf(out, " %s [options] \n\n", prog); - std::fprintf(out, "%sOPTIONS%s\n", B, R); + std::fprintf(out, "%sPASSES%s (with none named, every pass runs)\n", B, R); std::fprintf(out, - " -j, --json JSON output (default: human-readable)\n" - " -v, --verbose Human output: show full values (no \"...\" truncation)\n" " --secrets Scan file content for credential/key material\n" " --sbom Inventory software components and versions\n" " --cve Match components against known vulnerabilities\n" " --licenses Identify open-source licenses\n" " --boot Boot-security intel: U-Boot env, device tree, FIT\n" " --keys Public-key weakness: leaked/default keys, ROCA, weak RSA\n" - " --license-paths List the file locations of each license, not just\n" - " a count (human output; implies --licenses)\n" - " -A, --all Run every pass (the default when none is selected)\n" - " --kernel-cves-all List every CVE for the detected kernel version, not\n" - " just the curated set (needs the kernel feed; implies --cve)\n" - " --component-cves-all Human view: list every component CVE, not just the\n" - " high-signal set (KEV / CVSS>=9.0 / EPSS>=0.10; implies --cve)\n" - " --rules Add user-defined rules from a JSON file\n" + " -A, --all Run every pass (the explicit form of the default)\n\n"); + std::fprintf(out, "%sOUTPUT%s\n", B, R); + std::fprintf(out, + " -j, --json JSON instead of human-readable. JSON is ALWAYS\n" + " complete; the flags below only expand the human view.\n" + " -v, --verbose Show full values, no \"...\" truncation\n" + " --license-paths List where each license was found (implies --licenses)\n" + " --component-cves-full List every component CVE, not just the\n" + " high-signal set (implies --cve)\n" + " --kernel-cves-full List every CVE for the detected kernel version, not\n" + " just the curated set (needs the kernel feed; implies --cve)\n" + " --full Everything above at once (verbose + paths + both -cves-full)\n" + " -C, --outdir Also write the report to DIR: mithril-report.json, plus\n" + " sbom.cdx.json and sbom.spdx.json when the SBOM pass runs\n\n"); + std::fprintf(out, "%sCVE MIRROR%s\n", B, R); + std::fprintf(out, " --fetch-db Download a prebuilt CVE mirror, then exit\n" " --update-db Rebuild the local CVE mirror from source, then exit\n" " --with-kernel-feed With --fetch-db/--update-db, also get the kernel feed\n" - " --no-fetch Never offer to download the CVE mirror (for scripts/CI;\n" - " env: MITHRIL_NO_FETCH). A missing mirror then just errors.\n" - " -C, --outdir Write the JSON report to DIR/mithril-report.json\n" + " --no-fetch Never offer to download the mirror (for scripts/CI;\n" + " env: MITHRIL_NO_FETCH). A missing mirror then just errors.\n\n"); + std::fprintf(out, "%sOTHER%s\n", B, R); + std::fprintf(out, + " --rules Add user-defined rules from a JSON file\n" " --dump-kconfig Print the recovered kernel .config to stdout, then exit\n" " --threads Worker threads for tree scans (default: auto)\n" " -h, --help Print help\n" @@ -78,10 +86,8 @@ void print_help(std::FILE* out, const char* prog, bool color) { std::fprintf(out, " %s ./rootfs/ Run every analysis on an unpacked tree\n" " %s --secrets ./rootfs/ Only the secrets pass\n" - " %s -j ./rootfs/ JSON for tools / LLMs\n\n", + " %s -j ./rootfs/ JSON for tools / LLMs\n", prog, prog, prog); - std::fprintf(out, "Pipeline: moria -e firmware.bin -> mithril firmware.bin.extracted/\n"); - std::fprintf(out, "Docs: README.md, docs/\n"); } @@ -150,14 +156,18 @@ int main(int argc, char** argv) { fetch_db = true; } else if (!end_of_opts && std::strcmp(a, "--dump-kconfig") == 0) { dump_kconfig = true; - } else if (!end_of_opts && std::strcmp(a, "--kernel-cves-all") == 0) { + } else if (!end_of_opts && std::strcmp(a, "--kernel-cves-full") == 0) { kernel_cves_all = true; - } else if (!end_of_opts && std::strcmp(a, "--component-cves-all") == 0) { + } else if (!end_of_opts && std::strcmp(a, "--component-cves-full") == 0) { component_cves_all = true; } else if (!end_of_opts && std::strcmp(a, "--license-paths") == 0) { license_paths = true; } else if (!end_of_opts && (std::strcmp(a, "-v") == 0 || std::strcmp(a, "--verbose") == 0)) { verbose = true; + } else if (!end_of_opts && std::strcmp(a, "--full") == 0) { + // One switch for maximum human detail: every "show more" knob at once. + // (JSON is already complete, so --full is a no-op there.) + verbose = component_cves_all = kernel_cves_all = license_paths = true; } else if (!end_of_opts && std::strcmp(a, "--with-kernel-feed") == 0) { with_kernel_feed = true; } else if (!end_of_opts && std::strcmp(a, "--no-fetch") == 0) { @@ -203,7 +213,7 @@ int main(int argc, char** argv) { return 1; } - // --kernel-cves-all / --component-cves-all are CVE-pass features; make sure + // --kernel-cves-full / --component-cves-full are CVE-pass features; make sure // that pass runs. if (kernel_cves_all) passes.cve = true; if (component_cves_all) passes.cve = true; @@ -379,7 +389,7 @@ int main(int argc, char** argv) { // the OSV/NVD mirror — the table is built in). rep.kernel_cves = kernel_cve_scan(rep.kernel_version, &rep.kcv); - // --kernel-cves-all: also join the full kernel.org (Linux CNA) feed, the + // --kernel-cves-full: also join the full kernel.org (Linux CNA) feed, the // complete version-matched set. Merged in, deduped against the curated // entries (which keep their kconfig gating and exploit notes). Feed entries // are version-matched only (the feed carries no CONFIG data), so they are @@ -387,7 +397,7 @@ int main(int argc, char** argv) { if (kernel_cves_all && !rep.kernel_version.empty()) { auto feed = load_kernel_feed(kernel_feed_path()); if (!feed) { - std::string msg = std::string("--kernel-cves-all: no kernel CVE feed; run '") + + std::string msg = std::string("--kernel-cves-full: no kernel CVE feed; run '") + prog + " --fetch-db --with-kernel-feed' first"; rep.errors.emplace_back(std::string(), msg); std::fprintf(stderr, "%s: %s\n", prog, msg.c_str()); diff --git a/src/report.hpp b/src/report.hpp index b24cee6..58d318f 100644 --- a/src/report.hpp +++ b/src/report.hpp @@ -57,9 +57,9 @@ struct Report { std::vector notable; // path-rule hits (credential/crypto/config files) std::vector components; // SBOM components (dpkg/opkg/apk/...) std::vector cves; // SBOM-vs-OSV join results - std::vector kernel_cves; // curated checklist (+ feed if --kernel-cves-all) - bool kernel_cves_full = false; // --kernel-cves-all: the kernel.org feed was merged in - bool component_cves_all = false; // --component-cves-all: show every component CVE in human view + std::vector kernel_cves; // curated checklist (+ feed if --kernel-cves-full) + bool kernel_cves_full = false; // --kernel-cves-full: the kernel.org feed was merged in + bool component_cves_all = false; // --component-cves-full: show every component CVE in human view std::vector licenses; // license findings (SPDX tags + license files) std::vector boot; // boot-security findings (U-Boot env, DTB/FIT) std::vector keys; // key-weakness findings (leaked/ROCA/weak public keys) diff --git a/tests/test_cve.py b/tests/test_cve.py index 5eb057a..06bab2d 100644 --- a/tests/test_cve.py +++ b/tests/test_cve.py @@ -157,13 +157,13 @@ def main(): if "CVE-2099-0002" not in h: print(f"FAIL: real-impact CVE should survive the EPSS arm:\n{h}") fails += 1 - if "--component-cves-all" not in h: - print(f"FAIL: gated human view should point to --component-cves-all:\n{h}") + if "--component-cves-full" not in h: + print(f"FAIL: gated human view should point to --component-cves-full:\n{h}") fails += 1 - # --component-cves-all lists every CVE, with no gate banner or footer. - hall = run_human(binary, os.path.join(d, "vuln"), dbdir, ["--component-cves-all"]) + # --component-cves-full lists every CVE, with no gate banner or footer. + hall = run_human(binary, os.path.join(d, "vuln"), dbdir, ["--component-cves-full"]) if "CVE-2021-28831" not in hall or "CVE-2099-0002" not in hall or "hidden" in hall: - print(f"FAIL: --component-cves-all should list every CVE:\n{hall}") + print(f"FAIL: --component-cves-full should list every CVE:\n{hall}") fails += 1 # No local mirror: a non-interactive --cve run must not prompt (stdin is @@ -193,7 +193,7 @@ def main(): print("PASS: vulnerable busybox flagged CVE-2021-28831 (severity+basis), " "patched clean, NVD/CPE join flagged openssl 1.1.1m, " "human view gates to foothold-worthy (hard High/Critical floor; remote DoS " - "kept only when trending; --component-cves-all opts out)") + "kept only when trending; --component-cves-full opts out)") return 0 return 1 diff --git a/tests/test_kernelfeed.py b/tests/test_kernelfeed.py index 75fcd73..c288938 100644 --- a/tests/test_kernelfeed.py +++ b/tests/test_kernelfeed.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 -"""test_kernelfeed.py — integration test for --kernel-cves-all (the full +"""test_kernelfeed.py — integration test for --kernel-cves-full (the full kernel.org CVE feed) and its merge with the curated checklist. Seeds a small kernel-cve-index.json fixture (the on-disk feed format) into a throwaway MITHRIL_DB, builds a tiny tree carrying a kernel banner, and asserts: - 1. --kernel-cves-all surfaces the in-range feed CVEs (source "kernel.org"), + 1. --kernel-cves-full surfaces the in-range feed CVEs (source "kernel.org"), branch-aware: a bug fixed in 6.6.17 hits 6.6.10 but NOT 6.6.110, even though the mainline window spans both. 2. the JSON kernel_cve_scan flips to method "curated+kernel.org", exhaustive true, with a feed_in_range count. - 3. without the feed asset, --kernel-cves-all fails loud (a scan-level error + 3. without the feed asset, --kernel-cves-full fails loud (a scan-level error naming --fetch-db --with-kernel-feed), not silently. Usage: tests/test_kernelfeed.py [path/to/mithril] @@ -73,7 +73,7 @@ def main(): # --- Case 1: 6.6.10 -> branch-precise bug + coarse bug apply, 5.15 one not --- with open(os.path.join(tree, "vmlinuz"), "wb") as f: f.write(banner("6.6.10") + os.urandom(4096)) - rep = run_json(binary, tree, env, extra=["--kernel-cves-all"]) + rep = run_json(binary, tree, env, extra=["--kernel-cves-full"]) feed = {k["cve"]: k for k in rep.get("kernel_cves", []) if k.get("source") == "kernel.org"} if "CVE-2024-90001" not in feed: print(f"FAIL(1): branch-precise CVE not matched for 6.6.10: {sorted(feed)}") @@ -99,7 +99,7 @@ def main(): # --- Case 2: 6.6.110 -> the 6.6-branch bug is fixed (6.6.17); NOT reported --- with open(os.path.join(tree, "vmlinuz"), "wb") as f: f.write(banner("6.6.110") + os.urandom(4096)) - rep2 = run_json(binary, tree, env, extra=["--kernel-cves-all"]) + rep2 = run_json(binary, tree, env, extra=["--kernel-cves-full"]) feed2 = {k["cve"]: k for k in rep2.get("kernel_cves", []) if k.get("source") == "kernel.org"} if "CVE-2024-90001" in feed2: print("FAIL(2): 6.6.110 wrongly flagged for a bug fixed in 6.6.17 (cross-branch FP)") @@ -108,21 +108,21 @@ def main(): print("FAIL(2): coarse-range CVE should still match 6.6.110") fails += 1 - # --- Case 3: default run (no --kernel-cves-all) does not consult the feed --- + # --- Case 3: default run (no --kernel-cves-full) does not consult the feed --- rep3 = run_json(binary, tree, env) if any(k.get("source") == "kernel.org" for k in rep3.get("kernel_cves", [])): - print("FAIL(3): feed CVEs leaked into a default scan (no --kernel-cves-all)") + print("FAIL(3): feed CVEs leaked into a default scan (no --kernel-cves-full)") fails += 1 if rep3.get("kernel_cve_scan", {}).get("exhaustive") is not False: print("FAIL(3): default scan should be non-exhaustive") fails += 1 - # --- Case 4: --kernel-cves-all with no feed asset -> loud, actionable error --- + # --- Case 4: --kernel-cves-full with no feed asset -> loud, actionable error --- with tempfile.TemporaryDirectory() as db2, tempfile.TemporaryDirectory() as tree2: env2 = {**os.environ, "MITHRIL_DB": db2, "NO_COLOR": "1"} with open(os.path.join(tree2, "vmlinuz"), "wb") as f: f.write(banner("6.6.10") + os.urandom(4096)) - r = subprocess.run([binary, "--cve", "--kernel-cves-all", tree2], + r = subprocess.run([binary, "--cve", "--kernel-cves-full", tree2], capture_output=True, env=env2) msg = (r.stdout + r.stderr).decode() if "no kernel CVE feed" not in msg or "--with-kernel-feed" not in msg: @@ -130,7 +130,7 @@ def main(): fails += 1 if fails == 0: - print("PASS: --kernel-cves-all merges the branch-aware kernel.org feed, " + print("PASS: --kernel-cves-full merges the branch-aware kernel.org feed, " "excludes fixed branches, and fails loud without the asset") return 0 return 1 From 04b311ef2b9a8ccd17f0db77854e0397188ca980 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Thu, 22 Oct 2026 14:07:58 -0400 Subject: [PATCH 2/3] docs(cli): document -C output set and distinguish it from moria's -C The -C/--outdir man entry only mentioned mithril-report.json, but -C also writes sbom.cdx.json and sbom.spdx.json when the SBOM pass runs, and those standard-format SBOMs are reachable only through -C (the JSON report carries components inline, but not the CycloneDX/SPDX documents). Spell that out in the man page and README, and note that moria(1) also has a -C that means something different (its extraction output directory) so a moria user is not surprised. Docs only; no behavior change. --- README.md | 2 +- mithril.1 | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0074ba8..50d697d 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Build it once either way; every scan after that is offline. See `docs/cve-join.m ## Scope - **secrets**: credential and key material in file content (cloud keys, tokens, JWTs, private keys, high-entropy `KEY=…` values). `/etc/shadow` and `htpasswd` hashes are classified with weak-algorithm and empty-password flags, and credential/crypto/config files are flagged by path. -- **sbom**: components and versions from package databases, language manifests, binary version strings, libc filenames, and the kernel banner, keyed on purl (with CPE co-derived), emitted as CycloneDX and SPDX. +- **sbom**: components and versions from package databases, language manifests, binary version strings, libc filenames, and the kernel banner, keyed on purl (with CPE co-derived). The JSON report always carries the components inline; the standard-format bills of materials (CycloneDX and SPDX) are written as `sbom.cdx.json` and `sbom.spdx.json` when you pass `-C/--outdir`. - **cve**: the SBOM joined against the local OSV + NVD/CPE mirror, plus the curated kernel-CVE checklist, annotated with KEV and EPSS. The default human view shows only foothold-worthy component CVEs (on CISA KEV, or Critical, or a hard High/Critical floor of CVSS >= 7.0 with EPSS traction, low attack complexity, and real impact or a trending remote DoS), sorted worst-first; it drops Mediums, local DoS, and the high-complexity crypto class. `--component-cves-full` lists them all. JSON always carries the complete set plus a `component_cve_scan` summary. - **licenses**: SPDX identification from `SPDX-License-Identifier` tags and LICENSE/COPYING/NOTICE text. Human output summarizes by id and count; `--license-paths` lists each license's file locations, and JSON always carries the full `paths` array. diff --git a/mithril.1 b/mithril.1 index b32bfd1..e22860f 100644 --- a/mithril.1 +++ b/mithril.1 @@ -166,8 +166,23 @@ nonzero, as in a non-interactive run. Use it in scripts and CI. Add user-defined detection rules from a JSON file. .TP .BR \-C ", " \-\-outdir " " \fIDIR\fR -Write the JSON report to -.IR DIR /mithril\-report.json . +Also write the report to +.IR DIR , +regardless of the stdout format: +.I mithril\-report.json +(the complete JSON result set) and, when the SBOM pass runs, +.I sbom.cdx.json +and +.I sbom.spdx.json +(the CycloneDX and SPDX bills of materials). These standard-format SBOMs are +produced only through +.BR \-C . +Note that +.BR moria (1) +also has a +.B \-C +but means its extraction output directory; here it is the report output +directory. .TP .BI \-\-threads " N" Worker threads for tree scans. Default: automatic. From 766152600682e6fbc265134bec6a9318237a4017 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Thu, 22 Oct 2026 15:10:14 -0400 Subject: [PATCH 3/3] release: v0.4.0 Changelog entry for the 0.4.0 release (EC small-scalar key detection, the interactive CVE-mirror download prompt, the coherent CLI flag renames, and the monolithic-SBOM recovery). VERSION and the man page were bumped with the flag-rename change; this adds the debian/changelog entry. --- debian/changelog | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/debian/changelog b/debian/changelog index f645f26..294d0f6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,20 @@ +mithril (0.4.0-1) unstable; urgency=medium + + * --keys: recover near-zero EC private scalars by a bounded d*G==Q search, + detecting the October 2026 eMudhra P-384 CA compromise (P-256/384/521 and + secp256k1); the eight real eMudhra intermediates are fingerprinted. + * --cve: on an interactive terminal with no local mirror, offer to download + it before scanning. New --no-fetch (and MITHRIL_NO_FETCH) suppress the + offer; non-interactive runs still error as before. + * CLI: breaking flag renames for coherence. --component-cves-all is now + --component-cves-full and --kernel-cves-all is now --kernel-cves-full + ("all" means all passes; "full" means all detail within a pass). New --full + for maximum human detail. --help is regrouped; JSON is always complete. + * SBOM: recover components from monolithic non-ELF firmware. Document that + -C/--outdir also writes sbom.cdx.json and sbom.spdx.json. + + -- Matt Brown Thu, 22 Oct 2026 15:09:46 -0400 + mithril (0.3.0-1) unstable; urgency=medium * New --boot Boot Security pass: U-Boot env, device tree, and FIT posture.