Skip to content
This repository has been archived by the owner on Dec 10, 2024. It is now read-only.

Commit

Permalink
feat: support downloading policy from GitHub tree refs
Browse files Browse the repository at this point in the history
  • Loading branch information
alexashley committed Apr 3, 2024
1 parent 50af643 commit 0113bb6
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 20 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,17 @@ The following process is used to create a VSA:

`--policy-url`: Location of policy bundle that will be used to determine VSA result.
Supports http(s) urls for unauthenticated external downloads.
Absolute and relative paths can be used for an existing, local bundle.
Absolute and relative paths can be used for an existing, local bundle or directory.
It's also possible to download files from a particular GitHub commit

Examples:

- `https://github.com/liatrio/gh-trusted-builds-policy/releases/download/v1.4.0/bundle.tar.gz`
- `bundle.tar.gz`
- `../bundle.tar.gz`
- `/Users/myhome/bundle.tar.gz`
- `../policy`
- `https://github.com/liatrio/gh-trusted-builds-policy/tree/ef3194db6ca9a7a4b030686e4669c45db360a0c2/policy`

`--signer-identities-query`: A Rego query that should specify the expected attestation signer identities. The result should be a list of objects that can be unmarshalled into `cosign.Identity`. Defaults to `data.governance.signer_identities`.

Expand Down
2 changes: 2 additions & 0 deletions cmd/vsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ func VsaCmd() *cobra.Command {
Use: "vsa",
Short: "Creates a SLSA verification summary attestation by evaluating an artifact against an OPA policy",
RunE: func(cmd *cobra.Command, args []string) error {
opts.GetTokenFromEnv()

return vsa.Attest(opts)
},
}
Expand Down
134 changes: 115 additions & 19 deletions internal/attestors/vsa/vsa.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package vsa

import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
gh "github.com/liatrio/gh-trusted-builds-attestations/internal/github"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"

"github.com/google/go-containerregistry/pkg/name"
Expand All @@ -25,16 +31,22 @@ import (
"github.com/sigstore/sigstore/pkg/fulcioroots"
)

var (
matchTreeUrl = regexp.MustCompile(`https://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/tree/(?P<branchOrCommit>[^/]+)/(?P<path>[^?]+)`)
)

func Attest(opts *config.VsaCommandOptions) error {
ctx := context.Background()
var err error
bundlePath := opts.PolicyUrl.String()

if opts.PolicyUrl.Value().IsAbs() {
if err := downloadOPABundle(ctx, opts, policyBundleFilePath(opts.PolicyUrl.Value())); err != nil {
if bundlePath, err = downloadOPABundle(ctx, opts); err != nil {
return err
}
}

identities, err := querySignerIdentitiesFromPolicy(ctx, opts)
identities, err := querySignerIdentitiesFromPolicy(ctx, opts, bundlePath)
if err != nil {
return err
}
Expand All @@ -44,7 +56,7 @@ func Attest(opts *config.VsaCommandOptions) error {
return err
}

allowed, err := evaluatePolicy(ctx, opts, attestations)
allowed, err := evaluatePolicy(ctx, opts, bundlePath, attestations)
if err != nil {
return err
}
Expand Down Expand Up @@ -118,12 +130,12 @@ func collectAttestations(ctx context.Context, opts *config.VsaCommandOptions, id
return attestations, nil
}

func querySignerIdentitiesFromPolicy(ctx context.Context, opts *config.VsaCommandOptions) ([]cosign.Identity, error) {
func querySignerIdentitiesFromPolicy(ctx context.Context, opts *config.VsaCommandOptions, bundlePath string) ([]cosign.Identity, error) {
r := rego.New(
rego.Query(opts.SignerIdentitiesQuery),
rego.EnablePrintStatements(opts.Debug),
rego.PrintHook(topdown.NewPrintHook(os.Stderr)),
rego.LoadBundle(policyBundleFilePath(opts.PolicyUrl.Value())),
rego.LoadBundle(bundlePath),
)

rs, err := r.Eval(ctx)
Expand All @@ -148,7 +160,7 @@ func querySignerIdentitiesFromPolicy(ctx context.Context, opts *config.VsaComman
return identities, nil
}

func evaluatePolicy(ctx context.Context, opts *config.VsaCommandOptions, attestations []oci.Signature) (bool, error) {
func evaluatePolicy(ctx context.Context, opts *config.VsaCommandOptions, bundlePath string, attestations []oci.Signature) (bool, error) {
var input []map[string]string

for _, attestation := range attestations {
Expand Down Expand Up @@ -180,7 +192,7 @@ func evaluatePolicy(ctx context.Context, opts *config.VsaCommandOptions, attesta
rego.Input(input),
rego.EnablePrintStatements(opts.Debug),
rego.PrintHook(topdown.NewPrintHook(os.Stderr)),
rego.LoadBundle(policyBundleFilePath(opts.PolicyUrl.Value())),
rego.LoadBundle(bundlePath),
)

rs, err := r.Eval(ctx)
Expand All @@ -191,38 +203,122 @@ func evaluatePolicy(ctx context.Context, opts *config.VsaCommandOptions, attesta
return rs.Allowed(), nil
}

func downloadOPABundle(ctx context.Context, opts *config.VsaCommandOptions, outputFilepath string) error {
func downloadOPABundle(ctx context.Context, opts *config.VsaCommandOptions) (string, error) {
if matchTreeUrl.MatchString(opts.PolicyUrl.String()) {
return downloadGitHubArchive(ctx, opts)
}

return downloadBundleArchive(ctx, opts)
}

func downloadBundleArchive(ctx context.Context, opts *config.VsaCommandOptions) (string, error) {
outputFilePath := "bundle.tar.gz"
client := http.Client{Timeout: time.Minute}

request, err := http.NewRequestWithContext(ctx, http.MethodGet, opts.PolicyUrl.String(), nil)
if err != nil {
return err
return "", err
}

resp, err := client.Do(request)
if err != nil {
return err
return "", err
}
defer resp.Body.Close()

bundleFile, err := os.Create(outputFilepath)
bundleFile, err := os.Create(outputFilePath)
if err != nil {
return err
return "", err
}
defer bundleFile.Close()

_, err = io.Copy(bundleFile, resp.Body)
if err != nil {
return err
return "", err
}

return nil
return outputFilePath, nil
}

func downloadGitHubArchive(ctx context.Context, opts *config.VsaCommandOptions) (string, error) {
matches := matchTreeUrl.FindStringSubmatch(opts.PolicyUrl.String())
if len(matches) == 0 {
return "", fmt.Errorf("unexpected url format")
}

owner := matches[matchTreeUrl.SubexpIndex("owner")]
repo := matches[matchTreeUrl.SubexpIndex("repo")]
branchOrCommit := matches[matchTreeUrl.SubexpIndex("branchOrCommit")]
path := matches[matchTreeUrl.SubexpIndex("path")]

githubClient, err := gh.New(ctx, opts.GitHubToken)
if err != nil {
return "", err
}

archive, err := githubClient.GetRepositoryArchiveAtRef(ctx, &gh.RepositorySlug{Owner: owner, Repo: repo}, branchOrCommit)
if err != nil {
return "", err
}

tmpDir, err := writeArchiveToTmpDir("vsa-policy-*", archive)
if err != nil {
return "", err
}

entries, err := os.ReadDir(tmpDir)
if err != nil {
return "", err
}

// the archive contains a single directory named in the pattern 'org-repo-commitShortSha'
// it's difficult to know this upfront because the user can provide a branch name as well as a commit
dirName := ""
for _, e := range entries {
if e.IsDir() && strings.Contains(e.Name(), repo) {
dirName = e.Name()
}
}

return filepath.Join(tmpDir, dirName, path), nil
}

func policyBundleFilePath(policyUrl *url.URL) string {
if policyUrl.IsAbs() {
return "bundle.tar.gz"
func writeArchiveToTmpDir(tmpDirPrefix string, archive []byte) (string, error) {
tmpDir, err := os.MkdirTemp(os.TempDir(), tmpDirPrefix)
if err != nil {
return "", err
}

gr, err := gzip.NewReader(bytes.NewReader(archive))
defer gr.Close()

tr := tar.NewReader(gr)
for {
header, err := tr.Next()
if err != nil {
break
}

tmpPath := filepath.Join(tmpDir, header.Name)

if header.FileInfo().IsDir() {
if err := os.MkdirAll(tmpPath, os.FileMode(header.Mode)); err != nil {
return "", err
}
} else {
file, err := os.Create(tmpPath)
if err != nil {
return "", err
}

if _, err := io.Copy(file, tr); err != nil {
_ = file.Close()
return "", err
}

_ = file.Close()
}
}

return policyUrl.String()
return tmpDir, nil
}
6 changes: 6 additions & 0 deletions internal/config/vsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

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

type VsaCommandOptions struct {
Expand All @@ -11,6 +12,7 @@ type VsaCommandOptions struct {
Debug bool
SignerIdentitiesQuery string
PolicyQuery string
GitHubToken string
}

func NewVsaCommandOptions() *VsaCommandOptions {
Expand All @@ -33,3 +35,7 @@ func (vsa *VsaCommandOptions) AddFlags(cmd *cobra.Command) {

vsa.GlobalOptions.AddFlags(cmd)
}

func (vsa *VsaCommandOptions) GetTokenFromEnv() {
vsa.GitHubToken = os.Getenv("GITHUB_TOKEN")
}
30 changes: 30 additions & 0 deletions internal/github/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
Expand Down Expand Up @@ -43,6 +44,7 @@ type Client interface {
GetPullRequest(ctx context.Context, slug *RepositorySlug, number int) (*github.PullRequest, error)
ListPullRequestCommits(ctx context.Context, slug *RepositorySlug, number int) ([]*github.RepositoryCommit, error)
ListPullRequestReviews(ctx context.Context, slug *RepositorySlug, number int) ([]*github.PullRequestReview, error)
GetRepositoryArchiveAtRef(ctx context.Context, slug *RepositorySlug, ref string) ([]byte, error)
}

type githubClient struct {
Expand Down Expand Up @@ -159,6 +161,34 @@ func (g *githubClient) ListPullRequestReviews(ctx context.Context, slug *Reposit
return reviews, nil
}

func (g *githubClient) GetRepositoryArchiveAtRef(ctx context.Context, slug *RepositorySlug, ref string) ([]byte, error) {
archiveLocation, _, err := g.github.Repositories.GetArchiveLink(ctx, slug.Owner, slug.Repo, github.Tarball, &github.RepositoryContentGetOptions{
Ref: ref,
}, 0)

if err != nil {
return nil, err
}

response, err := g.github.Client().Get(archiveLocation.String())
if err != nil {
return nil, err
}

if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode)
}

defer response.Body.Close()

archive, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}

return archive, nil
}

type paginatedEndpoint[T any] func(*github.ListOptions) ([]*T, *github.Response, error)

func paginate[T any](endpoint paginatedEndpoint[T]) ([]*T, error) {
Expand Down

0 comments on commit 0113bb6

Please sign in to comment.