Skip to content

dennisdoomen/packageguard

Repository files navigation

PackageGuard

Get a grip on your open-source packages

About

What's this?

PackageGuard is a fully open-source tool to scan the NuGet, NPM, PNPM and Yarn dependencies of your codebase against a deny- or allowlist so to control the open-source licenses that you want to allow or certain versions of certain packages you want to enforce or avoid.

What's so special about that?

I've noticed that the commercial solutions for this are usually very expensive and have functionality that smaller companies may not need. Hopefully this little tools fills the gap between tools like GitHub's Dependabot and expensive commercial products like Blackduck, SNYK and others.

Who created this?

My name is Dennis Doomen and I'm a Microsoft MVP and Principal Consultant at Aviva Solutions with 28 years of experience under my belt. As a software architect and/or lead developer, I specialize in designing full-stack enterprise solutions based on .NET as well as providing coaching on all aspects of designing, building, deploying and maintaining software systems. I'm the author of several open-source projects such as Fluent Assertions, Reflectify, Liquid Projections, and I've been maintaining coding guidelines for C# since 2001.

Contact me through Email, Bluesky, Twitter/X or Mastadon

Download

The tool is available in two forms:

As a .NET Global Tool (recommended)

Install as a NuGet Tool package:

dotnet tool install PackageGuard --global

Then use packageguard --help to see a list of options.

As a portable deployment

Download the latest PackageGuard-{version}.zip from the releases page, extract it, and run it using dotnet PackageGuard.dll. This requires .NET 9.0 or later to be installed but works on Windows, Linux, and macOS.

USAGE:
    PackageGuard [path] [OPTIONS]            # When installed as a global tool
    dotnet PackageGuard.dll [path] [OPTIONS] # When using the portable deployment

ARGUMENTS:
    [path]    The path to a directory containing a .sln/.slnx file and/or a package.json, a specific .sln/.slnx file, a
              specific .csproj file, or a specific package.json. Defaults to the current working directory

OPTIONS:
                                 DEFAULT
    -h, --help                              Prints help information
    -c, --config-path                       The path to the configuration file. Defaults to hierarchical discovery of
                                            packageguard.config.json or .packageguard/config.json files starting from
                                            the solution directory
    -i, --restore-interactive    True       Allow enabling or disabling an interactive mode of "dotnet restore".
                                            Defaults to true
        --ignore-violations                 Don't fail the analysis if any violations are found. Defaults to false
    -f, --force-restore                     Force restoring the NuGet dependencies, even if the lockfile is up-to-date
    -s, --skip-restore                      Prevent the restore operation from running, even if the lock file is missing
                                            or out-of-date
    -a, --github-api-key                    GitHub API key to use for fetching package licenses. If not specified, you
                                            may run into GitHub's rate limiting issues
        --use-caching                       Maintains a cache of the package information to speed up future analysis
        --cache-file-path        False      Overrides the file path where analysis data is cached. Defaults to the
                                            "<workingdirectory>/.packageguard/cache.bin"
        --nuget                  True       Explicitly enable or disable scanning for .csproj, .sln or .slnx files
        --npm                               Explicitly specify the package manager to use (npm, yarn, pnpm). If not
                                            specified, it will detect it automatically
        --npm-exe-path                      The path to the npm, yarn or pnpm executable. If not specified, the system
                                            PATH is used

How do I configure it?

PackageGuard supports hierarchical configuration files that are automatically discovered based on your .NET solution and project structure. This allows you to define repository-wide policies at the solution level and add project-specific rules as needed. Since PackageGuard will scan a single package.json per run, it will use the configuration that is associated with that directy.

Hierarchical Configuration Discovery

PackageGuard will automatically look for configuration files in the following order:

  1. Solution level: packageguard.config.json in the same folder as your .sln, .slnx or package.json file
  2. Solution level: config.json in a .packageguard subdirectory of your solution or package.json folder
  3. Project level: packageguard.config.json in individual project directories
  4. Project level: config.json in a .packageguard subdirectory of project directories

Settings from multiple configuration files are merged together, with project-level settings taking precedence over solution-level settings for boolean values, while arrays (packages, licenses, feeds) are combined.

Manual Configuration Path

You can still specify a custom configuration file path using the --configpath CLI parameter to override the hierarchical discovery:

packageguard --configpath path/to/my-config.json

Configuration Format

Each configuration file should follow this JSON format:

{
    "settings": {
        "allow": {
          "prerelease": false,
          "licenses": [
              "Apache-2.0", // Uses SPDX naming
              "MIT",
          ],
          "packages": [
              "MyPackage/[7.0.0,8.0.0)"
          ],
          "feeds": [
            "*dev.azure.com*"
          ]
        },
        "deny": {
          "licenses": [],
          "packages": [
            "ProhibitedPackage"
          ]
        },
        "ignoredFeeds": [
          "https://pkgs.dev.azure.com/somecompany/project/_packaging/myfeed/nuget/v3/index.json"
        ]
    }
}

In this example, only NuGet and NPM packages with the MIT or Apache 2.0 licenses are allowed, the use of the package ProhibitedPackage and any pre-release packages (e.g. 0.1.2 or 1.0.2-beta.2) are prohibited, and MyPackage should stick to version 7 only. Both the allow and deny sections support the licenses and packages properties. But licenses and packages listed under allow have precedence over those under the deny section.

Important

Deny rules always take precedence over allow rules. If a package is denied by the deny section, it will be blocked regardless of what the allow section specifies.

