Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ mithril -j <dir> # JSON, for tools and agents
mithril --secrets <dir> # secrets only
mithril --sbom -C out/ <dir> # SBOM only -> out/sbom.cdx.json + out/sbom.spdx.json
mithril --cve <dir> # match components against the local mirror (offline)
mithril --cve --kernel-cves-all <dir> # also list every kernel.org CVE for the kernel version (opt-in)
mithril --cve --component-cves-all <dir> # human view: list every component CVE, not just high-signal (opt-in)
mithril --cve --kernel-cves-full <dir> # also list every kernel.org CVE for the kernel version (opt-in)
mithril --cve --component-cves-full <dir> # human view: list every component CVE, not just high-signal (opt-in)
mithril --licenses <dir> # licenses only
mithril --license-paths <dir> # licenses, with each license's file locations listed
mithril --boot <dir> # boot-security intel: U-Boot env, device tree, FIT/AVB, UEFI Secure Boot
Expand All @@ -69,15 +69,15 @@ 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`.

## 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.
- **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.
- **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.

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`.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.0
0.4.0
17 changes: 17 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -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 <matt@brownfinesecurity.com> 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.
Expand Down
14 changes: 7 additions & 7 deletions docs/cve-join.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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.
40 changes: 32 additions & 8 deletions mithril.1
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -157,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.
Expand Down
2 changes: 1 addition & 1 deletion src/cve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/cve.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/cveupdate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/cveupdate.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/dbpath.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
10 changes: 5 additions & 5 deletions src/human.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<Row> show;
for (const auto& r : rows)
if (rep.component_cves_all || r.high) show.push_back(r);
Expand Down Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
}
Expand Down
4 changes: 2 additions & 2 deletions src/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 += "}";
}
Expand Down Expand Up @@ -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 += "}";
}
Expand Down
2 changes: 1 addition & 1 deletion src/kernelcve.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading