A dependency doctor for .NET projects
PackageMedic 0.6 is the verified-experiments release of the read-only dependency doctor for SDK-style .NET projects. It can compare restore, build, and structured test evidence across independent immutable snapshots; export a deterministic CycloneDX 1.7 NuGet SBOM; and bind conclusive verified diffs to unsigned in-toto analysis evidence. The existing Impact Gate, Dependency Time Machine, PM001-PM008 diagnostics, semantic risk deltas, JSON, SARIF, and GitHub Action remain part of the same workflow.
Important: PackageMedic 0.6 never changes dependency declarations in the checkout.
--verify build|testis opt-in because restore, builds, analyzers, source generators, adapters, and tests can execute repository-controlled code. Snapshot isolation protects checkout integrity; it is not an operating-system sandbox or a package-safety verdict.
PackageMedic is not affiliated with, maintained by, sponsored by, or endorsed by Microsoft. .NET, NuGet, and related names are trademarks of their respective owners.
Read the complete PackageMedic documentation for installation, commands, configuration, baselines, GitHub Actions, reports, diagnostics, security, and troubleshooting.
Dependency problems often emerge across project boundaries: a PackageVersion can be unused in an entire central-management scope, two projects can quietly select different direct versions, or a harmless-looking direct-package update can add dozens of transitives from an unexpected source. PackageMedic evaluates projects through MSBuild and reads NuGet's resolved assets graph so findings and pull-request changes have project context and a causal path.
PackageMedic never writes to project files, props files, lock files, or assets files in the checkout. simulate edits one validated version value only in an owned disposable snapshot. Unless --no-restore is supplied, doctor runs the standard dotnet restore command. Vulnerability and deprecation auditing invoke separate official SDK/NuGet commands and may still contact configured feeds.
Install the latest stable release from NuGet:
dotnet tool install --global PackageMedic.ToolInstall PackageMedic 0.6 explicitly:
dotnet tool install --global PackageMedic.Tool --version 0.6.1Update an existing installation:
dotnet tool update --global PackageMedic.Tool- .NET 8 or a newer .NET runtime.
- A .NET SDK capable of loading the projects being analyzed.
The tool targets net8.0 to keep its minimum runtime broad and its runtime configuration permits compatible major-version roll-forward when only a newer .NET runtime is installed. Project evaluation and restore run through the active dotnet SDK, so SDK-style projects targeting .NET 8, 9, and 10 can be analyzed when their required SDK is installed.
Build and install a local package:
dotnet restore PackageMedic.sln --locked-mode
dotnet build --configuration Release
dotnet test --configuration Release
dotnet pack src/PackageMedic.Cli --configuration Release --output artifacts/packages
dotnet tool install --global --add-source ./artifacts/packages PackageMedic.Tool
package-medic --versionDuring development, use dotnet tool update instead of install when that package ID is already installed.
package-medic doctor
package-medic doctor ./src/MyProject/MyProject.csproj
package-medic doctor ./MySolution.sln
package-medic doctor ./MySolution.slnx
package-medic doctor ./src
package-medic audit ./MySolution.sln --include-transitive
package-medic doctor ./MySolution.sln --deprecated --include-transitive
package-medic diff origin/main ./MySolution.sln
package-medic simulate Example.Package --to 2.0.0 ./MySolution.sln
package-medic init
package-medic rules
package-medic explain PM007
package-medic explain PM008
package-medic clean . --dry-runThe path is optional and defaults to the current directory.
--no-restore
--format text|json|sarif
--output, -o <path>
--sarif-output <path>
--config <path>
--no-config
--baseline <path>
--fail-on none|warning|error
--fail-on-new none|warning|error
--audit
--deprecated
--include-transitive
--include-transitive-audit
--include-transitive-deprecated
--restore-timeout <seconds>
--evaluation-timeout <seconds>
--max-parallelism <1-32>
--verbosity quiet|normal|detailed
--version
--help
Examples:
# CI-oriented deterministic JSON; fail on warnings or errors
package-medic doctor . --format json --fail-on warning
# Use an assets graph produced by an earlier restore
package-medic doctor MySolution.sln --no-restore --verbosity detailed
# Report findings without failing the command
package-medic doctor --fail-on none
# Produce a repository-relative SARIF 2.1.0 report for CI
package-medic doctor . --format sarif --output artifacts/packagemedic.sarif
# Produce JSON and SARIF from one analysis
package-medic doctor . --format json --output artifacts/packagemedic.json --sarif-output artifacts/packagemedic.sarif
# Gate only diagnostics that were not accepted into the baseline
package-medic doctor . --baseline .packagemedic-baseline.json --fail-on none --fail-on-new warning
# Ask the active .NET SDK/NuGet tooling for known direct and transitive vulnerabilities
package-medic audit . --include-transitive --format json --fail-on error
# Add direct and transitive package deprecation evidence (PM008)
package-medic doctor . --deprecated --include-transitive --format json
# Keep vulnerability coverage transitive while checking deprecations directly only
package-medic audit . --include-transitive-audit --deprecated --format json
# Compare diagnostics, package versions, dependency kind, and CPM settings with a Git ref
package-medic diff origin/main . --format json --fail-on warning
# Explain dependency paths and enforce the repository Impact Gate
package-medic diff origin/main . --format json --output artifacts/impact.json
# Restore-validate one exact candidate in two independent snapshots of a clean HEAD
package-medic simulate Example.Package --to 2.0.0 MySolution.sln --format json
# Explicitly provide and redact only the private-feed variable needed by the simulation
package-medic simulate Contoso.Package --to 4.2.0 . --credential-env VSS_NUGET_EXTERNAL_FEED_ENDPOINTS
# Verify restore, build, and tests in immutable baseline/candidate snapshots
package-medic diff origin/main . --verify test --verification-configuration Release
# Verify one Time Machine candidate through build without editing the checkout
package-medic simulate Example.Package --to 2.0.0 . --verify build
# Export the observed NuGet inventory as deterministic CycloneDX 1.7 JSON
package-medic sbom . --output artifacts/packagemedic.cdx.json
# Bind a conclusive verified diff to unsigned in-toto analysis evidence
package-medic diff origin/main . --verify test --provenance-output artifacts/packagemedic.intoto.jsonRestore progress is written to standard error, so standard output remains valid JSON or SARIF. --output atomically writes the selected format, while --sarif-output can additionally write SARIF from that same in-memory analysis. PackageMedic creates destination directories and never mixes progress into report files.
--no-restore requires a usable obj/project.assets.json for every selected project. In diff mode that requirement applies independently to both the Git snapshot and the current checkout. With restore enabled, baseline and current receive separate owned NuGet package, HTTP, plugin, CLI-home, and temporary caches so a same-ID/version artifact cannot leak across the comparison. If either analysis is incomplete, PackageMedic returns exit code 2, exposes sanitized baseline/current errors, and deliberately reports no partial changes.
simulate <package-id> --to <exact-version> [path] requires a clean Git worktree and one unambiguous literal direct/central version declaration. It materializes two snapshots of the same HEAD commit, gives each an isolated NuGet/.NET cache and home, verifies the observed declaration by structure and SHA-256, changes only its version bytes in the candidate snapshot, and compares the independent restores with the same diagnostic and Impact Gate logic as diff.
Verdicts are pass, reject, noChange, and incomplete. A candidate absent from configured feeds or blocked by RestoreLockedMode is a complete rejection/exit 1; timeout, snapshot, audit, evaluation, or cleanup failures are incomplete/exit 2. Restore remains the default evidence level. --verify build and --verify test opt into the same ordered stage on both snapshots; a candidate-only deterministic failure can reject, while missing or contradictory evidence remains incomplete. Runtime compatibility and package safety are never claimed. See the complete Dependency Time Machine reference, verified-experiment architecture, and the independent JSON schema.
Run package-medic init to create .packagemedic.json. The CLI searches from the selected target to the repository root; --config selects an explicit file and --no-config disables discovery. Command-line values take precedence over configuration, which takes precedence over safe defaults.
{
"$schema": "https://raw.githubusercontent.com/GonzMeza/package-medic/main/schemas/packagemedic.schema.json",
"schemaVersion": 1,
"failOn": "none",
"failOnNew": "warning",
"baseline": ".packagemedic-baseline.json",
"maxParallelism": 4,
"exclude": ["**/bin/**", "**/obj/**"],
"rules": {
"PM006": { "enabled": true, "severity": "warning" }
},
"suppressions": [
{
"rule": "PM003",
"path": "src/Legacy/**",
"package": "Example.Legacy",
"reason": "Intentional exception tracked in issue 42"
}
],
"impact": {
"failOnDowngrade": true,
"failOnDirectToTransitive": true,
"maxAddedPackages": 40,
"maxAddedTransitivePackages": 25,
"failOnSourceChange": true,
"failOnContentChange": true,
"requirePackageSourceMapping": true,
"requireLockedMode": true,
"allowedSources": ["https://api.nuget.org/v3/index.json"]
},
"timeouts": { "restoreSeconds": 300, "evaluationSeconds": 60 }
}Every suppression requires a reason. Suppressed, excluded, and disabled findings are removed from failure thresholds but remain counted in report policy metadata; suppression reasons are preserved in suppressedDiagnostics and detailed text output.
diff builds a deterministic path from each changed transitive package back to the direct dependency that introduced it. The nested diff.impact report includes changed-package direction, direct/transitive additions, source and signature evidence when NuGet metadata is available, and the maximum blast radius: the largest number of changed transitives attributed to one direct root.
The impact policy can reject downgrades, loss of direct control, dependency-growth budgets, source changes, content-hash changes for the same package identity, packages outside an HTTPS source allowlist, missing Package Source Mapping for multi-feed projects, and missing locked restore. Its PMI001–PMI010 codes are Impact Gate violations, not suppressible PM diagnostics. They are returned in text and JSON and make a complete diff exit with code 1; operationally incomplete comparisons still return 2.
When those reproducibility gates are enabled, PackageMedic fails closed: Package Source Mapping must come from effective NuGet.Config files inside the analysis root, reference configured sources, and contain a usable pattern for every resolved package. A lock file must also remain inside that root, be bounded, and contain the NuGet lock-file schema (version and dependencies); an unrelated or external file does not satisfy requireLockedMode.
Package-source provenance depends on metadata written by NuGet during restore. Credential-bearing, query-qualified, fragment-qualified, or reparse-point-backed metadata is rejected rather than normalized into an approved source identity. If allowedSources is configured and a changed package's source cannot be established, the gate fails closed with PMI006 rather than treating unknown provenance as trusted. Use the literal local only when local packages are an intentional reviewed part of the build.
See the complete Dependency Impact Gate reference for every policy, violation code, report field, and GitHub Action output.
Create a deterministic baseline after reviewing the current findings:
package-medic baseline create . --output .packagemedic-baseline.json
package-medic doctor . --baseline .packagemedic-baseline.json --fail-on none --fail-on-new warningCurrent diagnostics are classified as new or existing; baseline entries no longer present are counted as resolved and listed in JSON resolvedDiagnostics. Refresh the accepted state explicitly with:
package-medic baseline update . --baseline .packagemedic-baseline.jsonBaselines use the same portable fingerprint as SARIF, contain no timestamps, and are stable across repository locations and source-line movement.
PackageMedic 0.6 maps PM001–PM008 to deterministic SARIF 2.1.0 with repository-relative locations, stable fingerprints, standard baseline states, rule help links, confidence, and original NuGet codes. SARIF can be consumed by GitHub Code Scanning or any compatible CI system.
The official GitHub Action installs the PackageMedic version associated with its tag, emits native file annotations, writes a job summary, preserves the CLI exit-code contract, and can upload the SARIF report after the scan.
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v6
- uses: GonzMeza/package-medic@v0.6.1
with:
mode: scan
path: .
config: .packagemedic.json
baseline: .packagemedic-baseline.json
fail-on: none
fail-on-new: warning
audit: 'true'
include-transitive-audit: 'true'
deprecated: 'true'
annotations: new
upload-sarif: 'true'Repositories without GitHub Code Scanning can disable upload-sarif; native annotations and the generated report remain available. See the complete Action reference for every input, output, permission, and security boundary.
For pull-request dependency diffs, use the unprivileged pull_request event, check out enough history, and leave the default mode: auto. It selects github.event.pull_request.base.sha without fetching or modifying Git. pull_request_target is deliberately rejected because its default checkout is the trusted base branch and would produce a misleading self-comparison. diff-base remains an explicit override. Diff mode is intentionally incompatible with baseline and fail-on-new, because the Git comparison itself defines what is new:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: GonzMeza/package-medic@v0.6.1
with:
audit: 'true'
deprecated: 'true'
fail-on: warning
verify: testThe Action restores both graphs by default. Setting restore: 'false' is safe only when usable assets files are tracked for every project in both revisions; otherwise the comparison is marked incomplete and exits with code 2. Verified diffs expose verification-status, build/test regression flags, candidate test counts, incompleteness, CycloneDX, and unsigned-evidence outputs. Build/test verification is allowed on GitHub-hosted runners; persistent self-hosted runners require explicit allow-self-hosted-verification: 'true' acknowledgement and independent hardening.
The PR summary shows the Impact Gate result, dependency-growth counts, source and content changes, maximum blast radius, and each failed policy with its causal package path. Later workflow steps can consume impact-gate-passed, impact-violations, impact-added-direct, impact-added-transitive, impact-max-blast-radius, impact-source-changes, and impact-content-changes.
| Code | Default severity | Meaning |
|---|---|---|
| PM001 | warning | An effective central PackageVersion is unused by affected projects. |
| PM002 | warning | A direct package has non-equivalent explicit versions in overlapping TFM scopes across non-CPM projects. |
| PM003 | warning | A PackageReference uses Version while CPM is enabled; intentional VersionOverride is respected. |
| PM004 | error | Multiple effective central entries define the same package for a project. |
| PM005 | NuGet level | Restore or project.assets.json contains an important NU warning/error such as NU1605, NU1107, or NU1109. |
| PM006 | warning | A PackageVersion, PackageReference Version, or VersionOverride uses a documented NuGet floating pattern. |
| PM007 | warning/error | NuGet reports a known vulnerability: low/moderate/unknown are warnings; high/critical are errors. |
| PM008 | warning/error | NuGet reports a deprecated package: critical bugs are errors; legacy/other/unknown reasons are warnings. |
Every diagnostic includes an explanation, evidence, affected project/scope, source location when available, a suggested action, and confidence where relevant. Use package-medic rules to list rules or package-medic explain PM008 for one rule. See the diagnostic reference and the SARIF contract.
JSON output is stable and camel-cased. Scan reports keep version, target, summary, diagnostics, and analysisErrors; their independent schemaVersion remains 1. diff uses nested schema version 3 for semantic graph, Impact Gate, and comparative verification evidence. simulate uses schema version 2 for its repository, request, mutation, executed verification, comparison, verdict, rejection, and operational-error boundaries. SARIF remains focused on observed PM001–PM008 findings; hypothetical simulations never emit SARIF. CycloneDX and in-toto evidence are separate output contracts. All formats are deterministically ordered and contain no timestamps or temporary paths.
| Exit code | Meaning |
|---|---|
0 |
Analysis passed, or simulation completed with pass/noChange. |
1 |
A configured gate failed, or simulation completed with a rejected candidate. |
2 |
Usage or an operational failure prevented a complete result. |
--fail-on none disables the diagnostic threshold, not the independent dependency Impact Gate. Operational failures still return 2.
- No telemetry is collected.
- PackageMedic does not implement its own advisory HTTP client.
--auditand--deprecateddelegate to separate officialdotnet list packagecommands, which can contact configured NuGet sources. dotnet restorecan contact feeds from the user's NuGet configuration unless--no-restoreis used.- PackageMedic's own CI uses committed NuGet lockfiles, locked restore, and a direct/transitive PM007 self-audit before packaging.
- Common credential-shaped values in subprocess output are redacted without corrupting JSON, and unsafe terminal controls are removed before display.
- Subprocess output is bounded, restore/evaluation timeouts are configurable, and cancellation terminates the process tree.
- Repository-controlled configuration, baseline, solution, assets, XML source, and Action report inputs have explicit size/count limits to prevent memory exhaustion in CI.
- Recursive discovery uses one filesystem pass, reports inaccessible directories as operational errors, rejects missing solution projects, and cannot escape the analysis root through symbolic links or junctions.
- Restore, audit, and MSBuild evaluation use deterministic bounded worker queues, so process and pending-task counts stay controlled. Use
--max-parallelismormaxParallelismin.packagemedic.jsonto tune large repositories. - Assets files are parsed directly from file streams and JSON/SARIF report destinations are streamed to reduce peak memory; package and policy lookups are indexed rather than repeatedly rescanned.
- Package-source provenance is accepted only from bounded regular NuGet restore metadata; reparse-point-backed, credential-bearing, query-qualified, and fragment-qualified values are not exposed as trusted evidence.
- If a persistent package loses previously observed source or content-hash evidence,
diffrecords that loss as a source/content change and the default Impact Gate requires review. diffresolves and archives a commit without checkout, branch switching, index changes, or worktree changes; gives both analyses independent package/network/plugin caches; rejects tracked or repository-local archive-transforming attributes; preserves executable mode on Unix; enforces archive/entry/expanded-size limits; and cleans up marker-owned temporary state without following links.simulateuses two separately restored snapshots of one cleanHEAD, validates a SHA-256/XML precondition, preserves declaration bytes outside the version value, and rechecks the original worktree after no-follow cleanup.- Every simulation snapshot has independent NuGet/HTTP/plugin caches, CLI home, user-home aliases, app-data, and temporary directories. No private-feed variable is inherited unless named with
--credential-env, whose value is then treated as a redaction secret. - Snapshot isolation protects checkout integrity but is not an OS sandbox: restore/MSBuild still runs with the caller's host permissions and can execute repository-controlled logic or contact configured sources.
clean --dry-runonly lists PM001 candidates. Version 0.6 offers no apply/fix command.
- SDK-style C# projects and
PackageReferenceonly;packages.configis not supported. - The installed SDK must support MSBuild's evaluated
-getProperty/-getItemJSON output and the target project's SDK. - PackageMedic favors avoiding false positives: dynamically generated or unsafe-to-evaluate conditions may result in no diagnostic.
- PM001 reasons over effective evaluated central items, direct references, and the existing resolved graph; it does not speculate about packages used only by source code reflection or custom build logic.
- PM006 ignores unresolved MSBuild expressions and only recognizes documented floating-version forms; it is not an update recommender.
- Normal scan restore failures return exit
2. A candidate restore rejection inside an otherwise valid simulation is evidence and returns1; timeout or infrastructure failure remains2. - Vulnerability results depend on the active SDK/NuGet audit sources and can return operational error
2when those sources are unavailable. - Deprecation results likewise depend on configured NuGet sources and are only requested with
--deprecated. diff --no-restorecannot reconstruct missing assets from Git; both selected revisions must already contain usable tracked assets files.simulateaccepts one exact direct/central package version and a clean committed tree. It deliberately rejects dynamic, conditional, ambiguous, transitive-only, external, submodule/LFS-missing, untracked/generated, or Git export-transformed dependency inputs.- A default passing simulation proves only observed restore and dependency-graph policy. Requested build/test stages add comparative evidence but still do not prove API, binary, runtime compatibility, or package safety.
- No automatic fixes, IDE extension, or desktop UI is included yet.
src/PackageMedic.Cli/ command parsing and terminal/JSON orchestration
src/PackageMedic.Core/ discovery, MSBuild evaluation, assets reader, rules
tests/PackageMedic.Core.Tests/ rule and serialization tests
tests/PackageMedic.IntegrationTests/ CLI/fixture and exit-code tests
fixtures/ real SDK-style analysis scenarios
docs/diagnostics/ diagnostic reference
schemas/packagemedic-simulation.schema.json Dependency Time Machine report contract
schemas/packagemedic-analysis-attestation.schema.json unsigned analysis-evidence contract
See architecture.md for the execution boundary and design choices.
Candidates after the verified-experiments 0.6 release is proven:
- 0.7: dependency bisect and a read-only VS Code extension over the stable report contracts
- 0.8: explicit review-first remediation plans and a separately gated apply workflow
- 0.9 only if a dedicated stabilization cycle is required before 1.0
Package publishing, feed administration, and replacing NuGet remain out of scope.
Issues and pull requests are welcome. Read CONTRIBUTING.md, the Code of Conduct, and SECURITY.md before contributing.
Release history is documented in CHANGELOG.md.
MIT © 2026 GonzMeza.
