Skip to content

Commit

Permalink
Refactor github release provenance
Browse files Browse the repository at this point in the history
This adds unittests and strategy pattern by using intoto.Provenancer interface on environments
  • Loading branch information
marcofranssen committed Oct 28, 2021
1 parent db61ea0 commit e3fd8a4
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 63 deletions.
79 changes: 25 additions & 54 deletions cmd/slsa-provenance/cli/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import (
"fmt"
"io"
"os"
"path"
"strings"

"github.com/peterbourgon/ff/v3/ffcli"
"github.com/pkg/errors"

"github.com/philips-labs/slsa-provenance-action/lib/github"
"github.com/philips-labs/slsa-provenance-action/lib/intoto"
)

// RequiredFlagError creates a required flag error for the given flag name
Expand Down Expand Up @@ -68,56 +67,29 @@ func Generate(w io.Writer) *ffcli.Command {
return errors.Wrap(err, "failed to unmarshal runner context json")
}

environment := github.Environment{
Context: &gh,
Runner: &runner,
ghToken := os.Getenv("GITHUB_TOKEN")
if ghToken == "" {
return errors.New("GITHUB_TOKEN environment variable not set")
}

if *tagName != "" {
ghToken := os.Getenv("GITHUB_TOKEN")
if ghToken == "" {
return fmt.Errorf("GITHUB_TOKEN environment variable not set")
}
tc := github.NewOAuth2Client(ctx, func() string { return ghToken })
pc := github.NewProvenanceClient(tc)

repoParts := strings.Split(gh.Repository, "/")
repo := repoParts[len(repoParts)-1]
rel, err := pc.FetchRelease(ctx, gh.RepositoryOwner, repo, *tagName)
if err != nil {
return err
}
assets, err := pc.DownloadReleaseAssets(ctx, gh.RepositoryOwner, repo, rel.GetID())
if err != nil {
return err
}
err = os.MkdirAll(*artifactPath, os.FileMode(os.O_RDWR))
if err != nil {
return err
}

for _, asset := range assets {
err := saveFile(path.Join(*artifactPath, asset.GetName()), asset.Content)
defer asset.Content.Close()
if err != nil {
return err
}
}

defer func() {
provenanceFile, err := os.Open(*outputPath)
if err != nil {
fmt.Printf("%s", err)
}
pc.AddProvenanceToRelease(ctx, gh.RepositoryOwner, repo, rel.GetID(), provenanceFile)
}()
}

stmt, err := environment.GenerateProvenanceStatement(ctx, *artifactPath)
tc := github.NewOAuth2Client(ctx, func() string { return ghToken })
pc := github.NewProvenanceClient(tc)
env := createEnvironment(gh, runner, *tagName, pc)
stmt, err := env.GenerateProvenanceStatement(ctx, *artifactPath)
if err != nil {
return errors.Wrap(err, "failed to generate provenance")
}

if *tagName != "" {
// defer func() {
// provenanceFile, err := os.Open(*outputPath)
// if err != nil {
// fmt.Printf("%s", err)
// }
// pc.AddProvenanceToRelease(ctx, gh.RepositoryOwner, repo, rel.GetID(), provenanceFile)
// }()
}

// NOTE: At L1, writing the in-toto Statement type is sufficient but, at
// higher SLSA levels, the Statement must be encoded and wrapped in an
// Envelope to support attaching signatures.
Expand All @@ -133,14 +105,13 @@ func Generate(w io.Writer) *ffcli.Command {
}
}

