malcontent finds supply chain compromises by diffing capabilities
#supply #chain #attack #detection
At a glance
- What is it?
- malcontent is a Go tool from Chainguard that scores a program's capabilities with YARA rules and, in its diff mode, compares two versions of a package to find what changed and what that change implies. It is built for Linux packages in CI pipelines, and its value depends on having a trustworthy baseline to compare against.
- Who is it for?
- malcontent is a good fit for teams that ship or consume Linux packages and want a capability baseline they can diff against a newer release, especially inside a CI gate built on the exit flags. It is a poor fit if you need a verdict on a single opaque binary with no prior version to compare, if you rely on Windows or macOS coverage, or if you cannot accept third-party rules with licensing restrictions in your build.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Capability context rather than a single malware verdict
malcontent is a supply chain security tool written in Go, and the idea at its centre is that a program is more informative when you know what it can do, not merely whether it matches a known bad sample. The documentation describes it as discovering supply-chain compromises through context, differential analysis and YARA. That is three ideas in one sentence, and each maps to a mode of operation. The rule base is large, with 14,500 or more YARA rules including third-party rules from Avast, Elastic, FireEye, Mandiant, Nextron and ReversingLabs. Input handling is broad. Binary files in the common formats are analyzed, a.out, ELF, Mach-O and PE, and code in AppleScript, C, Go, JavaScript and TypeScript, PHP, Perl, Python, Ruby and Shell is analyzed too. Archives and container images are supported transparently, so you can point it at a package archive or an OCI image without unpacking it first. Output comes as JSON, YAML, Markdown, Text or a Terminal and TUI rendering, and the project is built to run inside a CI/CD pipeline. Rules are also embedded so the tool keeps working on an air-gapped network, which matters for the organizations most likely to care about supply chain compromise.
Analyze, diff and scan are three different questions
The three modes are easy to confuse and each answers a different question. analyze is unfiltered analysis of what a program is capable of, producing a list of capabilities often seen in malware, categorized by risk level. The documentation is blunt about its accuracy in this mode. Without differential context, the tool stays paranoid, so expect some false positives, and the guidance is that CRITICAL findings should be considered malicious. diff is the risk-weighted differential analysis between two sources, and it is described as the most powerful method for discovering malware. The mode is designed around the idea that a legitimate release and a compromised release differ, and the risk direction matters more than the raw capability list. scan is threshold-based, filtering capabilities against a risk level so that a pipeline can pass or fail on a rule. The threshold is a command line choice rather than a fixed policy, with values of any, low, medium, high or critical for both the overall result and the per file result. The two numeric options --min-level and --min-file-level are marked obsolete in favour of the string forms, which is a small hint about how the interface has been moving.
Tuning the rule set with ignore flags and third-party rules
A rule corpus this size is only useful if you can shape it, and the global options give you several levers. --ignore-rules takes YARA rule names as a comma-separated list and supports filepath.Match globs, with the documentation offering py_lib_alias_val and py_lib_* as examples, which suggests the intended use is silencing noisy rules in a specific language family rather than curating a whole policy file. --ignore-tags takes rule tags and defaults to false_positive,ignore, so known-noisy findings are excluded before you look at anything. --include-data-files pulls in files detected as non-program files, binary or source, which widens coverage when a package ships data alongside code. There is also --all, which ignores nothing within a provided scan path, and --ignore-self, which excludes the malcontent binary from its own scan, a small courtesy that matters when you point the tool at a directory that contains it. The most consequential flag is --third-party, which includes third-party rules that may have licensing restrictions. The default is to leave them out, and the project itself is Apache-2.0, so the restriction is on the imported rule sets rather than on the tool. A commercial or redistributable deployment needs to settle that question before it enables the flag, and no legal interpretation is offered here beyond the fact that the distinction exists in the interface.
Driving the CLI: options, limits and exit codes
The tool ships as a single binary with subcommands, and the analyze subcommand is the entry point most people try first. The usage text reads:
NAME:
malcontent analyze - fully interrogate a path
USAGE:
malcontent analyze [options]
OPTIONS:
--image string, -i string [ --image string, -i string ] Scan one or more images
--processes Scan the commands (paths) of running processes
--help, -h show helpThe scan of a running system is therefore a first class use, through --processes, and container images are named with --image. The global options that matter for a pipeline are these:
GLOBAL OPTIONS:
--all Ignore nothing within a provided scan path
--exit-extraction Exit when encountering file extraction errors
--exit-first-miss Exit with error if scan source has no matching capabilities
--exit-first-hit Exit with error if scan source has matching capabilities
--format string Output format (interactive, json, markdown, simple, strings, terminal, yaml) (default: "auto")
--jobs int, -j int Concurrently scan files within target scan paths (default: 12)The exit flags are the piece that makes the tool usable as a gate, since --exit-first-hit fails a build on a match and --exit-first-miss fails when nothing matched, which catches a rule set that has quietly stopped working. --jobs defaults to 12 concurrent file scans, --output writes to a file rather than stdout, and --stats prints scan statistics. The readme does not document a package manager install or a single line install command, so obtain a tagged release binary from the project releases, or build from source, which needs Go 1.27.0 as declared in go.mod. The build also depends on the yara-x C API, since the Makefile sets linker flags for a local yara_x_capi library.
Registry credentials scoped to a single host, on purpose
Pulling container images is a place where a security tool can leak credentials, and malcontent handles it in a deliberately narrow way. OCI pulls are anonymous by default. Authentication is opt-in through the --oci-auth flag, which reads Basic credentials from the MALCONTENT_REGISTRY_USER and MALCONTENT_REGISTRY_PASS environment variables, and those credentials are scoped to one registry, sent only when the image registry host matches MALCONTENT_REGISTRY_HOST exactly, with an anonymous fallback for anything else. The point is that a private credential is never presented to an unexpected and potentially malicious registry that happens to be referenced by a manifest. The ambient Docker keychain at ~/.docker/config.json is intentionally never used, which is a trade-off for anyone who expects their existing login to just work. Host names are normalized by the google/go-containerregistry library, so the documented value for Docker Hub is index.docker.io rather than docker.io, and getting that wrong silently produces anonymous behaviour rather than an error. The go.mod pins google/go-containerregistry v0.22.1, and the tool also supports OCI authentication scoped by the same variables when scanning images in CI.
Platform coverage is uneven, and the readme says so
The clearest limitation is stated in the project itself. malcontent is at its best analyzing programs that run on Linux, and while it performs well for programs designed for other UNIX platforms such as macOS, it does so to a lesser extent for Windows. That ordering follows from the rule corpus, which is built around a Linux package ecosystem, and it means a Windows-heavy shop should expect thinner coverage than the rule count suggests. The other limitation is behavioural. In analyze and scan mode, with no differential context available, the tool stays paranoid and false positives are expected, so a bare scan of an unfamiliar binary produces a wall of capability findings with no indication of which one is new. The diff mode is what turns that list into a signal, and it needs two sources that are meant to be the same software. Extraction limits bound the work as well: --max-depth caps archive recursion at 32 by default, --max-files caps the number of files scanned at 2097152, and --max-image-size caps OCI images at 17179869184 bytes. Each takes 0 or -1 for unlimited, so a large image or a deeply nested archive can be silently truncated unless you raise them.
Compared with running YARA yourself
The go.mod makes the underlying engine visible. The project requires github.com/VirusTotal/yara-x/go v1.21.0, and the yara-x C API is what the Makefile links against, so malcontent is a layer above a scanner you could otherwise call directly:
module github.com/chainguard-dev/malcontent
go 1.27.0
require (
github.com/VirusTotal/yara-x/go v1.21.0
github.com/urfave/cli/v3 v3.14.0
github.com/google/go-containerregistry v0.22.1
)Using yara-x directly gives you rule matches and nothing else. malcontent adds three things on top: a capability taxonomy where a match becomes a named capability with a risk level, a diff mode that compares two artifacts and reports risk direction, and rendering, with JSON, YAML, Markdown and a terminal interface built on charmbracelet bubbletea and lipgloss for the TUI. The cost of that layer is that the rule set is curated and opinionated, and a large part of it comes from third-party vendors whose licensing you have to accept with --third-party. The Makefile also references a sample repository for testing, so a corpus of expected results exists:
SAMPLES_REPO ?= chainguard-sandbox/malcontent-samples
SAMPLES_COMMIT ?= fca96935d01d3b0a7a2765626279c676bd9d495fThat pinning suggests regression coverage exists for rule behaviour, which matters more for a detector than for most tools, since a silent rule regression looks exactly like a clean file.
Editorial conclusion
malcontent is a good fit for teams that ship or consume Linux packages and want a capability baseline they can diff against a newer release, especially inside a CI gate built on the exit flags. It is a poor fit if you need a verdict on a single opaque binary with no prior version to compare, if you rely on Windows or macOS coverage, or if you cannot accept third-party rules with licensing restrictions in your build. Verify first by running the same artifact through analyze and through diff to see whether the risk wording matches your own tolerance. The last push was on 2026-10-20 and the current release is v1.27.1 from 2026-10-07.
Frequently asked questions
What are the three modes of malcontent?
analyze enumerates the capabilities of a program without filtering, diff performs risk-weighted differential analysis between two sources, and scan applies a risk threshold so a pipeline can pass or fail. The documentation describes diff as the most powerful of the three.
How do I install malcontent?
The readme does not give a package manager install command, so use a tagged release binary from the project releases, or build from source. Building requires Go 1.27.0 and the yara-x C API, since the Makefile links against a local yara_x_capi library.
Does malcontent send my registry credentials to other registries?
No. OCI pulls are anonymous by default, and with --oci-auth the credentials from MALCONTENT_REGISTRY_USER and MALCONTENT_REGISTRY_PASS are sent only when the registry host matches MALCONTENT_REGISTRY_HOST exactly. The ambient Docker keychain is never used.
What does --third-party do in malcontent?
It includes third-party rules which may have licensing restrictions. Those rules are excluded by default, and the tool itself is Apache-2.0, so the licensing question applies to the imported rule sets.
Can malcontent be used in CI/CD pipelines?
The project is designed for that use, with exit flags such as --exit-first-hit and --exit-first-miss, output written with --output and scan statistics with --stats. The documentation does not describe a hosted service, so the pipeline runs the binary itself.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/chainguard-dev-malcontent)Was this useful?