trustmod is a Go-native dependency review assistant. It looks at the module
graph, local source evidence, vulnerability data, repository metadata, policy
rules, and baselines, then returns one of three verdicts: ALLOW, REVIEW, or
BLOCK.
It does not prove that a package is safe. The goal is more practical: make dependency changes easier to review, and avoid quietly adding packages with surprising security, maintenance, malware-like, or footprint signals.
trustmod module review
Module: github.com/example/lib
Version: v0.8.1
Verdict: REVIEW
Risk: 48/100
Profile: backend-service
Why:
REVIEW TM-FP-001 Adds many transitive modules
ALLOW TM-VER-005 Module is below v1
ALLOW TM-CAP-002 Filesystem write capability detected
Data:
osv ok
deps.dev ok
github ok
scorecard skipped_no_provider_data
Requires Go 1.23 or newer.
go install github.com/maksemen2/trustmod/cmd/trustmod@latestFrom a checkout:
go install ./cmd/trustmodtrustmod init
trustmod audit .
trustmod check github.com/go-chi/chi/v5
trustmod diff --base mainUseful report formats:
trustmod audit --format json --out trustmod.json
trustmod audit --format markdown --out trustmod.md
trustmod audit --format sarif --out trustmod.sarif
trustmod graph --format dot --out deps.dotReview a dependency before adding it:
trustmod check github.com/go-chi/chi/v5Review and add a dependency in one command:
trustmod add github.com/go-chi/chi/v5
trustmod add github.com/example/review-me --dry-run
trustmod add github.com/example/review-me --allow-review --tidyadd runs the same review before invoking go get. A REVIEW result requires
--allow-review; a BLOCK result requires --force or --allow-block.
--require-allow refuses every non-ALLOW result.
Review a pull request:
trustmod diff --base main
trustmod diff --base main --changed-files-only
trustmod diff --base main --only-newCompare alternatives:
trustmod compare \
github.com/go-chi/chi/v5 \
github.com/gin-gonic/gin \
github.com/labstack/echo/v4 \
--use-case http-routertrustmod starts from --cwd or the current directory and uses the nearest
go.work or go.mod.
If a repository has a go.work file, trustmod audit --cwd repo analyzes the
workspace modules listed in go.work use (...) as one workspace. This is a good
fit for a monorepo with a backend service, cron jobs, and workers that should be
reviewed together.
Without go.work, run trustmod for each application:
trustmod audit --cwd ./backend
trustmod audit --cwd ./cron-cleanup
trustmod audit --cwd ./workerIf you run from a repository root that has no go.mod and no go.work,
trustmod does not recursively guess which nested modules matter. That avoids
accidentally analyzing fixtures, examples, or test modules as production code.
trustmod diff handles nested modules when you run it from the nested project.
For example, trustmod --cwd ./backend diff --base main compares
main:backend/go.mod with the current backend module.
ALLOW: no blocking or review-level findings under the active policy.REVIEW: a human should look at the change before it is accepted.BLOCK: the policy says this should not pass.
Provider failures are reported as data availability statuses. They do not fail
the process unless --strict-data is enabled.
Low-context signals, such as a v0 module, a normal environment read, or weak
maintenance metadata, are still shown and still add risk points. Under the
default profile, they do not automatically force REVIEW unless the combined
risk crosses the policy threshold.
0: success; no policy failure.1: policy failure according tofail_onor--fail-on.2: usage error, unsupported format, bad flag, or bad user-supplied file.3: invalid config, policy, or explicitly requested baseline.4: local Go analysis failed.5: provider failure while--strict-datais enabled.6: privacy guard prevented a remote provider query.7: internal error.
trustmod init writes a starter .trustmod.yaml,
.trustmod/policy.yml, .trustmod/baseline.yml, and
.trustmod/rules.yml.
Configuration precedence is:
- CLI flags
- Environment variables
- Config and policy files
- Built-in defaults
Example .trustmod.yaml:
default_profile: backend-service
output: human
policy_path: .trustmod/policy.yml
baseline_path: .trustmod/baseline.yml
rules_path: .trustmod/rules.yml
timeout: 20s
concurrency: 8
allow_private_remote: false
offline: false
strict_data: false
no_cache: false
include_tests: false
providers:
osv: true
deps.dev: true
github: true
scorecard: true
govulncheck: falseExample policy:
version: 1
profile: backend-service
fail_on: [BLOCK]
licenses:
banned: [AGPL-3.0, GPL-3.0]
deny:
modules:
- example.com/disallowed/...
allow:
finding_codes:
- TM-VER-005
thresholds:
risk_review: 30
risk_block: 101
transitive_review: 20
profiles:
strict:
strict: true
fail_on: [BLOCK, REVIEW]
providers:
required: [osv]Useful policy commands:
trustmod policy validate
trustmod policy print-default
trustmod policy explain
trustmod policy test trustmod.jsonBuilt-in rules cover common Go dependency risks. Teams can add project-specific
source rules in .trustmod/rules.yml or pass a file explicitly:
trustmod audit --rules .trustmod/rules.yml
trustmod check github.com/mymmrac/telego --rules examples/rules/telegram-domain.ymlExample rule:
version: 1
rules:
- id: org-telegram-api
code: TM-CUSTOM-TELEGRAM
title: Telegram API access
description: The dependency contains a literal Telegram API endpoint.
severity: medium
verdict: REVIEW
confidence: high
remediation:
- Confirm that outbound Telegram API access is expected for this dependency.
match:
domains:
- telegram.orgRule matchers currently support imports, selectors, string literals through
strings, and literal URL/host domains through domains. Set
require_all: true when every listed condition must match in the same file.
Custom finding codes are stable and can be used in policy allow lists and
baselines.
Baselines accept known findings without hiding them. Accepted findings remain in
JSON with baselineAccepted: true, but they no longer drive the final verdict.
trustmod baseline approve github.com/legacy/package \
--code TM-MNT-002 \
--reason "approved legacy dependency" \
--expires 2026-12-31
trustmod baseline list
trustmod baseline revoke github.com/legacy/package --code TM-MNT-002
trustmod baseline pruneExample baseline entry:
version: 1
created_at: "2026-05-22T00:00:00Z"
accepted_findings:
- module: github.com/legacy/package
version: v1.2.3
code: TM-MNT-002
reason: Stable approved legacy dependency
approved_by: platform-team
expires: "2026-12-31"Local evidence comes first:
go list -m -json allfor selected modules, replacements, retractions, and deprecations where Go reports them.go list -deps -json ./...for package and test footprint.go mod graphandgo mod why -mfor graph and path hints.go mod verifyfor checksum verification.- A local AST scan for imports, calls,
unsafe, cgo,initfunctions, process execution, filesystem writes, network APIs, environment access, plugin loading, insecure randomness, and literal network request domains where they can be determined safely. - Malware-oriented local source rules for encoded payload execution,
download-and-execute chains, sensitive data exfiltration patterns,
/etc/passwdaccess, shell downloader pipelines, and suspicious URL infrastructure. - Optional custom source rules from
.trustmod/rules.ymlor--rules.
Optional enrichment:
- OSV for advisory data.
- deps.dev for licenses, advisories, and package metadata.
- GitHub API for repository metadata.
- OpenSSF Scorecard for maintenance signals.
govulncheckfor reachable vulnerability evidence when requested.
Run govulncheck when it is installed:
trustmod audit --govulncheck
trustmod audit --govulncheck-path ./bin/govulncheckProviders can be disabled with flags such as --disable-github,
--disable-scorecard, --disable-depsdev, --disable-osv, and
--disable-govulncheck, or through .trustmod.yaml.
Provider statuses are meant to be explicit:
ok: data returned.disabled: intentionally disabled.not_requested: available but not requested, such as defaultgovulncheck.skipped_private: private module paths were withheld.skipped_no_public_modules: there was nothing public to query.skipped_no_eligible_versions: the provider could not query the selected versions.skipped_unsupported_host: the provider does not support that module host.skipped_no_provider_data: the provider has no data, such as a Scorecard 404.offline_cache_hit: offline mode found cached data.offline_cache_miss,unavailable,rate_limited,timeout,cancelled,error: data was expected but unavailable.
Use --strict-data when missing enabled provider data should fail CI.
The first run for a large module can be dominated by Go module downloads and remote providers. Repeated runs are much faster when the Go module cache and the trustmod provider cache are warm.
Useful knobs:
# Keep provider caching enabled. Avoid --no-cache unless debugging freshness.
trustmod check google.golang.org/grpc
# Increase parallelism for provider calls and local analysis phases.
trustmod check google.golang.org/grpc --concurrency 16
# Run only local evidence when you need a quick first pass.
trustmod check google.golang.org/grpc \
--disable-github \
--disable-scorecard \
--disable-depsdev \
--disable-osv
# Use cached provider data only.
trustmod check google.golang.org/grpc --offlineFor trustmod check, the local capability scan is targeted at the requested
module source tree instead of scanning every transitive dependency source tree.
Transitive dependencies are still represented in the module graph and can still
be enriched by enabled providers.
trustmod has no telemetry, analytics, hosted backend, or default update check.
Private module paths are not sent to remote providers by default. Private module detection uses:
GOPRIVATEGONOPROXYGONOSUMDB- local
replacedirectives - main module paths
- workspace module paths
Allow remote enrichment for private paths only when that disclosure is acceptable:
trustmod audit --allow-private-remoteThe cache stores provider responses keyed by request metadata. Tokens are not stored in the cache.
trustmod cache path
trustmod cache stats
trustmod cache prune
trustmod cache clearOffline mode keeps local Go analysis and cache reads:
trustmod audit --offlineGitHub metadata is optional, but unauthenticated API calls are severely
rate-limited. If GitHub returns a primary or secondary rate limit, trustmod
reports rate_limited and stops querying GitHub for that run.
Set TRUSTMOD_GITHUB_TOKEN or GITHUB_TOKEN to raise limits. With a token,
trustmod uses GitHub GraphQL to fetch repository metadata, including exact
archive timestamps. Without a token, REST fallback can still report archive
status, but not always the archive timestamp.
Create a minimal fine-grained token:
- Open GitHub Settings -> Developer settings -> Personal access tokens -> Fine-grained tokens.
- Generate a new token.
- Choose public read-only repository access when available, or restrict it to the repositories you need.
- Do not grant write permissions.
Shell:
export TRUSTMOD_GITHUB_TOKEN="github_pat_..."
trustmod check github.com/gin-gonic/gin
unset TRUSTMOD_GITHUB_TOKENGitHub Actions:
env:
GITHUB_TOKEN: ${{ github.token }}This repository includes a composite action in action.yml. It does not need a
separate action repository; consumers can reference this repository directly:
name: trustmod
on:
pull_request:
jobs:
review:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.23"
- id: trustmod
uses: maksemen2/trustmod@v0.1.0
with:
command: diff
base: main
sarif: trustmod.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always() && steps.trustmod.outputs['sarif-path'] != ''
with:
sarif_file: ${{ steps.trustmod.outputs['sarif-path'] }}The action supports:
command:audit,diff, orcheck.args: extra trustmod arguments.base: base ref fordiff.format: stdout report format.policy: policy file path.baseline: baseline file path.rules: custom source rules YAML path.sarif: optional SARIF output path.offline: run without remote provider calls.govulncheck: run localgovulncheck.install-govulncheck: installgovulncheckfirst.govulncheck-version: version used by the optional install step.concurrency: maximum concurrency per analysis phase.timeout: per-command/provider timeout.strict-data: fail when enabled provider data is unavailable.
Outputs:
exit-codesarif-path
Pin to a release tag such as maksemen2/trustmod@v0.1.0 for reproducible CI.
Use a moving major tag such as @v0 only if you want compatible fixes without
editing workflow files.
Finding codes are stable and are safe to use in policy and baseline files.
Security:
TM-SEC-001: reachable known vulnerability.TM-SEC-002: known vulnerability, reachability unknown.TM-SEC-003: known vulnerability, not reached by current analysis.TM-SEC-004: vulnerability scanner unavailable.TM-SEC-005: known malicious package indicator.TM-SEC-006: checksum verification failed.TM-SEC-007:go.summissing required entry.TM-SEC-008: security advisory data unavailable.
Malware-oriented local rules:
TM-MAL-001: encoded payload execution pattern.TM-MAL-002: download and execute pattern.TM-MAL-003: sensitive data exfiltration pattern.TM-MAL-004: suspicious/etc/passwdaccess.TM-MAL-005: suspicious URL literal.TM-MAL-006: shell downloader pipeline.
Other families:
TM-ID-*: module identity, repository, canonicalization, and replace signals.TM-VER-*: versioning signals, including retractions, pre-releases, pseudo-versions, v0 modules, and downgrades.TM-MNT-*: maintenance signals. Archived GitHub repositories include the archive timestamp and age when token-backed GraphQL metadata is available.TM-LIC-*: license signals.TM-FP-*: dependency footprint signals.TM-CAP-*: local capability signals.TM-CUSTOM-*: custom source rules loaded from.trustmod/rules.ymlor--rules.TM-POL-*: policy-generated findings.TM-BAS-001: baseline exception expired.TM-GO-001: local Go command failed.TM-GIT-001: git diff data unavailable.
For local remediation text:
trustmod explain TM-VER-005JSON is the most complete format. Markdown is intended for PR comments. SARIF can be uploaded to GitHub code scanning. JUnit is useful when CI systems expect test-like failure output.
Capability evidence includes file, line, matched text, and best-effort source
links. Network client capabilities also include literal request domains when
they are visible in source, with long domain lists truncated in human output.
Public GitHub modules get web links when trustmod can derive a version ref.
During local human output, local files may also be shown as vscode://file/...
links. Machine-specific local paths are not serialized into JSON or YAML.
trustmod report can render an existing project report or compare report:
trustmod report trustmod.json --format markdown --out trustmod.md
trustmod report compare.json --format markdown --out comparison.mdThe repository includes small synthetic modules under examples/evaluation.
They are intentionally boring: each one isolates a behavior so reviewers can see
what is supposed to be ALLOW, REVIEW, or BLOCK.
| Case | Command | Expected signal |
|---|---|---|
| Benign HTTP client | trustmod audit --cwd examples/evaluation/benign/http-client --offline |
TM-CAP-003, no TM-MAL-* |
| Suspicious URL literal | trustmod audit --cwd examples/evaluation/suspicious/rare-domain --offline |
TM-MAL-005 |
| Custom Telegram rule | trustmod audit --cwd examples/evaluation/suspicious/telegram-api --rules examples/rules/telegram-domain.yml --offline |
TM-CUSTOM-TELEGRAM |
| Download and execute | trustmod audit --cwd examples/evaluation/malicious/download-exec --offline |
TM-MAL-002, BLOCK |
| Secret exfiltration pattern | trustmod audit --cwd examples/evaluation/malicious/exfiltrate-secret --offline |
TM-MAL-003, BLOCK |
The same corpus is covered by automated tests, so rule changes have to preserve the expected benign/suspicious/malicious split.
Use trustmod as a review assistant, not as a guarantee.
- Capabilities are not vulnerabilities.
- Static scanning is heuristic and does not execute code.
- Provider data may be stale, unavailable, or rate-limited.
- Some providers do not apply to every module host.
- GitHub stars and repository activity are weak signals.
govulncheckreachability depends on build configuration and installed tool behavior.- Private modules are skipped remotely by default, so remote advisory and license data may be absent.
- License output is not legal advice.
go test ./...
go vet ./...
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest runBuild a local binary:
go build -o trustmod ./cmd/trustmod