Example: Multi-level Configuration

Solution-level configuration (MySolution/packageguard.config.json):

{
    "settings": {
        "allow": {
            "licenses": ["MIT", "Apache-2.0"],
            "packages": ["Microsoft.*", "System.*"]
        },
        "deny": {
            "packages": ["UnsafePackage"]
        }
    }
}

Project-level configuration (MySolution/WebProject/packageguard.config.json):

{
    "settings": {
        "allow": {
            "licenses": ["BSD-3-Clause"],
            "packages": ["WebSpecificPackage/[1.0.0,2.0.0)"]
        }
    }
}

The effective configuration for WebProject will allow:

  • Licenses: MIT, Apache-2.0, BSD-3-Clause (merged)
  • Packages: Microsoft., System., WebSpecificPackage/[1.0.0,2.0.0) (merged)
  • Denied packages: UnsafePackage (inherited)

Identifying packages and license

License names are case-insensitive and follow the SPDX identifier naming conventions, but we have special support for certain proprietary Microsoft licenses such as used by the Microsoft.AspNet.WebApi* packages. For those, we support using the license name Microsoft .NET Library License.

Package names can include just the NuGet or NPM ID but may also include a NuGet-compatible version (range) separated by /. Here's a summary of the possible notations:

Notation Valid versions
"Package/1.0" 1.0
"Package/[1.0,)" v ≥ 1.0
"Package/(1.0,)" v > 1.0
"Package/[1.0]" v == 1.0
"Package/(,1.0]" v ≤ 1.0
"Package/(,1.0)" v < 1.0
"Package/[1.0,2.0]" 1.0 ≤ v ≤ 2.0
"Package/(1.0,2.0)" 1.0 < v < 2.0
"Package/[1.0,2.0)" 1.0 ≤ v < 2.0

About feeds

PackageGuard follows the same logic for getting the applicable NuGet or NPM feeds as dotnet, NPM package managers or your IDE does. That also means that it will use the configured credential providers to access authenticated and private feeds.

You can tell PackageGuard to allow all packages from a particular feed, even if a package on that feed doesn't meet the licenses or packages listed under allow. Just add the element feeds under the allow element and specify a wildcard pattern that matches the name or URL of the feed.

{
    "settings": {
        "allow": {
            "feeds": ["*dev.azure.com*"]
        }
    }
}

And in case you want to prevent PackageGuard from trying to access a particular feed altogether, add them to the ignoredFeeds element. Notice that PackageGuard may still trigger a dotnet restore call if the package lock file (project.assets.json) doesn't exist yet, unless you use the SkipRestore option, that will use all available NuGet feeds.

How do I use it?

With this configuration in place, simply invoke PackageGuard like this

packageguard --configpath <path-to-config-file> <path-to-solution-file-or-project>

If you pass a directory, PackageGuard will try to find the .sln, .slnx or package.json files there. But you can also specify a specific .csproj or package.json to scan.

If everything was configured correctly, you'll get something like:

The exit code indicates either 0 for success or 1 for failure.

Additional notes

Speeding up the analysis using caching

One of the most expensive operations that PackageGuard needs to do is to download find the license information from GitHub or other sources. You can significantly speed-up the analysis process by using the --use-caching flag.

By default, this will cause PackageGuard to persist the license information it retrieved to a binary file under .packageguard\cache.bin. You can commit this file to source control so successive runs can reuse the license information it collected during a previous run.

If PackageGuard finds new packages in your project or solution that did not exist during the previous run, then it will update the cache after the analysis is completed.

Github rate limiting issues

If you're running into errors from GitHub like

Response status code does not indicate success: 403 (rate limit exceeded).

it means PackageGuard has ran into the rate limits of api.github.com while trying to fetch license information from certain repositories. You can solve that by either waiting an hour or creating a GitHub Personal Access Token with the public_repo scope. You can find more information about those tokens here.

After having generated such a token, pass it to PackageGuard through its github-api-key option or set-up an environment variable named GITHUB_API_KEY.

Roadmap

This is a rough list of items from my personal backlog that I'll be working on the coming weeks.

Minor features

  • Allow specifying the location of dotnet.exe
  • Allow ignoring certain .csproj files or folders using Globs or wildcards (e.g. build.csproj)
  • Allow marking all violations as a warning
  • Allow marking individual violations as a warning
  • Expose the internal engine through the PackageGuard.Core NuGet package
  • Add direct support for Nuke
  • Display the reason why a package was marked as a violation

Building

To build this repository locally, you need the following:

You can also build, run the unit tests and package the code using the following command-line:

build.ps1

Or, if you have, the Nuke tool installed:

nuke

Also try using --help to see all the available options or --plan to see what the scripts does.

Contributing

Your contributions are always welcome! Please have a look at the contribution guidelines first.

Previous contributors include:

contrib.rocks image

(Made with contrib.rocks)

Versioning

This library uses Semantic Versioning to give meaning to the version numbers. For the versions available, see the tags on this repository.

Credits

This library wouldn't have been possible without the following tools, packages and companies:

Support the project

You may also like

  • My Blog
  • Reflectify - Reflection extensions without causing dependency pains
  • .NET Library Starter Kit - A battle-tested starter kit for building open-source and internal NuGet libraries using "dotnet new", born from half a billion downloads
  • C# Coding Guidelines - Forkable coding guidelines for all C# versions

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Dependency scanner to ensure only certain NuGet and NPM packages are used

Resources

License

Contributing

Stars

Watchers

Forks

Sponsor this project

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •