Modes (Fast vs Deep)¶
depshieldx has two modes:
fastdeep
Plain install and plain scan default to fast.
deep is supported for PyPI, npm/yarn/pnpm, Cargo/crates.io, Go modules,
Maven/Maven Central, NuGet/NuGet.org, Pub/pub.dev, RubyGems/rubygems.org, and
Composer/Packagist input.
Fast mode¶
Fast mode:
- resolves the exact package versions that would be installed
- checks provenance for the resolved package set
- queries OSV, GitHub Advisories, CISA KEV, and deps.dev
- blocks if any resolved package or dependency is reported as vulnerable by the blocking sources
Fast mode does not use Docker or Trivy.
Deep mode¶
Deep mode does everything in fast mode first, then:
- installs the resolved package set in Docker
- runs Trivy on the sandboxed install result
- blocks if the Docker environment is unavailable or Trivy returns blocking findings
For install --deep, the host install only happens after the fast checks and the
Docker + Trivy stage both pass.
depshieldx shells out to the local pip (or, for npm, the local npm; for Cargo,
the local cargo; for Go, the local go; for Maven, the local mvn; for NuGet, the
local dotnet; for Pub, the local dart; for RubyGems, the local bundle; for
Composer, the local composer) for resolution, download, and host install steps, so
keeping those tools up to date is part of the security model.
Behavioral tracing, per ecosystem¶
How each ecosystem is traced
Deep mode traces filesystem writes, subprocess launches, and network access
in-process during the sandboxed install (via sys.addaudithook) and actively
blocks disallowed ones in real time.
npm has no equivalent in-process hook, so behavioral tracing instead wraps the
sandboxed npm install in strace, observing the same categories of activity
across the whole install (including lifecycle scripts) rather than blocking
individual syscalls live -- filesystem/network isolation is still enforced
by the container itself either way. The npm sandbox runs in a small node:20 +
strace image depshieldx builds and caches locally the first time it's
needed.
Cargo/crates.io behavioral tracing works the same way as npm's: the sandboxed
cargo build --offline runs wrapped in strace, observing filesystem,
process, and network activity (including build.rs scripts and proc-macros)
rather than blocking individual syscalls live. The Cargo sandbox runs in a
small rust:1-slim + strace image built and cached locally the first time
it's needed. One mechanical difference from npm/PyPI: since there's no new
installed output that needs to survive the sandbox, Trivy scans a host-side
vendor directory of the resolved .crate files (checksum-verified against
crates.io) built before the container runs, rather than a bind-mounted install
destination.
Go modules behavioral tracing works the same way: the sandboxed go build
runs wrapped in strace, observing filesystem, process, and network activity
(init() functions and //go:generate-produced code are Go's equivalent of
Cargo's build.rs/proc-macros) rather than blocking individual syscalls live.
The Go sandbox runs in a small golang:1-bookworm + strace image built and
cached locally the first time it's needed. Like Cargo, Trivy scans a host-side
go.mod/go.sum pair built before the container runs -- Trivy reads Go's
manifest files natively, needing no extracted source tree the way Cargo's
vendor directory does. One Go-specific wrinkle: unlike Cargo (which compiles
every declared dependency regardless of use), Go only compiles what's actually
imported, so a scratch program blank-imports every resolved module to force
real compilation -- and not every module has an importable root package
(some are subpackage-only, e.g. golang.org/x/crypto). Those are skipped from
tracing rather than failing the whole build, and listed in the full JSON
report's skipped_modules.
Maven behavioral tracing is different in one fundamental way: unlike Cargo's
build.rs or Go's init(), a jar consumed as a plain Maven dependency has no
code that runs automatically just by being resolved, or even sitting on the
compile classpath. The one real exception is an annotation processor
registered via META-INF/services (Lombok, MapStruct, Dagger, and similar)
-- javac auto-discovers and invokes it during any compile it's present
for, regardless of whether the compiled source actually uses its target
annotations. So the sandboxed mvn compile (against a trivial scratch source
file, wrapped in strace) traces real activity for that class of dependency,
and genuinely zero extra activity for the (large majority) of ordinary
libraries that register no processor -- an accurate verdict, not a
coverage gap. The Maven sandbox runs in a small
maven:3-eclipse-temurin-21 + strace image built and cached locally the
first time it's needed, with Maven's own default-lifecycle plugin set
pre-warmed into the image at build time. Like Cargo/Go, Trivy scans a
host-side scratch pom.xml built before the container runs, listing every
resolved coordinate as a pinned direct dependency.
NuGet behavioral tracing has the broadest code-execution surface of the four
compiled ecosystems: a .nupkg consumed as a plain PackageReference has no
code that runs automatically during dotnet restore alone, but any package
shipping a build/*.targets or build/*.props file gets it imported and
evaluated during dotnet build -- not a narrow processor-registration
mechanism like Maven's, but MSBuild's own general build-time extensibility
point, available to any package that uses it. So the sandboxed dotnet build
(against a trivial scratch source file, wrapped in strace) traces this real,
broader surface. The NuGet sandbox runs in a small
mcr.microsoft.com/dotnet/sdk:8.0 + strace image built and cached locally
the first time it's needed -- no plugin pre-warming needed, unlike
Maven's compile goal. Like Cargo/Go/Maven, Trivy scans a host-side
packages.lock.json built before the container runs (via a real, networked
dotnet restore against the resolved set) -- Trivy's NuGet support needs
a real lock file, detecting nothing from a bare .csproj.
Pub's own real code-execution surface is Dart's official Native Assets
"hooks" feature: a package can ship a hook/build.dart file (a real Dart
entry point, typically used to compile a native C/Rust library) that the
toolchain invokes for the root package and every transitive dependency during
dart run/dart test -- confirmed this fires even when nothing actually
imports the package, the same "presence in the dependency graph is enough"
pattern Maven's/NuGet's own surfaces have. dart pub get alone never triggers
hooks -- so the sandboxed dart run (against a trivial scratch
entry-point file, wrapped in strace) is what actually traces this surface;
dart compile exe was deliberately not used instead, since it refuses to run
hooks at all. The Pub sandbox runs in a small dart:3 + strace image built
and cached locally the first time it's needed. Like the other ecosystems,
Trivy scans a host-side pubspec.lock built before the container runs (via a
real, offline dart pub get against the resolved set's own freshly-built
local package cache).
RubyGems behavioral tracing looks different from every other ecosystem here
in one fundamental way: a native-extension gem's own extconf.rb build
script runs as an unavoidable, inherent part of installing the gem itself,
not a separate later run/build step -- so there's no clean split between
"resolve/restore, unstraced" and "run/build, straced". bundle install is
never safe to run on the host at all -- unlike
cargo fetch --locked/go mod download/dotnet restore/dart pub get, none
of which invoke a compiler, bundle install genuinely compiles native code
for any gem that ships one. So depshieldx never shells out to
bundle install/bundle lock on the host to build the Trivy-facing
lockfile; it writes Gemfile.lock directly from the resolution it already has
(itself produced by a real, safe bundle lock scratch resolve). The sandboxed
bundle install --local -- the same command Docker deep mode's offline
install step already runs -- is what gets wrapped in strace instead.
The RubyGems sandbox runs in a small ruby:3 + strace image built and
cached locally the first time it's needed -- deliberately not -slim,
since it needs to ship a real C toolchain (gcc/make) for native-extension
compilation to succeed (nokogiri, sqlite3, pg, bcrypt, ...).
Composer behavioral tracing targets a narrower, but real and non-opt-in,
surface: a plain composer install --no-plugins --no-scripts never executes a
dependency's own code at all -- Composer 2.2+ blocks any package
containing a Composer plugin by default unless the project explicitly
allow-lists it (depshieldx never does), and Composer's script hooks are
root-project-only by design. The real surface is PHP's own "files" autoload
mechanism: a package can declare
"autoload": {"files": ["bootstrap.php"]}, and that file executes
unconditionally the moment anything actually loads the generated
vendor/autoload.php -- even with zero explicit reference to the
package's own classes -- unlike ordinary PSR-4 class autoloading, which
is lazy. So the sandboxed install runs composer install unstraced first
(already proven safe), then straces a trivial scratch probe script that does
nothing but load the autoloader. The Composer sandbox runs in a small
php:8.4-cli + strace image built and cached locally the first time it's
needed, with the zip PHP extension and unzip added, and Composer itself
copied from the official composer:2 image.
Install vs Scan¶
install and scan use the same fast/deep validation logic.
The only difference is:
installinstalls on the host after the checks passscanstops after the checks and does not install anything
This same behavior applies to:
- direct package names
- multiple package names in one command
requirements.txtuv.lockpyproject.toml