func saveFile(path string, content io.ReadCloser) error {
assetFile, err := os.Create(path)
if err != nil {
return err
func createEnvironment(gh github.Context, runner github.RunnerContext, tagName string, pc *github.ProvenanceClient) intoto.Provenancer {
if tagName != "" {
return github.NewReleaseEnvironment(gh, runner, tagName, pc)
}
defer assetFile.Close()

_, err = io.Copy(assetFile, content)

return err
return &github.Environment{
Context: &gh,
Runner: &runner,
}
}
101 changes: 101 additions & 0 deletions lib/github/provenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path"
"strings"

"github.com/pkg/errors"

Expand Down Expand Up @@ -48,3 +51,101 @@ func (e *Environment) GenerateProvenanceStatement(ctx context.Context, artifactP

return stmt, nil
}

// ReleaseEnvironment implements intoto.Provenancer to Generate provenance based on a GitHub release
type ReleaseEnvironment struct {
*Environment
pc *ProvenanceClient
tagName string
}

// NewReleaseEnvironment creates a new instance of ReleaseEnvironment with the given tagName and provenanceClient
func NewReleaseEnvironment(gh Context, runner RunnerContext, tagName string, pc *ProvenanceClient) *ReleaseEnvironment {
return &ReleaseEnvironment{
Environment: &Environment{
Context: &gh,
Runner: &runner,
},
pc: pc,
tagName: tagName,
}
}

// GenerateProvenanceStatement generates provenance from the GitHub release environment
//
// Release assets will be downloaded to the given artifactPath
//
// The artifactPath has to be a directory.
func (e *ReleaseEnvironment) GenerateProvenanceStatement(ctx context.Context, artifactPath string) (*intoto.Statement, error) {
err := os.MkdirAll(artifactPath, 0755)
if err != nil {
return nil, err
}
isDir, err := isEmptyDirectory(artifactPath)
if err != nil {
return nil, err
}
if !isDir {
return nil, errors.New("artifactPath has to be an empty directory")
}

owner := e.Context.RepositoryOwner
repo := repositoryName(e.Context.Repository)
rel, err := e.pc.FetchRelease(ctx, owner, repo, e.tagName)
if err != nil {
return nil, err
}
assets, err := e.pc.DownloadReleaseAssets(ctx, owner, repo, rel.GetID())
if err != nil {
return nil, err
}

err = saveAssets(artifactPath, assets)
if err != nil {
return nil, err
}

return e.Environment.GenerateProvenanceStatement(ctx, artifactPath)
}

func isEmptyDirectory(p string) (bool, error) {
f, err := os.Open(p)
if err != nil {
return false, err
}
defer f.Close()

_, err = f.Readdirnames(1)
if err == io.EOF {
return true, nil
}
return false, err
}

func saveAssets(artifactPath string, assets []ReleaseAsset) error {
for _, asset := range assets {
err := saveFile(path.Join(artifactPath, asset.GetName()), asset.Content)
if err != nil {
return err
}
}
return nil
}

func saveFile(path string, content io.ReadCloser) error {
assetFile, err := os.Create(path)
if err != nil {
return err
}
defer assetFile.Close()
defer content.Close()

_, err = io.Copy(assetFile, content)

return err
}

func repositoryName(repo string) string {
repoParts := strings.Split(repo, "/")
return repoParts[len(repoParts)-1]
}
74 changes: 74 additions & 0 deletions lib/github/provenance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,80 @@ func TestGenerateProvenance(t *testing.T) {
assertRecipe(assert, predicate.Recipe)
}

func TestGenerateProvenanceFromGitHubRelease(t *testing.T) {
assert := assert.New(t)

ctx := context.Background()
os.Setenv("GITHUB_ACTIONS", "true")

repoURL := "https://github.com/philips-labs/slsa-provenance-action"

ghContext := github.Context{
RunID: "1029384756",
RepositoryOwner: "philips-labs",
Repository: "philips-labs/slsa-provenance-action",
Event: []byte(pushGitHubEvent),
EventName: "push",
SHA: "849fb987efc0c0fc72e26a38f63f0c00225132be",
}
materials := []intoto.Item{
{URI: "git+" + repoURL, Digest: intoto.DigestSet{"sha1": ghContext.SHA}},
}

runner := github.RunnerContext{}
_, filename, _, _ := runtime.Caller(0)
rootDir := path.Join(path.Dir(filename), "../..")
artifactPath := path.Join(rootDir, "release-assets")

tc := github.NewOAuth2Client(ctx, tokenRetriever)
client := github.NewProvenanceClient(tc)

version := "v0.0.0-rel-test"
releaseId, err := createGitHubRelease(
ctx,
client,
owner,
repo,
version,
path.Join(rootDir, "bin", "slsa-provenance"),
path.Join(rootDir, "README.md"),
)
if !assert.NoError(err) {
return
}
defer func() {
_ = os.RemoveAll(artifactPath)
_, err := client.Repositories.DeleteRelease(ctx, owner, repo, releaseId)
assert.NoError(err)
}()

env := github.NewReleaseEnvironment(ghContext, runner, version, client)
stmt, err := env.GenerateProvenanceStatement(ctx, artifactPath)
if !assert.NoError(err) {
return
}

binaryName := "slsa-provenance"
binaryPath := path.Join(artifactPath, binaryName)
readmeName := "README.md"
readmePath := path.Join(artifactPath, readmeName)

assert.Len(stmt.Subject, 2)
assertSubject(assert, stmt.Subject, binaryName, binaryPath)
assertSubject(assert, stmt.Subject, readmeName, readmePath)

assert.Equal(intoto.SlsaPredicateType, stmt.PredicateType)
assert.Equal(intoto.StatementType, stmt.Type)

predicate := stmt.Predicate
assert.Equal(fmt.Sprintf("%s%s", repoURL, github.HostedIDSuffix), predicate.ID)
assert.Equal(materials, predicate.Materials)
assert.Equal(fmt.Sprintf("%s%s", repoURL, github.HostedIDSuffix), predicate.Builder.ID)

assertMetadata(assert, predicate.Metadata, ghContext, repoURL)
assertRecipe(assert, predicate.Recipe)
}

func assertRecipe(assert *assert.Assertions, recipe intoto.Recipe) {
assert.Equal(github.RecipeType, recipe.Type)
assert.Equal(0, recipe.DefinedInMaterial)
Expand Down
35 changes: 26 additions & 9 deletions lib/github/releases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,12 @@ func TestAddProvenanceToRelease(t *testing.T) {
tc := github.NewOAuth2Client(ctx, tokenRetriever)
client := github.NewProvenanceClient(tc)

rel, _, err := client.Repositories.CreateRelease(
ctx,
owner,
repo,
&gh.RepositoryRelease{TagName: stringPointer("v0.0.0-test"), Draft: boolPointer(true), Prerelease: boolPointer(true)},
)
if !assert.NoError(err) && assert.Nil(rel) {
releaseId, err := createGitHubRelease(ctx, client, owner, repo, "v0.0.0-test")
if !assert.NoError(err) {
return
}
defer func() {
_, err := client.Repositories.DeleteRelease(ctx, owner, repo, rel.GetID())
_, err := client.Repositories.DeleteRelease(ctx, owner, repo, releaseId)
assert.NoError(err)
}()

Expand All @@ -133,7 +128,7 @@ func TestAddProvenanceToRelease(t *testing.T) {
}
assert.Equal("example_build.provenance", stat.Name())

asset, err := client.AddProvenanceToRelease(ctx, owner, repo, rel.GetID(), provenance)
asset, err := client.AddProvenanceToRelease(ctx, owner, repo, releaseId, provenance)
if !assert.NoError(err) && assert.Nil(asset) {
return
}
Expand Down Expand Up @@ -191,3 +186,25 @@ func createProvenanceClient(ctx context.Context) *github.ProvenanceClient {
}
return client
}

func createGitHubRelease(ctx context.Context, client *github.ProvenanceClient, owner, repo, version string, assets ...string) (int64, error) {
rel, _, err := client.Repositories.CreateRelease(
ctx,
owner,
repo,
&gh.RepositoryRelease{TagName: stringPointer(version), Name: stringPointer(version), Draft: boolPointer(true), Prerelease: boolPointer(true)},
)
if err != nil {
return 0, err
}

for _, a := range assets {
asset, err := os.Open(a)
if err != nil {
return 0, err
}
client.AddProvenanceToRelease(ctx, owner, repo, rel.GetID(), asset)
}

return rel.GetID(), nil
}
6 changes: 6 additions & 0 deletions lib/intoto/intoto.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package intoto

import (
"context"
"encoding/json"
"time"
)
Expand All @@ -12,6 +13,11 @@ const (
StatementType = "https://in-toto.io/Statement/v0.1"
)

// Provenancer generates provenance statements for given artifacts
type Provenancer interface {
GenerateProvenanceStatement(ctx context.Context, artifactPath string) (*Statement, error)
}

// Envelope wraps an in-toto statement to be able to attach signatures to the Statement
type Envelope struct {
PayloadType string `json:"payloadType"`
Expand Down

0 comments on commit e3fd8a4

Please sign in to comment.