Skip to content

Commit

Permalink
Merge pull request #17 from philips-labs/discover
Browse files Browse the repository at this point in the history
  • Loading branch information
Brend-Smits authored Mar 24, 2022
2 parents 7f7dea1 + 1fb2510 commit c487012
Show file tree
Hide file tree
Showing 18 changed files with 3,726 additions and 1,641 deletions.
84 changes: 37 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,39 @@ Fatt is a small commandline utility that allows you to fetch attestations for yo

> :warning: This project is currently nothing more than a POC.
`fatt` tries to find any [purl][] in your project by looking at predefined fields in the [supported packages](#supported-packages-and-attestations). These fields describe using a [purl][] where to grab the attestation from.
`fatt` tries to find any [purl][] in your project by searching the given path recursively for `attestations.txt`. Within an `attestations.txt` you can describe where your project stores attestations using [purl][] format.

In addition `fatt` allows to fetch these attestations from an OCI registry. It assumes that given location contains an uploaded blob using cosign containing the contents of `attestations.txt`.

## Fatt Usage

```bash
$ ./bin/fatt --help
Discover and resolve your attestations
$ ./bin/fatt list --help
Lists all attestations

Usage:
fatt [command]

Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
list Lists all attestations
version Prints the fatt version
fatt list <discovery-path> [flags]

Flags:
-p, --file-path string the filepath to find attestation purls (defaults to current working dir)
-h, --help help for fatt
-r, --resolver string the resolver to use for finding attestations (default "multi")

Use "fatt [command] --help" for more information about a command.
-f, --filter string filter attestations using template expressions
-h, --help help for list
--key string path to the public key file, URL, or KMS URI
-o, --output-format string output format for the list (default "purl")
```

### List command: Filter Option
### List filter options

The following attestations fields can be filtered on.
Filters use the Go template language.

The following fields are supported.

* Type
* Namespace
* Version
* Name
* Scheme
* PURL.Type
* PURL.Namespace
* PURL.Name
* PURL.Version
* PURL.Subpath
* PURL.Qualifiers

The following functions are available.

Expand All @@ -50,44 +49,35 @@ pkg:docker/philips-labs/fatt@sha256:6cc65b2c82c2baa3391890abb8ab741efbcbc87baff3
pkg:docker/philips-labs/fatt@sha256:6cc65b2c82c2baa3391890abb8ab741efbcbc87baff3b06d5797afacb314ddd9?repository_url=ghcr.io&attestation_type=sbom
```

## Supported packages and attestations

### NPM
## OCI

#### SBOM
Any attestations published to an OCI registry should be captured in a `attestations.txt`. To distribute this discovery filewe can publish this as well to an OCI registry.

To fetch an SBOM you can define a [purl][] with `attestation_type`=`sbom` qualifier in `package.json` within a attestations array.
We can do this utilizing cosign. (we might add integration later to reduce the manual steps).

<details>
<summary>Example cosign stored sbom</summary>
<summary>Store attestations.txt using cosign.</summary>

Using cosign we can leverage any [OCI registry][] to store our attestations.
Using cosign we can leverage any [OCI registry][] to store our attestations. Once we stored the attestations we can capture that in an `attestations.txt` using [purl][] format. This `attestations.txt` we can also store in the [OCI registry][].

```shell
$ cosign upload blob -f sbom.spdx.json ghcr.io/philips-labs/fatt:example-sbom-attestation
Uploading file from [sbom.spdx.json] to [ghcr.io/philips-labs/fatt:example-sbom-attestation] with media type [text/plain]
File [sbom.spdx.json] is available directly at [ghcr.io/philips-labs/fatt@sha256:6cc65b2c82c2baa3391890abb8ab741efbcbc87baff3b06d5797afacb314ddd9]
$ cosign upload blob -f attestations.txt ghcr.io/philips-labs/fatt:attestations
Uploading file from [attestations.txt] to [ghcr.io/philips-labs/fatt:attestations] with media type [text/plain]
File [attestations.txt] is available directly at [ghcr.io/philips-labs/fatt@sha256:6cc65b2c82c2baa3391890abb8ab741efbcbc87baff3b06d5797afacb314ddd9]
$ cosign sign --key cosign.key ghcr.io/philips-labs/fatt:attestations
```

Now we can use a purl to link this attestation to our Node package.
Using `fatt` we can now list the attestations stored in the OCI registry. `fatt` utilizes `sget` to fetch the `attestations.txt` and verify the signature. As we captured our attestations in PURL format we can also translate the attestations to docker format so we can also utilize sget to fetch the attestations themself.

```json
{
"name": "@philips-labs/awesome-npm",
"attestations": [
"pkg:docker/philips-labs/fatt@sha256:6cc65b2c82c2baa3391890abb8ab741efbcbc87baff3b06d5797afacb314ddd9?repository_url=ghcr.io&attestation_type=sbom",
]
}
```
```shell
$ attestations="$(bin/fatt list --key cosign.pub -o docker ghcr.io/philips-labs/fatt:attestations)"
Fetching attestations from ghcr.io/philips-labs/fatt:attestations…
Using `fatt` we can now scan our project for attestations and fetch them using sget.
Verification for ghcr.io/philips-internal/attestations/slsa-workflow-examples/awesome-node-cli --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
```shell
$ attestations="$(bin/fatt list -p examples/awesome-npm -o docker)"
Fetching attestations for current working directory…
Found attestations: [{PURL:{Type:docker Namespace:philips-labs Name:fatt Version:sha256:6cc65b2c82c2baa3391890abb8ab741efbcbc87baff3b06d5797afacb314ddd9 Qualifiers:repository_url=ghcr.io&attestation_type=sbom Subpath:} Type:SBOM} {PURL:{Type:docker Namespace:philips-labs Name:fatt Version:sha256:6cc65b2c82c2baa3391890abb8ab741efbcbc87baff3b06d5797afacb314ddd9 Qualifiers:repository_url=ghcr.io&attestation_type=provenance Subpath:} Type:SBOM}]
Attestation type: sbom
Attestation type: provenance
$ while read -r a ; do sget "$a" ; done <<< "$attestations"
{
"SPDXID": "SPDXRef-DOCUMENT",
Expand Down
2 changes: 1 addition & 1 deletion cmd/fatt/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const (
)

var (
ro = &options.RootOptions{}
ro = options.NewRootOptions()
)

// New create a new instance of the fatt commandline interface
Expand Down
40 changes: 28 additions & 12 deletions cmd/fatt/cli/list.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
"fmt"
"os"

Expand All @@ -10,32 +11,47 @@ import (
"github.com/philips-labs/fatt/pkg/attestation"
)

var (
lo = &options.ListOptions{}
)

// NewListCommand creates a new instance of a list command
func NewListCommand() *cobra.Command {
lo := options.NewListOptions()

cmd := &cobra.Command{
Use: "list",
Use: "list <discovery-path>",
Short: "Lists all attestations",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Fprintln(os.Stderr, "Fetching attestations for current working directory…")

if ro.FilePath == "" {
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
d, err := os.Getwd()
if err != nil {
return err
}
ro.FilePath = d
lo.FilePath = d
} else {
lo.FilePath = args[0]
}

return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, "Fetching attestations from %s…\n", lo.FilePath)
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()

d, err := lo.GetDiscoverer(ctx)
if err != nil {
return err
}

attReader, err := d.Discover(lo.FilePath)
if err != nil {
return err
}

r, err := ro.GetResolver()
r, err := lo.GetResolver()
if err != nil {
return err
}

atts, err := r.Resolve(ro.FilePath)
atts, err := r.Resolve(attReader)
if err != nil {
return fmt.Errorf("failed to resolve attestations: %w", err)
}
Expand Down
30 changes: 28 additions & 2 deletions cmd/fatt/cli/options/list.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
package options

import (
"context"
"io"
"os"

"github.com/spf13/cobra"

"github.com/philips-labs/fatt/pkg/attestation"
"github.com/philips-labs/fatt/pkg/oci"
"github.com/philips-labs/fatt/pkg/attestation/discoverers/fs"
"github.com/philips-labs/fatt/pkg/attestation/discoverers/oci"
"github.com/philips-labs/fatt/pkg/attestation/resolvers/txt"
poci "github.com/philips-labs/fatt/pkg/oci"
)

// ListOptions commandline options for the list command
type ListOptions struct {
*OCIOptions
FilePath string
OutputFormat string
Filter string
}

// NewListOptions initializes the ListOptions object
func NewListOptions() *ListOptions {
return &ListOptions{OCIOptions: &OCIOptions{}}
}

var _ CommandFlagger = (*ListOptions)(nil)

// AddFlags implements CommandFlagger to add the RootOptions as flags to the given command
func (o *ListOptions) AddFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(&o.OutputFormat, "output-format", "o", "purl", "output format for the list")
cmd.PersistentFlags().StringVarP(&o.Filter, "filter", "f", "", "filter attestations using template expressions")
o.OCIOptions.AddFlags(cmd)
}

// GetPrinter returns the printer based on the OutputFormat flag
Expand All @@ -29,10 +42,23 @@ func (o *ListOptions) GetPrinter(w io.Writer) (attestation.Printer, error) {

switch o.OutputFormat {
case "docker":
p = oci.NewDockerPrinter(w)
p = poci.NewDockerPrinter(w)
default:
p = attestation.NewDefaultPrinter(w)
}

return p, nil
}

// GetResolver returns the resolver based on the resolver commmandline options
func (o *ListOptions) GetResolver() (attestation.Resolver, error) {
return &txt.Resolver{}, nil
}

// GetDiscoverer discovers attestation.txt files from given location
func (o *ListOptions) GetDiscoverer(ctx context.Context) (attestation.Discoverer, error) {
if _, err := os.Stat(o.FilePath); err == nil {
return &fs.Discoverer{}, nil
}
return oci.NewDiscoverer(o.KeyRef, oci.WithContext(ctx)), nil
}
17 changes: 17 additions & 0 deletions cmd/fatt/cli/options/oci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package options

import (
"github.com/spf13/cobra"
)

// OCIOptions commandline options to fetch from oci registry
type OCIOptions struct {
KeyRef string
}

var _ CommandFlagger = (*OCIOptions)(nil)

// AddFlags implements CommandFlagger to add the RootOptions as flags to the given command
func (o *OCIOptions) AddFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(&o.KeyRef, "key", "", "", "path to the public key file, URL, or KMS URI")
}
36 changes: 5 additions & 31 deletions cmd/fatt/cli/options/root.go
Original file line number Diff line number Diff line change
@@ -1,47 +1,21 @@
package options

import (
"fmt"
"strings"

"github.com/spf13/cobra"

"github.com/philips-labs/fatt/pkg/attestation"
"github.com/philips-labs/fatt/pkg/attestation/resolvers/packagejson"
"github.com/philips-labs/fatt/pkg/attestation/resolvers/txt"
)

// RootOptions commandline options for the root command
type RootOptions struct {
FilePath string
Resolver string
}

// NewRootOptions initializes the RootOptions object
func NewRootOptions() *RootOptions {
return &RootOptions{}
}

var _ CommandFlagger = (*RootOptions)(nil)

// AddFlags implements CommandFlagger to add the RootOptions as flags to the given command
func (o *RootOptions) AddFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(&o.FilePath, "file-path", "p", "", "the filepath to find attestation purls (defaults to current working dir)")
cmd.PersistentFlags().StringVarP(&o.Resolver, "resolver", "r", "multi", "the resolver to use for finding attestations")
}

// GetResolver returns the resolver based on the resolver commmandline options
func (o *RootOptions) GetResolver() (attestation.Resolver, error) {
var r attestation.Resolver

switch strings.ToLower(o.Resolver) {
case "txt":
r = &txt.Resolver{}
case "packagejson":
r = &packagejson.Resolver{}
case "multi":
r = attestation.NewMultiResolver(
&txt.Resolver{},
&packagejson.Resolver{},
)
default:
return nil, fmt.Errorf("unsupported resolver, supported resolvers are `txt`, `packagejson`, `multi`")
}

return r, nil
}
12 changes: 0 additions & 12 deletions examples/awesome-npm/package.json

This file was deleted.

Loading

0 comments on commit c487012

Please sign in to comment.