go-supply-guard

command module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 10, 2026 License: GPL-3.0 Imports: 23 Imported by: 0

README

Go Supply Guard

A gopher guarding a stack of software packages using a shield.

Have you ever run

go get -u

then checked that all tests still succeed and moved on? If so, you are vulnerable to a supply chain attack.

What are supply chain attacks?

Supply chain attacks happen by either tricking you into importing a package that the attacker crafted. This package then contains code that

  • exfiltrates data from your program (passwords, tokens, etc.),
  • abuses your ressources (running a bitcoin miner, DDOS attacks, etc.) or
  • destroys your infrastructure.

An attacker can trick you into importing their package in a couple of different ways:

  • Putting up a package with a very similar name, hoping that you misspell it when importing it,
  • hijacking an existing package and pushing a new, compromised version or
  • poisioning the package cache you are using to inject a compromised package.

What makes them so hard to detect?

For most programs or libraries of a non-trivial size, it is simply an impossible task to review all changes that come into your codebase through a go get -u. Even if you only use well-known, trusted dependencies, there's still no guarantee that they won't be exploited^1.

How Go Supply Guard works

The supply guard scans your code-base and all its dependencies and reports back all "capabilities" that each package has. A capability is defined as calling a specific go function. For example, under the built-in "stdlib" rules, if a package uses os/exec to execute some external program, the supply guard will report the capability "exec" for that package.

Unless every capability of the code-base has been declared as expected, the supply guard will report an error. This makes it much more difficult for dependencies to smuggle exploits into your code-base.

Installation

We recommend using the go's tool dependencies to install go-supply-guard. Simply run

go get -tool github.com/Kernalytics/go-supply-guard

to add it to your project. You can then run it using

go tool go-supply-guard

Alternatively, you can also install it globally using

go install github.com/Kernalytics/go-supply-guard

Usage

When you first run the supply guard, you should check if there are already built-in rules for the packages in your supply chain:

go tool go-supply-guard suggest ./...

[!TIP] You can leave out the ./..., it is the default.

If you are happy with the suggested built-in rules, you can proceed to inspect your supply chain:

go tool go-supply-guard -built-in <BUILT-IN1> -built-in <BUILT-IN2> show ./...

[!IMPORTANT] You normally want to include at least the stdlib built-in rules, since they establish capabilities of the standard library which is available to every package.

This will render a dependency graph of your imports with all capabilities of your imported packages. Any capabilities shown in green are expected and won't be reported when checking for new capabilities. Capabilities in yellow are unexpected. Those are capabilities that we'll have to write rules for. Every unexpected capability is listed together with the source-location that caused it.

You can then investigate each dependency and decide whether the listed capabilities make sense or if they are suspicious ("Why does my UUID package need an HTTP client?").

If you think the current capabilities are fine, you can auto-generate rules for them:

go tool go-supply-guard -built-in <BUILT-IN> init ./... > supply-rules.json

With the rules in place, you can now integrate the following command into your workflow:

go tool go-supply-guard -built-in <BUILT-IN> -rules supply-rules.json check ./...

If any new capabilities are detected in any dependent package, this command will report them and return an error code. You can either run this command manually whenever you update your dependencies, or as part of your CI/CD pipeline.

Rules

Go supply guard is based on rules. Rules specify what capabilities a package provides and which capabilities we expect it to have. Every rule-file is a JSON file with a list of rules. Each rule can have one of the following formats:

Defining expected capabilities

Use a rule like

{
  "package": "example.com/A",
  "expected": ["io", "syscall"],
  "encapsulates": ["reflect"]
}

to specify that package example.com/A is expected to use the capabilities io and syscall. Furthermore the package and all its dependencies are allowed to have the reflect capability.

Defining capabilities

You can define capabilities on global functions, member functions and global variables:

Global functions
{
  "package": "example.com/hosts",
  "type": false,
  "func": "GetAll",
  "has": ["hosts.read"]
}

This rule assigns the capability hosts.read to the function GetAll in the package example.com/hosts.

Member functions
{
  "package": "example.com/file",
  "type": "File",
  "func": "Write",
  "has": ["fs.write"]
}

Assigns the capability fs.write to the function Write of the type File in the package example.com/file.

Global variables
{
  "package": "example.com/cfg",
  "global": "Config",
  "has": ["cfg"]
}

Every package that uses the Config global in the example.com/cfg package now gets the cfg capability.

Matcher

Every attribute that takes a string (package, type, func, global) can also take a list of strings, in which case it matches any of the elements in that list:

{
  "package": "example.com/ping",
  "func": ["Ping", "PingAll"],
  "has": ["ping"]
}

Now we can match either function Ping or PingAll.

It is also possible to use a regex:

{
  "package": {
    "pattern": "^example\\.com/",
    "has": ["example"]
  }
}

This will assign the example capability to every package from the example.com domain.

Limitations

When a library is using interfaces, the capabilities of that library are not static, but will depend on its usage. For example, if a library has a function that uses a Writer, such as:

func WriteContent(w io.Writer) error {
   //...
   if _, err := w.Write(x); err!=nil {
     return err
   }
   //...
}

If you pass an os.File object into that function, the library, instead of the caller will have the fs.write capability. This is a limitation of the rule evaluation strategy that we're currently working on improving.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
e1
e2

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL