Skip to content

Commit 9db8688

Browse files
authored
Add 'provenance' basics (#44)
Add basics needed for provenance Taking the prior attestations as input (too difficult to deal with notes within go) Some basic logic for creating the provenance itself and merging results with the prior provenance Still more to do (like hooking this up at least)
1 parent 472a102 commit 9db8688

File tree

4 files changed

+199
-6
lines changed

4 files changed

+199
-6
lines changed

sourcetool/cmd/checklevel.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818

1919
type CheckLevelArgs struct {
2020
commit, owner, repo, branch, outputVsa, outputUnsignedVsa string
21-
minDays int
2221
}
2322

2423
// checklevelCmd represents the checklevel command
@@ -32,20 +31,20 @@ var (
3231
3332
This is meant to be run within the corresponding GitHub Actions workflow.`,
3433
Run: func(cmd *cobra.Command, args []string) {
35-
doCheckLevel(checkLevelArgs.commit, checkLevelArgs.owner, checkLevelArgs.repo, checkLevelArgs.branch, checkLevelArgs.minDays, checkLevelArgs.outputVsa, checkLevelArgs.outputUnsignedVsa)
34+
doCheckLevel(checkLevelArgs.commit, checkLevelArgs.owner, checkLevelArgs.repo, checkLevelArgs.branch, checkLevelArgs.outputVsa, checkLevelArgs.outputUnsignedVsa)
3635
},
3736
}
3837
)
3938

40-
func doCheckLevel(commit, owner, repo, branch string, minDays int, outputVsa, outputUnsignedVsa string) {
39+
func doCheckLevel(commit, owner, repo, branch, outputVsa, outputUnsignedVsa string) {
4140
if commit == "" || owner == "" || repo == "" || branch == "" {
4241
log.Fatal("Must set commit, owner, repo, and branch flags.")
4342
}
4443

4544
gh_client := github.NewClient(nil)
4645
ctx := context.Background()
4746

48-
sourceLevel, err := checklevel.DetermineSourceLevel(ctx, gh_client, commit, owner, repo, branch, minDays)
47+
sourceLevel, err := checklevel.DetermineSourceLevelControlOnly(ctx, gh_client, commit, owner, repo, branch)
4948
if err != nil {
5049
log.Fatal(err)
5150
}
@@ -84,7 +83,6 @@ func init() {
8483
checklevelCmd.Flags().StringVar(&checkLevelArgs.owner, "owner", "", "The GitHub repository owner - required.")
8584
checklevelCmd.Flags().StringVar(&checkLevelArgs.repo, "repo", "", "The GitHub repository name - required.")
8685
checklevelCmd.Flags().StringVar(&checkLevelArgs.branch, "branch", "", "The branch within the repository - required.")
87-
checklevelCmd.Flags().IntVar(&checkLevelArgs.minDays, "min_days", 1, "The minimum duration that the rules need to have been enabled for.")
8886
checklevelCmd.Flags().StringVar(&checkLevelArgs.outputVsa, "output_vsa", "", "The path to write a signed VSA with the determined level.")
8987
checklevelCmd.Flags().StringVar(&checkLevelArgs.outputUnsignedVsa, "output_unsigned_vsa", "", "The path to write an unsigned vsa with the determined level.")
9088
}

sourcetool/cmd/prov.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
3+
*/
4+
package cmd
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"fmt"
10+
"log"
11+
12+
"github.com/slsa-framework/slsa-source-poc/sourcetool/pkg/provenance"
13+
14+
"github.com/google/go-github/v68/github"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
type ProvArgs struct {
19+
prevAttPath, commit, prevCommit, owner, repo, branch string
20+
}
21+
22+
// provCmd represents the prov command
23+
var (
24+
provArgs ProvArgs
25+
provCmd = &cobra.Command{
26+
Use: "prov",
27+
Short: "A brief description of your command",
28+
Long: `A longer description that spans multiple lines and likely contains examples
29+
and usage of using your command. For example:
30+
31+
Cobra is a CLI library for Go that empowers applications.
32+
This application is a tool to generate the needed files
33+
to quickly create a Cobra application.`,
34+
Run: func(cmd *cobra.Command, args []string) {
35+
doProv(provArgs.prevAttPath, provArgs.commit, provArgs.prevCommit, provArgs.owner, provArgs.repo, provArgs.branch)
36+
},
37+
}
38+
)
39+
40+
func doProv(prevAttPath, commit, prevCommit, owner, repo, branch string) {
41+
gh_client := github.NewClient(nil)
42+
ctx := context.Background()
43+
newProv, err := provenance.CreateSourceProvenance(ctx, gh_client, prevAttPath, commit, prevCommit, owner, repo, branch)
44+
if err != nil {
45+
log.Fatal(err)
46+
}
47+
provStr, err := json.Marshal(newProv)
48+
if err != nil {
49+
log.Fatal(err)
50+
}
51+
fmt.Printf("%s\n", string(provStr))
52+
}
53+
54+
func init() {
55+
rootCmd.AddCommand(provCmd)
56+
57+
provCmd.Flags().StringVar(&provArgs.prevAttPath, "prev_att_path", "", "Path to the file with the attestations for the previous commit (as an in-toto bundle).")
58+
provCmd.Flags().StringVar(&provArgs.commit, "commit", "", "The commit to check.")
59+
provCmd.Flags().StringVar(&provArgs.prevCommit, "prev_commit", "", "The commit prior to 'commit'.")
60+
provCmd.Flags().StringVar(&provArgs.owner, "owner", "", "The GitHub repository owner - required.")
61+
provCmd.Flags().StringVar(&provArgs.repo, "repo", "", "The GitHub repository name - required.")
62+
provCmd.Flags().StringVar(&provArgs.branch, "branch", "", "The branch within the repository - required.")
63+
}

sourcetool/pkg/checklevel/checklevel.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,12 @@ func commitPushTime(ctx context.Context, gh_client *github.Client, commit string
6868
return time.Time{}, errors.New(fmt.Sprintf("Could not find repo activity for commit %s", commit))
6969
}
7070

71-
func DetermineSourceLevel(ctx context.Context, gh_client *github.Client, commit string, owner string, repo string, branch string, minDays int) (string, error) {
71+
// Determines the source level using GitHub's built in controls only.
72+
// This is necessarily only as good as GitHub's controls and existing APIs.
73+
// This is a useful demonstration on how SLSA Level 2 can be acheived with ~minimal effort.
74+
//
75+
// Returns the determined source level (level 2 max) or error.
76+
func DetermineSourceLevelControlOnly(ctx context.Context, gh_client *github.Client, commit string, owner string, repo string, branch string) (string, error) {
7277
rules, _, err := gh_client.Repositories.GetRulesForBranch(ctx, owner, repo, branch)
7378

7479
if err != nil {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package provenance
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"encoding/json"
7+
"io"
8+
"os"
9+
"time"
10+
11+
"github.com/google/go-github/v68/github"
12+
"github.com/slsa-framework/slsa-source-poc/sourcetool/pkg/checklevel"
13+
)
14+
15+
type SourceProvenanceProperty struct {
16+
// The time from which this property has been continuously enforced.
17+
Since time.Time
18+
}
19+
type SourceProvenance struct {
20+
// The commit this provenance documents.
21+
Commit string `json:"commit"`
22+
// The commit preceeding 'Commit' in the current context.
23+
PrevCommit string `json:"prev_commit"`
24+
// The properties observed for this commit.
25+
Properties map[string]SourceProvenanceProperty `json:"properties"`
26+
}
27+
28+
func createCurrentProvenance(ctx context.Context, gh_client *github.Client, commit, prevCommit, owner, repo, branch string) (*SourceProvenance, error) {
29+
sourceLevel, err := checklevel.DetermineSourceLevelControlOnly(ctx, gh_client, commit, owner, repo, branch)
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
levelProp := SourceProvenanceProperty{Since: time.Now()}
35+
var curProv SourceProvenance
36+
curProv.Commit = commit
37+
curProv.PrevCommit = prevCommit
38+
curProv.Properties = make(map[string]SourceProvenanceProperty)
39+
curProv.Properties[sourceLevel] = levelProp
40+
41+
return &curProv, nil
42+
}
43+
44+
func convertLineToProv(line string) (*SourceProvenance, error) {
45+
var sp SourceProvenance
46+
err := json.Unmarshal([]byte(line), sp)
47+
if err != nil {
48+
return nil, err
49+
}
50+
return &sp, nil
51+
}
52+
53+
func getPrevProvenance(ctx context.Context, gh_client *github.Client, prevAttPath, prevCommit string) (*SourceProvenance, error) {
54+
if prevAttPath == "" {
55+
// There is no prior provenance
56+
return nil, nil
57+
}
58+
59+
f, err := os.Open(prevAttPath)
60+
if err != nil {
61+
return nil, err
62+
}
63+
reader := bufio.NewReader(f)
64+
65+
for {
66+
line, err := reader.ReadString('\n')
67+
if err != nil {
68+
if err == io.EOF {
69+
// Handle end of file gracefully
70+
if line != "" {
71+
// Is this source provenance?
72+
sp, err := convertLineToProv(line)
73+
if err == nil {
74+
// Should be good!
75+
return sp, nil
76+
}
77+
}
78+
break
79+
}
80+
return nil, err
81+
}
82+
// Is this source provenance?
83+
sp, err := convertLineToProv(line)
84+
if err == nil {
85+
// Should be good!
86+
return sp, nil
87+
}
88+
}
89+
90+
return nil, nil
91+
}
92+
93+
func CreateSourceProvenance(ctx context.Context, gh_client *github.Client, prevAttPath, commit, prevCommit, owner, repo, branch string) (*SourceProvenance, error) {
94+
// Source provenance is based on
95+
// 1. The current control situation (we assume 'commit' has _just_ occurred).
96+
// 2. How long the properties have been enforced according to the previous provenance.
97+
98+
curProv, err := createCurrentProvenance(ctx, gh_client, commit, prevCommit, owner, repo, branch)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
prevProv, err := getPrevProvenance(ctx, gh_client, prevAttPath, prevCommit)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
// No prior provenance found, so we just go with current.
109+
if prevProv == nil {
110+
return curProv, nil
111+
}
112+
113+
// There was prior provenance, so update the Since field for each property
114+
// to the oldest encountered.
115+
for propName, curProp := range curProv.Properties {
116+
prevProp, ok := prevProv.Properties[propName]
117+
if !ok {
118+
continue
119+
}
120+
if prevProp.Since.Before(curProp.Since) {
121+
curProp.Since = prevProp.Since
122+
curProv.Properties[propName] = curProp
123+
}
124+
}
125+
126+
return curProv, nil
127+
}

0 commit comments

Comments
 (0)