From 3ff1cadabe76920bcafdcb8a70aca60f21222fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 4 Mar 2026 23:46:02 -0600 Subject: [PATCH 01/41] Add all control labels and level conf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/slsa/controls.go | 70 ++++++++++++++++++++++++++++++++++++++++++++ pkg/slsa/levels.go | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 pkg/slsa/controls.go create mode 100644 pkg/slsa/levels.go diff --git a/pkg/slsa/controls.go b/pkg/slsa/controls.go new file mode 100644 index 0000000..f2bf12b --- /dev/null +++ b/pkg/slsa/controls.go @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors +// SPDX-License-Identifier: Apache-2.0 + +package slsa + +import "slices" + +type ( + ControlName string + ControlState string + + // ControlSet is a list of controls + ControlSet []ControlName +) + +const ( + // Control constants + DEPRECATED_ContinuityEnforced ControlName = "CONTINUITY_ENFORCED" + DEPRECATED_ProvenanceAvailable ControlName = "PROVENANCE_AVAILABLE" + DEPRECATED_ReviewEnforced ControlName = "REVIEW_ENFORCED" + DEPRECATED_TagHygiene ControlName = "TAG_HYGIENE" + + // Virtual control to manage policy lifcycle + PolicyAvailable ControlName = "POLICY_AVAILABLE" + + SLSA_SOURCE_ORG_SCS ControlName = "SLSA_SOURCE_ORG_SCS" + SLSA_SOURCE_ORG_ACCESS_CONTROL ControlName = "SLSA_SOURCE_ORG_ACCESS_CONTROL" + SLSA_SOURCE_ORG_SAFE_EXPUNGE ControlName = "SLSA_SOURCE_ORG_SAFE_EXPUNGE" + SLSA_SOURCE_ORG_CONTINUITY ControlName = "SLSA_SOURCE_ORG_CONTINUITY" + SLSA_SOURCE_SCS_REPO_ID ControlName = "SLSA_SOURCE_SCS_REPO_ID" + SLSA_SOURCE_SCS_REVISION_ID ControlName = "SLSA_SOURCE_SCS_REVISION_ID" + SLSA_SOURCE_SCS_DIFF_DISPLAY ControlName = "SLSA_SOURCE_SCS_DIFF_DISPLAY" + SLSA_SOURCE_SCS_VSA ControlName = "SLSA_SOURCE_SCS_VSA" + SLSA_SOURCE_SCS_HISTORY ControlName = "SLSA_SOURCE_SCS_HISTORY" + SLSA_SOURCE_SCS_CONTINUITY ControlName = "SLSA_SOURCE_SCS_CONTINUITY" + SLSA_SOURCE_SCS_IDENTITY ControlName = "SLSA_SOURCE_SCS_IDENTITY" + SLSA_SOURCE_SCS_PROVENANCE ControlName = "SLSA_SOURCE_SCS_PROVENANCE" + SLSA_SOURCE_SCS_PROTECTED_REFS ControlName = "SLSA_SOURCE_SCS_PROTECTED_REFS" + SLSA_SOURCE_SCS_TWO_PARTY_REVIEW ControlName = "SLSA_SOURCE_SCS_TWO_PARTY_REVIEW" + + // Control lifecycle states + StateNotEnabled ControlState = "not_enabled" + StateInProgress ControlState = "in_progress" + StateActive ControlState = "active" +) + +// AllLevelControls is a set holding all controls of the SLSA Source spec +var AllLevelControls = ControlSet{ + SLSA_SOURCE_ORG_SCS, + SLSA_SOURCE_ORG_ACCESS_CONTROL, + SLSA_SOURCE_ORG_SAFE_EXPUNGE, + SLSA_SOURCE_ORG_CONTINUITY, + SLSA_SOURCE_SCS_REPO_ID, + SLSA_SOURCE_SCS_REVISION_ID, + SLSA_SOURCE_SCS_DIFF_DISPLAY, + SLSA_SOURCE_SCS_VSA, + SLSA_SOURCE_SCS_HISTORY, + SLSA_SOURCE_SCS_CONTINUITY, + SLSA_SOURCE_SCS_IDENTITY, + SLSA_SOURCE_SCS_PROVENANCE, + SLSA_SOURCE_SCS_PROTECTED_REFS, + SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, +} + +func (cs ControlSet) GetControl(ctrl ControlName) ControlName { + if slices.Contains(cs, ctrl) { + return ctrl + } + return "" +} diff --git a/pkg/slsa/levels.go b/pkg/slsa/levels.go new file mode 100644 index 0000000..065764b --- /dev/null +++ b/pkg/slsa/levels.go @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright 2026 The SLSA Authors +// SPDX-License-Identifier: Apache-2.0 + +package slsa + +type Level struct { + Level uint8 + Controls ControlSet +} + +var Level0 = ControlSet{} + +var Level1 = ControlSet{ + SLSA_SOURCE_ORG_SCS, + SLSA_SOURCE_SCS_REPO_ID, + SLSA_SOURCE_SCS_REVISION_ID, + SLSA_SOURCE_SCS_DIFF_DISPLAY, + SLSA_SOURCE_SCS_VSA, +} + +var Level2 = ControlSet{ + SLSA_SOURCE_ORG_SCS, + SLSA_SOURCE_ORG_ACCESS_CONTROL, + SLSA_SOURCE_ORG_SAFE_EXPUNGE, + SLSA_SOURCE_SCS_REPO_ID, + SLSA_SOURCE_SCS_REVISION_ID, + SLSA_SOURCE_SCS_DIFF_DISPLAY, + SLSA_SOURCE_SCS_VSA, + SLSA_SOURCE_SCS_HISTORY, + SLSA_SOURCE_SCS_CONTINUITY, + SLSA_SOURCE_SCS_IDENTITY, + SLSA_SOURCE_SCS_PROVENANCE, +} + +var Level3 = ControlSet{ + SLSA_SOURCE_ORG_SCS, + SLSA_SOURCE_ORG_ACCESS_CONTROL, + SLSA_SOURCE_ORG_SAFE_EXPUNGE, + SLSA_SOURCE_ORG_CONTINUITY, + SLSA_SOURCE_SCS_REPO_ID, + SLSA_SOURCE_SCS_REVISION_ID, + SLSA_SOURCE_SCS_DIFF_DISPLAY, + SLSA_SOURCE_SCS_VSA, + SLSA_SOURCE_SCS_HISTORY, + SLSA_SOURCE_SCS_CONTINUITY, + SLSA_SOURCE_SCS_IDENTITY, + SLSA_SOURCE_SCS_PROVENANCE, + SLSA_SOURCE_SCS_PROTECTED_REFS, +} + +var Level4 = ControlSet{ + SLSA_SOURCE_ORG_SCS, + SLSA_SOURCE_ORG_ACCESS_CONTROL, + SLSA_SOURCE_ORG_SAFE_EXPUNGE, + SLSA_SOURCE_ORG_CONTINUITY, + SLSA_SOURCE_SCS_REPO_ID, + SLSA_SOURCE_SCS_REVISION_ID, + SLSA_SOURCE_SCS_DIFF_DISPLAY, + SLSA_SOURCE_SCS_VSA, + SLSA_SOURCE_SCS_HISTORY, + SLSA_SOURCE_SCS_CONTINUITY, + SLSA_SOURCE_SCS_IDENTITY, + SLSA_SOURCE_SCS_PROVENANCE, + SLSA_SOURCE_SCS_PROTECTED_REFS, + SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, +} From 0f6ca96796dfc9cc684792b65ef478a80aa99507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 4 Mar 2026 23:49:22 -0600 Subject: [PATCH 02/41] Support full controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/attest/provenance.go | 21 +++- pkg/attest/vsa.go | 2 +- pkg/ghcontrol/checklevel.go | 19 +++- pkg/policy/policy.go | 17 ++- pkg/slsa/slsa_types.go | 42 +++---- pkg/sourcetool/backends/vcs/github/github.go | 112 +++++++++++++++++-- pkg/sourcetool/tool.go | 2 +- 7 files changed, 160 insertions(+), 55 deletions(-) diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index d0f074f..a08879c 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -8,7 +8,6 @@ import ( "context" "errors" "fmt" - "log" "os" "strings" "time" @@ -150,13 +149,29 @@ func (pa ProvenanceAttestor) createCurrentProvenance(ctx context.Context, commit // See https://github.com/slsa-framework/source-tool/issues/272 curProvPred.AddControl( &provenance.Control{ - Name: slsa.ProvenanceAvailable.String(), + Name: slsa.SLSA_SOURCE_SCS_PROVENANCE.String(), }, ) return addPredToStatement(&curProvPred, provenance.SourceProvPredicateType, commit) } +// GetVSA returns the revision VSA +func (pa ProvenanceAttestor) GetVSA(ctx context.Context, commit, ref string) (*spb.Statement, *v1.VerificationSummary, error) { + notes, err := pa.gh_connection.GetNotesForCommit(ctx, commit) + if notes == "" { + Debugf("didn't find notes for commit %s", commit) + return nil, nil, nil + } + + if err != nil { + return nil, nil, fmt.Errorf("fetching notes from commit: %w", err) + } + + bundleReader := NewBundleReader(bufio.NewReader(strings.NewReader(notes)), pa.verifier) + return getVsaFromReader(bundleReader, commit, ref, "") +} + // Gets provenance for the commit from git notes. func (pa ProvenanceAttestor) GetProvenance(ctx context.Context, commit, ref string) (*spb.Statement, *provenance.SourceProvenancePred, error) { notes, err := pa.gh_connection.GetNotesForCommit(ctx, commit) @@ -166,7 +181,7 @@ func (pa ProvenanceAttestor) GetProvenance(ctx context.Context, commit, ref stri } if err != nil { - log.Fatal(err) + return nil, nil, fmt.Errorf("fetching notes from commit: %w", err) } bundleReader := NewBundleReader(bufio.NewReader(strings.NewReader(notes)), pa.verifier) diff --git a/pkg/attest/vsa.go b/pkg/attest/vsa.go index 59e60aa..c366f89 100644 --- a/pkg/attest/vsa.go +++ b/pkg/attest/vsa.go @@ -160,7 +160,7 @@ func getVsaFromReader(reader *BundleReader, commit, ref, repoUri string) (*spb.S // Does repo match? // remove git+http:// from resourceUri cleanResourceUri := strings.TrimPrefix(vsaPred.GetResourceUri(), "git+") - if cleanResourceUri != repoUri { + if repoUri != "" && cleanResourceUri != repoUri { Debugf("ResourceUri is %s but we want %s", cleanResourceUri, repoUri) continue } diff --git a/pkg/ghcontrol/checklevel.go b/pkg/ghcontrol/checklevel.go index c718f0d..f888b28 100644 --- a/pkg/ghcontrol/checklevel.go +++ b/pkg/ghcontrol/checklevel.go @@ -101,7 +101,7 @@ func (ghc *GitHubConnection) ruleMeetsRequiresReview(rule *github.PullRequestBra } // Computes the continuity control returning nil if it's not enabled. -func (ghc *GitHubConnection) computeContinuityControl(ctx context.Context, rules *github.BranchRules) (*provenance.Control, error) { +func (ghc *GitHubConnection) computeRepoContinuityControl(ctx context.Context, rules *github.BranchRules) (*provenance.Control, error) { oldestDeletion, err := ghc.getOldestActiveRule(ctx, rules.Deletion) if err != nil { return nil, fmt.Errorf("looking for oldest branch delete protection: %w", err) @@ -122,9 +122,11 @@ func (ghc *GitHubConnection) computeContinuityControl(ctx context.Context, rules newestRule = oldestNoFf } - return &provenance.Control{Name: string(slsa.ContinuityEnforced), Since: timestamppb.New(newestRule.UpdatedAt.Time)}, nil + return &provenance.Control{Name: string(slsa.SLSA_SOURCE_SCS_CONTINUITY), Since: timestamppb.New(newestRule.UpdatedAt.Time)}, nil } +// enforcesTagHygiene checks a repository ruleset to understand if it is +// configured to protect the branch func enforcesTagHygiene(ruleset *github.RepositoryRuleset) bool { if ruleset.Rules != nil && ruleset.Rules.Update != nil && @@ -138,6 +140,7 @@ func enforcesTagHygiene(ruleset *github.RepositoryRuleset) bool { return false } +// computeTagHygieneControl func (ghc *GitHubConnection) computeTagHygieneControl(ctx context.Context, allRulesets []*github.RepositoryRuleset) (*provenance.Control, error) { var validRuleset *github.RepositoryRuleset for _, ruleset := range allRulesets { @@ -168,7 +171,10 @@ func (ghc *GitHubConnection) computeTagHygieneControl(ctx context.Context, allRu return nil, nil } - return &provenance.Control{Name: slsa.TagHygiene.String(), Since: timestamppb.New(validRuleset.UpdatedAt.Time)}, nil + return &provenance.Control{ + Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS.String(), + Since: timestamppb.New(validRuleset.UpdatedAt.Time), + }, nil } // Computes the review control returning nil if it's not enabled. @@ -189,7 +195,10 @@ func (ghc *GitHubConnection) computeReviewControl(ctx context.Context, rules []* } if oldestActive != nil { - return &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: timestamppb.New(oldestActive.UpdatedAt.Time)}, nil + return &provenance.Control{ + Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW.String(), + Since: timestamppb.New(oldestActive.UpdatedAt.Time), + }, nil } return nil, nil @@ -356,7 +365,7 @@ func (ghc *GitHubConnection) GetBranchControls(ctx context.Context, ref string) } // Compute the controls enforced. - continuityControl, err := ghc.computeContinuityControl(ctx, branchRules) + continuityControl, err := ghc.computeRepoContinuityControl(ctx, branchRules) if err != nil { return nil, fmt.Errorf("could not populate ContinuityControl: %w", err) } diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index bb283a8..ccf3bbb 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -279,7 +279,7 @@ func (pe *PolicyEvaluator) CreateLocalPolicy(ctx context.Context, repo *models.R // If the controls returned controls := slsa.Controls(provPred.GetControls()) - tagHygiene := controls.GetControl(slsa.TagHygiene) + tagHygiene := controls.GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS) if tagHygiene != nil { p.ProtectedTag = &ProtectedTag{ Since: tagHygiene.GetSince(), @@ -387,7 +387,7 @@ func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, contr return []slsa.ControlName{}, nil } - reviewControl := controls.GetControl(slsa.ReviewEnforced) + reviewControl := controls.GetControl(slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW) if reviewControl == nil { return []slsa.ControlName{}, fmt.Errorf("policy requires review, but that control is not enabled") } @@ -396,29 +396,36 @@ func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, contr return []slsa.ControlName{}, fmt.Errorf("policy requires review since %v, but that control has only been enabled since %v", branchPolicy.GetSince(), reviewControl.GetSince()) } - return []slsa.ControlName{slsa.ReviewEnforced}, nil + return []slsa.ControlName{slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, nil } +// computeTagHygiene checks if the current state of the protected refs +// matches what we see in the policy policy has SLSA_SOURCE_SCS_PROTECTED_REFS func computeTagHygiene(_ *ProtectedBranch, tagPolicy *ProtectedTag, controls slsa.Controls) ([]slsa.ControlName, error) { if tagPolicy == nil { // There is no tag policy, so the control isn't met, but it's not an error. return []slsa.ControlName{}, nil } + // The tag entry in the policy does not have tag_hygiene if !tagPolicy.GetTagHygiene() { return []slsa.ControlName{}, nil } - tagHygiene := controls.GetControl(slsa.TagHygiene) + // Get the current state of protected refs + tagHygiene := controls.GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS) + + // Tags are not protected. Policy fails if tagHygiene == nil { return []slsa.ControlName{}, fmt.Errorf("policy requires tag hygiene, but that control is not enabled") } + // Tags were protected later than the policy date. Fail. mmmh.. if tagPolicy.GetSince().AsTime().Before(tagHygiene.GetSince().AsTime()) { return []slsa.ControlName{}, fmt.Errorf("policy requires tag hygiene since %v, but that control has only been enabled since %v", tagPolicy.GetSince(), tagHygiene.GetSince()) } - return []slsa.ControlName{slsa.TagHygiene}, nil + return []slsa.ControlName{slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, nil } func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls slsa.Controls) ([]slsa.ControlName, error) { diff --git a/pkg/slsa/slsa_types.go b/pkg/slsa/slsa_types.go index c705585..78189da 100644 --- a/pkg/slsa/slsa_types.go +++ b/pkg/slsa/slsa_types.go @@ -13,8 +13,6 @@ import ( ) type ( - ControlName string - ControlState string SlsaSourceLevel ControlName ) @@ -23,30 +21,16 @@ func (c ControlName) String() string { } const ( - SlsaSourceLevel1 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_1" - SlsaSourceLevel2 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_2" - SlsaSourceLevel3 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_3" - SlsaSourceLevel4 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_4" - ContinuityEnforced ControlName = "CONTINUITY_ENFORCED" - ProvenanceAvailable ControlName = "PROVENANCE_AVAILABLE" - ReviewEnforced ControlName = "REVIEW_ENFORCED" - TagHygiene ControlName = "TAG_HYGIENE" - PolicyAvailable ControlName = "POLICY_AVAILABLE" - SourceBranchesAnnotation = "source_branches" - SourceRefsAnnotation = "source_refs" - AllowedOrgPropPrefix = "ORG_SOURCE_" - - // Control lifecycle states - StateNotEnabled ControlState = "not_enabled" - StateInProgress ControlState = "in_progress" - StateActive ControlState = "active" + SlsaSourceLevel1 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_1" + SlsaSourceLevel2 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_2" + SlsaSourceLevel3 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_3" + SlsaSourceLevel4 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_4" + + SourceBranchesAnnotation = "source_branches" + SourceRefsAnnotation = "source_refs" + AllowedOrgPropPrefix = "ORG_SOURCE_" ) -// AllLevelControls lists all the SLSA controls managed by sourcetool -var AllLevelControls = []ControlName{ - ContinuityEnforced, ProvenanceAvailable, ReviewEnforced, TagHygiene, -} - func IsSlsaSourceLevel(control ControlName) bool { return slices.Contains( []ControlName{ @@ -112,16 +96,16 @@ func (controls *Controls) Names() []ControlName { type SourceVerifiedLevels []ControlName // Returns the list of control names that must be set for the given slsa level. -func GetRequiredControlsForLevel(level SlsaSourceLevel) []ControlName { +func GetRequiredControlsForLevel(level SlsaSourceLevel) ControlSet { switch level { case SlsaSourceLevel1: - return []ControlName{} + return Level1 case SlsaSourceLevel2: - return []ControlName{ContinuityEnforced, TagHygiene} + return Level2 case SlsaSourceLevel3: - return []ControlName{ContinuityEnforced, TagHygiene, ProvenanceAvailable} + return Level3 case SlsaSourceLevel4: - return []ControlName{ContinuityEnforced, TagHygiene, ProvenanceAvailable, ReviewEnforced} + return Level4 default: return []ControlName{} } diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index 02ddf3c..ef2245c 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "log" + "time" "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/auth" @@ -17,6 +18,55 @@ import ( "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) +// InherentControls are the controls that are always true because we are +// in git and/org GitHub. +var InherentControls = slsa.ControlSet{ + // GitHub uses git + slsa.SLSA_SOURCE_ORG_SCS, + + // GitHub enforces access control + slsa.SLSA_SOURCE_ORG_ACCESS_CONTROL, + + // There is no safe expunge in Git or GitHub but as long as users + // cannot force push, then things cannot be expunged. So I think + // it passes. + // slsa.SLSA_SOURCE_ORG_SAFE_EXPUNGE, + + // All the org controls are default expect for expunge, so we tie + // org continuity to the branch continuity + // slsa.SLSA_SOURCE_ORG_CONTINUITY, + + // GitHub gives you a repo id/uri + slsa.SLSA_SOURCE_SCS_REPO_ID, + + // Git commit + slsa.SLSA_SOURCE_SCS_REVISION_ID, + + // git diff + slsa.SLSA_SOURCE_SCS_DIFF_DISPLAY, + + // We disable VSA until checking we have one + // slsa.SLSA_SOURCE_SCS_VSA, + + // Git is change history + slsa.SLSA_SOURCE_SCS_HISTORY, + + // We will compute continuity from the GH api + // slsa.SLSA_SOURCE_SCS_CONTINUITY, + + // Both git and GitHub have user identities + slsa.SLSA_SOURCE_SCS_IDENTITY, + + // We disable provenance until we check we have an attestation + // slsa.SLSA_SOURCE_SCS_PROVENANCE, + + // This depends on branch protection + // slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, + + // We will check for two party review in the API + // slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, +} + func New() *Backend { return &Backend{ authenticator: auth.New(), @@ -83,7 +133,8 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos return nil, fmt.Errorf("getting github connection: %w", err) } - // Get the active controls + // The branch controls returned from ghcontrol only include the 4 + // legacy checks sourcetool did (continuity, review, RequiredChecks, tag hygiene) activeControls, err := ghc.GetBranchControls(ctx, branch.FullRef()) if err != nil { return nil, fmt.Errorf("checking status: %w", err) @@ -102,20 +153,58 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos } if attestation != nil { activeControls.AddControl(&provenance.Control{ - Name: slsa.ProvenanceAvailable.String(), + Name: slsa.SLSA_SOURCE_SCS_PROVENANCE.String(), + }) + + // If we got the provenance attestaion, we assume we also have a VSA + activeControls.AddControl(&provenance.Control{ + Name: slsa.SLSA_SOURCE_SCS_VSA.String(), }) } else { log.Printf("No provenance attestation found on %s", commit.SHA) } + // NewControlSetStatus returns all the controls for the framework in + // StateNotEnabled. status := slsa.NewControlSetStatus() + sinceForever := time.Unix(1, 0) for i, ctrl := range status.Controls { + // Check if it's an inherent control, turn it on and don't look back + if c := InherentControls.GetControl(slsa.ControlName(ctrl.Name.String())); c != "" { + status.Controls[i].Since = &sinceForever + status.Controls[i].State = slsa.StateActive + status.Controls[i].Message = "Inherent" + continue + } + + // Check if it's one of the active controls if c := activeControls.GetControl(ctrl.Name); c != nil { t := c.GetSince().AsTime() status.Controls[i].Since = &t status.Controls[i].State = slsa.StateActive status.Controls[i].Message = b.controlImplementationMessage(slsa.ControlName(c.GetName())) } + + // Enable ORG_SAFE_EXPUNGE when branch protection (protected refs) is active. + // Without force push, content cannot be expunged. + if ctrl.Name == slsa.SLSA_SOURCE_ORG_SAFE_EXPUNGE { + if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS); c != nil { + t := c.GetSince().AsTime() + status.Controls[i].Since = &t + status.Controls[i].State = slsa.StateActive + status.Controls[i].Message = b.controlImplementationMessage(ctrl.Name) + } + } + + // Enable ORG_CONTINUITY when SCS branch continuity is active. + if ctrl.Name == slsa.SLSA_SOURCE_ORG_CONTINUITY { + if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_CONTINUITY); c != nil { + t := c.GetSince().AsTime() + status.Controls[i].Since = &t + status.Controls[i].State = slsa.StateActive + status.Controls[i].Message = b.controlImplementationMessage(ctrl.Name) + } + } } // The only control which can be in in_progress state in GitHub is @@ -123,7 +212,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos // here and report back the status. switchProvCtlToInProgress := false var provenanceMessage string - if c := activeControls.GetControl(slsa.ProvenanceAvailable); c == nil { + if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_PROVENANCE); c == nil { pr, err := b.FindWorkflowPR(ctx, r) if err != nil { return nil, fmt.Errorf("looking for provenance workflow pull request: %w", err) @@ -139,7 +228,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos // Populate the recommended actions for i := range status.Controls { // Piggyback on this loop to switch the provenance status for efficiency - if switchProvCtlToInProgress && status.Controls[i].Name == slsa.ProvenanceAvailable { + if switchProvCtlToInProgress && status.Controls[i].Name == slsa.SLSA_SOURCE_SCS_PROVENANCE { status.Controls[i].State = slsa.StateInProgress status.Controls[i].Message = provenanceMessage } @@ -153,14 +242,15 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos // controlImplementationMessage returns an implementation message to populate the // status message when controls are active. func (b *Backend) controlImplementationMessage(ctrlName slsa.ControlName) string { + //nolint:exhaustive switch ctrlName { - case slsa.ProvenanceAvailable: + case slsa.SLSA_SOURCE_SCS_PROVENANCE, slsa.DEPRECATED_ProvenanceAvailable: return "Signed provenance metadata is being published on every commit" - case slsa.TagHygiene: + case slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, slsa.DEPRECATED_TagHygiene: return "Tag protections are configured in the repository" - case slsa.ReviewEnforced: + case slsa.DEPRECATED_ReviewEnforced: return "Code review is enforced in the repository" - case slsa.ContinuityEnforced: + case slsa.SLSA_SOURCE_ORG_CONTINUITY, slsa.DEPRECATED_ContinuityEnforced: return "Push and delete protection is enabled on the branch" case slsa.PolicyAvailable: return "The repository has published a policy" @@ -229,7 +319,7 @@ func (b *Backend) GetLatestCommit(ctx context.Context, r *models.Repository, bra func (b *Backend) getRecommendedAction(r *models.Repository, _ *models.Branch, control slsa.ControlName, state slsa.ControlState) *slsa.ControlRecommendedAction { //nolint:exhaustive // Not all drivers handle all controls switch control { - case slsa.ProvenanceAvailable: + case slsa.DEPRECATED_ProvenanceAvailable: switch state { case slsa.StateInProgress: return &slsa.ControlRecommendedAction{ @@ -243,7 +333,7 @@ func (b *Backend) getRecommendedAction(r *models.Repository, _ *models.Branch, c default: return nil } - case slsa.ContinuityEnforced: + case slsa.SLSA_SOURCE_ORG_CONTINUITY, slsa.DEPRECATED_ContinuityEnforced: if state == slsa.StateNotEnabled { return &slsa.ControlRecommendedAction{ Message: "Enable branch push/delete protection", @@ -251,7 +341,7 @@ func (b *Backend) getRecommendedAction(r *models.Repository, _ *models.Branch, c } } return nil - case slsa.TagHygiene: + case slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, slsa.DEPRECATED_TagHygiene: if state == slsa.StateNotEnabled { return &slsa.ControlRecommendedAction{ Message: "Enable tag push/update/delete protection", diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index f7970ae..300d74c 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -223,7 +223,7 @@ func (t *Tool) createPolicy(r *models.Repository, branch *models.Branch, control } // If the controls returned - tagHygiene := controls.GetActiveControls().GetControl(slsa.TagHygiene) + tagHygiene := controls.GetActiveControls().GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS) if tagHygiene != nil { p.ProtectedTag = &policy.ProtectedTag{ Since: tagHygiene.GetSince(), From 9434285767818bd7086ddc8a1dff7ad8c1eea60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 4 Mar 2026 23:50:43 -0600 Subject: [PATCH 03/41] =?UTF-8?q?Make=20tests=20pass=20(easy=20?= =?UTF-8?q?=F0=9F=98=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/ghcontrol/checklevel_test.go | 12 +- pkg/policy/policy_test.go | 307 +++++++++++++++---------------- pkg/sourcetool/tool_test.go | 2 +- 3 files changed, 159 insertions(+), 162 deletions(-) diff --git a/pkg/ghcontrol/checklevel_test.go b/pkg/ghcontrol/checklevel_test.go index 2e0f8a3..d1abcc0 100644 --- a/pkg/ghcontrol/checklevel_test.go +++ b/pkg/ghcontrol/checklevel_test.go @@ -241,17 +241,17 @@ func TestBuiltinBranchControls(t *testing.T) { { branchRules: createContinuityBranchRules(), rulesetRules: rulesForBranchContinuity(), - expectedControl: slsa.ContinuityEnforced, + expectedControl: slsa.SLSA_SOURCE_SCS_CONTINUITY, }, { branchRules: createReviewBranchRules(), rulesetRules: rulesForReviewEnforced(), - expectedControl: slsa.ReviewEnforced, + expectedControl: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, }, { branchRules: createTagHygieneRules(), rulesetRules: rulesForTagHygiene(), - expectedControl: slsa.TagHygiene, + expectedControl: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, }, } for _, tt := range tests { @@ -285,17 +285,17 @@ func TestBuiltinBranchControlsEnabledLater(t *testing.T) { { branchRules: createContinuityBranchRules(), rulesetRules: rulesForBranchContinuity(), - name: slsa.ContinuityEnforced, + name: slsa.SLSA_SOURCE_SCS_CONTINUITY, }, { branchRules: createReviewBranchRules(), rulesetRules: rulesForReviewEnforced(), - name: slsa.ReviewEnforced, + name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, }, { branchRules: createTagHygieneRules(), rulesetRules: rulesForTagHygiene(), - name: slsa.TagHygiene, + name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, }, } for _, tt := range tests { diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index 8fcef68..9d595a1 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -148,17 +148,35 @@ func validateMockServerRequestPath(t *testing.T, r *http.Request, expectedPolicy } } +// controlsForLevel creates a slsa.Controls collection with all required controls +// for the given SLSA level, each with the given Since timestamp. +func controlsForLevel(level slsa.SlsaSourceLevel, since *timestamppb.Timestamp) slsa.Controls { + required := slsa.GetRequiredControlsForLevel(level) + controls := slsa.Controls{} + for _, name := range required { + controls.AddControl(&provenance.Control{Name: name.String(), Since: since}) + } + return controls +} + +// controlsForLevelWith creates controls for a level plus additional controls. +func controlsForLevelWith(level slsa.SlsaSourceLevel, since *timestamppb.Timestamp, extra ...*provenance.Control) slsa.Controls { + controls := controlsForLevel(level, since) + controls.AddControl(extra...) + return controls +} + func TestEvaluateSourceProv_Success(t *testing.T) { - // Controls for mock provenance - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlierFixedTime} - provenanceAvailableEarlier := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: earlierFixedTime} - reviewEnforcedEarlier := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: earlierFixedTime} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlierFixedTime} orgTestControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_test", Since: earlierFixedTime} - // Valid Provenance Predicate (attest.SourceProvenancePred) + // Valid Provenance Predicate with L3 controls + review + tag hygiene + org control + l3Controls := controlsForLevelWith(slsa.SlsaSourceLevel3, earlierFixedTime, + &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW.String(), Since: earlierFixedTime}, + orgTestControl, + ) + validProvPredicateL3Controls := provenance.SourceProvenancePred{ - Controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier, orgTestControl}, + Controls: l3Controls, } provenanceStatement := createStatementForTest(t, &validProvPredicateL3Controls, provenance.SourceProvPredicateType) @@ -193,19 +211,13 @@ func TestEvaluateSourceProv_Success(t *testing.T) { if policyPath != expectedPolicyFilePath { t.Errorf("EvaluateSourceProv() policyPath = %q, want %q", policyPath, expectedPolicyFilePath) } - expectedLevels := slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.ReviewEnforced, slsa.TagHygiene, "ORG_SOURCE_TESTED"} + expectedLevels := slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, "ORG_SOURCE_TESTED"} if !slices.Equal(verifiedLevels, expectedLevels) { t.Errorf("EvaluateSourceProv() verifiedLevels = %v, want %v", verifiedLevels, expectedLevels) } } func TestEvaluateSourceProv_Failure(t *testing.T) { - // Controls for mock provenance - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlierFixedTime} - provenanceAvailableEarlier := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: earlierFixedTime} - reviewEnforcedEarlier := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: earlierFixedTime} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlierFixedTime} - // Policies policyL3ReviewTagsNow := RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ @@ -213,19 +225,19 @@ func TestEvaluateSourceProv_Failure(t *testing.T) { }, ProtectedTag: &ProtectedTag{Since: fixedTime, TagHygiene: true}, } - policyL1NoExtrasNow := RepoPolicy{ // Policy for default/branch not found cases + policyL1NoExtrasNow := RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ {Name: "otherbranch", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), Since: fixedTime}, }, ProtectedTag: nil, } - // Valid Provenance Predicate (attest.SourceProvenancePred) + // Valid Provenance Predicates validProvPredicateL3Controls := provenance.SourceProvenancePred{ - Controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, + Controls: controlsForLevel(slsa.SlsaSourceLevel3, earlierFixedTime), } validProvPredicateL2Controls := provenance.SourceProvenancePred{ - Controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneEarlier, reviewEnforcedEarlier}, // Missing provenanceAvailable for L3 + Controls: controlsForLevel(slsa.SlsaSourceLevel2, earlierFixedTime), } tests := []struct { @@ -240,7 +252,7 @@ func TestEvaluateSourceProv_Failure(t *testing.T) { policyContent: &policyL3ReviewTagsNow, // Expects L3 provenanceStatement: createStatementForTest(t, &validProvPredicateL2Controls, provenance.SourceProvPredicateType), // Prov only has L2 controls ghConnBranch: "main", - expectedErrorContains: "policy sets target level SLSA_SOURCE_LEVEL_3 which requires [CONTINUITY_ENFORCED TAG_HYGIENE PROVENANCE_AVAILABLE], but branch is only eligible for SLSA_SOURCE_LEVEL_2 because it only has [CONTINUITY_ENFORCED TAG_HYGIENE REVIEW_ENFORCED]", + expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_2", }, { name: "Malformed Policy JSON -> Error", @@ -333,9 +345,9 @@ func createVsaSummary(ref string, verifiedLevels []slsa.ControlName) *provenance func TestEvaluateTagProv_Success(t *testing.T) { // Controls for mock provenance - tagHygieneEarlier := provenance.Control{Name: slsa.TagHygiene.String(), Since: earlierFixedTime} + tagHygieneEarlier := provenance.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS.String(), Since: earlierFixedTime} origL2ReviewedSummary := createVsaSummary("refs/heads/orig", []slsa.ControlName{ - slsa.ControlName(slsa.SlsaSourceLevel2), slsa.ReviewEnforced, + slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, }) mainL3Summary := createVsaSummary("refs/heads/main", []slsa.ControlName{ slsa.ControlName(slsa.SlsaSourceLevel3), @@ -354,7 +366,7 @@ func TestEvaluateTagProv_Success(t *testing.T) { TagHygiene: true, }, vsaSummaries: []*provenance.VsaSummary{origL2ReviewedSummary}, - expectedLevels: slsa.SourceVerifiedLevels{slsa.ReviewEnforced, slsa.ControlName(slsa.SlsaSourceLevel2)}, + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, }, { name: "Policy has protected_tag setting, and multiple summaries", @@ -365,7 +377,7 @@ func TestEvaluateTagProv_Success(t *testing.T) { vsaSummaries: []*provenance.VsaSummary{origL2ReviewedSummary, mainL3Summary}, // The spec says we MUST NOT return multiple levels per track in a VSA... expectedLevels: slsa.SourceVerifiedLevels{ - slsa.ReviewEnforced, slsa.ControlName(slsa.SlsaSourceLevel3), + slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, }, }, { @@ -436,12 +448,9 @@ func TestEvaluateTagProv_Success(t *testing.T) { } func TestEvaluateControl_Success(t *testing.T) { - // Controls - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlierFixedTime} - provenanceAvailableEarlier := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: earlierFixedTime} - reviewEnforcedEarlier := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: earlierFixedTime} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlierFixedTime} orgTestControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_test", Since: earlierFixedTime} + reviewControl := &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW.String(), Since: earlierFixedTime} + l3ControlsWithExtras := controlsForLevelWith(slsa.SlsaSourceLevel3, earlierFixedTime, reviewControl, orgTestControl) // Policies fullPolicy := RepoPolicy{ @@ -483,33 +492,33 @@ func TestEvaluateControl_Success(t *testing.T) { name: "Commit time before policy Since -> SLSA Level 1", policyContent: &fullPolicy, controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: earlierFixedTime.AsTime(), // Commit time before policyL3ReviewTagsNow.Since (now) - Controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier, orgTestControl}, + CommitPushTime: earlierFixedTime.AsTime(), + Controls: l3ControlsWithExtras, }, ghConnBranch: "main", - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, // Expect L1 because commit time is before policy enforcement - expectedPolicyPath: "TEMP_POLICY_FILE_PATH", // Placeholder, will be replaced by actual temp file path + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, + expectedPolicyPath: "TEMP_POLICY_FILE_PATH", }, { name: "Commit time after policy Since, controls meet policy -> Expected levels", policyContent: &fullPolicy, controlStatus: &ghcontrol.GhControlStatus{ CommitPushTime: laterFixedTime.AsTime(), - Controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier, orgTestControl}, + Controls: l3ControlsWithExtras, }, ghConnBranch: "main", - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.ReviewEnforced, slsa.TagHygiene, "ORG_SOURCE_TESTED"}, + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, "ORG_SOURCE_TESTED"}, expectedPolicyPath: "TEMP_POLICY_FILE_PATH", }, { name: "Branch not in policy, commit after default policy since -> Default policy (SLSA L1)", - policyContent: &basicPolicy, // main is in policy, but we test "develop" + policyContent: &basicPolicy, controlStatus: &ghcontrol.GhControlStatus{ CommitPushTime: laterFixedTime.AsTime(), - Controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier, orgTestControl}, + Controls: l3ControlsWithExtras, }, - ghConnBranch: "develop", // Testing "develop" branch - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, // Default is L1 + ghConnBranch: "develop", + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, expectedPolicyPath: "DEFAULT", }, } @@ -556,10 +565,6 @@ func TestEvaluateControl_Failure(t *testing.T) { earlier := timestamppb.New(time.Now().Add(-time.Hour)) later := timestamppb.New(time.Now().Add(time.Hour)) - // Controls - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlier} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlier} - // Policies policyL3Review := RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ @@ -569,17 +574,17 @@ func TestEvaluateControl_Failure(t *testing.T) { tests := []struct { name string - policyContent any // RepoPolicy or string for malformed + policyContent any controlStatus *ghcontrol.GhControlStatus - ghConnBranch string // Branch for GitHub connection + ghConnBranch string expectedErrorContains string }{ { name: "Commit time after policy Since, controls DO NOT meet policy -> Error", - policyContent: &policyL3Review, // Requires L3, Review, Tags + policyContent: &policyL3Review, controlStatus: &ghcontrol.GhControlStatus{ CommitPushTime: later.AsTime(), - Controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneEarlier}, // Only meets L2 + Controls: controlsForLevel(slsa.SlsaSourceLevel2, earlier), }, ghConnBranch: "main", expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_2", @@ -710,40 +715,34 @@ const ( ) func TestComputeEligibleSlsaLevel(t *testing.T) { - continuityEnforcedControl := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: fixedTime} - provenanceAvailableControl := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: fixedTime} - reviewEnforcedControl := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: fixedTime} - tagHygieneControl := &provenance.Control{Name: slsa.TagHygiene.String(), Since: fixedTime} - tests := []struct { - name string - controls slsa.Controls - expectedLevel slsa.SlsaSourceLevel - expectedReason string + name string + controls slsa.Controls + expectedLevel slsa.SlsaSourceLevel }{ { name: "SLSA Level 4", - controls: slsa.Controls{continuityEnforcedControl, provenanceAvailableControl, reviewEnforcedControl, tagHygieneControl}, + controls: controlsForLevel(slsa.SlsaSourceLevel4, fixedTime), expectedLevel: slsa.SlsaSourceLevel4, }, { name: "SLSA Level 3", - controls: slsa.Controls{continuityEnforcedControl, provenanceAvailableControl, tagHygieneControl}, + controls: controlsForLevel(slsa.SlsaSourceLevel3, fixedTime), expectedLevel: slsa.SlsaSourceLevel3, }, { name: "SLSA Level 2", - controls: slsa.Controls{continuityEnforcedControl, tagHygieneControl}, + controls: controlsForLevel(slsa.SlsaSourceLevel2, fixedTime), expectedLevel: slsa.SlsaSourceLevel2, }, { - name: "SLSA Level 1 - ProvenanceAvailable only", - controls: slsa.Controls{provenanceAvailableControl}, + name: "SLSA Level 1 - partial controls only", + controls: slsa.Controls{&provenance.Control{Name: slsa.SLSA_SOURCE_SCS_CONTINUITY.String(), Since: fixedTime}}, expectedLevel: slsa.SlsaSourceLevel1, }, { - name: "SLSA Level 1 - ContinuityEnforced control absent", - controls: nil, // Represents absence of ContinuityEnforced; could also use slsa.Controls{} + name: "SLSA Level 1 - no controls", + controls: nil, expectedLevel: slsa.SlsaSourceLevel1, }, } @@ -759,12 +758,8 @@ func TestComputeEligibleSlsaLevel(t *testing.T) { } func TestEvaluateBranchControls(t *testing.T) { - // Controls - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlierFixedTime} - provenanceAvailableEarlier := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: earlierFixedTime} - reviewEnforcedEarlier := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: earlierFixedTime} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlierFixedTime} - tagHygieneNow := &provenance.Control{Name: slsa.TagHygiene.String(), Since: fixedTime} + reviewEarlier := &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW.String(), Since: earlierFixedTime} + tagHygieneNow := &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS.String(), Since: fixedTime} // Branch Policies policyL3Review := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, Since: fixedTime} @@ -793,15 +788,15 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Success - L3, Review, Tags", branchPolicy: &policyL3Review, tagPolicy: &tagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.ReviewEnforced, slsa.TagHygiene}, + controls: controlsForLevelWith(slsa.SlsaSourceLevel3, earlierFixedTime, reviewEarlier), + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, { name: "Success - L1", branchPolicy: &policyL1NoExtras, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{}, // L1 is met by default if policy targets L1 and other conditions pass + controls: controlsForLevel(slsa.SlsaSourceLevel1, earlierFixedTime), expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, expectError: false, }, @@ -809,59 +804,61 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Success - L2 & Review", branchPolicy: &policyL2Review, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, // Provenance not needed for L2 - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.ReviewEnforced}, + controls: controlsForLevelWith(slsa.SlsaSourceLevel2, earlierFixedTime, reviewEarlier), + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, expectError: false, }, { - name: "Success - L2 & Tags", - branchPolicy: &policyL2NoReview, - tagPolicy: &tagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneEarlier}, // Provenance not needed for L2 - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.TagHygiene}, + name: "Success - L2 & Tags", + branchPolicy: &policyL2NoReview, + tagPolicy: &tagHygienePolicy, + controls: controlsForLevelWith(slsa.SlsaSourceLevel2, earlierFixedTime, + &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS.String(), Since: earlierFixedTime}, + ), + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, { name: "Success - L4", branchPolicy: &policyL4, tagPolicy: &tagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel4), slsa.TagHygiene}, + controls: controlsForLevel(slsa.SlsaSourceLevel4, earlierFixedTime), + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel4), slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, { name: "Error - computeSlsaLevel Fails (Policy L3, Controls L1)", - branchPolicy: &policyL3Review, // Wants L3 + branchPolicy: &policyL3Review, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{}, // Only eligible for L1 + controls: slsa.Controls{}, expectedLevels: slsa.SourceVerifiedLevels{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", }, { name: "Error - computeReviewEnforced Fails (Policy L2+Review, Review control missing)", - branchPolicy: &policyL2Review, // Wants L2 & Review + branchPolicy: &policyL2Review, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneEarlier}, // Eligible for L2, but Review control missing + controls: controlsForLevel(slsa.SlsaSourceLevel2, earlierFixedTime), expectedLevels: slsa.SourceVerifiedLevels{}, expectError: true, expectedErrorContains: "policy requires review, but that control is not enabled", }, { name: "Error - computeTagHygiene Fails (Policy L1+Tags, Tag control Since later than Policy Since)", - branchPolicy: &policyL1Earlier, // Wants L1 & Tags, Policy.Since = earlier + branchPolicy: &policyL1Earlier, tagPolicy: &ProtectedTag{Since: earlierFixedTime, TagHygiene: true}, - controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneNow}, // Eligible L1, Tag.Since = now + controls: controlsForLevelWith(slsa.SlsaSourceLevel1, earlierFixedTime, tagHygieneNow), expectedLevels: slsa.SourceVerifiedLevels{}, expectError: true, - expectedErrorContains: "policy requires tag hygiene since", // ... but that control has only been enabled since ... + expectedErrorContains: "policy requires tag hygiene since", }, { name: "Success - Mixed Requirements (L3, Review, No Tags)", branchPolicy: &policyL3Review, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.ReviewEnforced}, + controls: controlsForLevelWith(slsa.SlsaSourceLevel3, earlierFixedTime, reviewEarlier), + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, expectError: false, }, } @@ -918,7 +915,7 @@ func TestComputeTagHygiene(t *testing.T) { policyNotRequiresTagHygiene := ProtectedTag{TagHygiene: false, Since: now} // Controls - tagHygieneControlEnabledNow := &provenance.Control{Name: slsa.TagHygiene.String(), Since: now} + tagHygieneControlEnabledNow := &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS.String(), Since: now} tests := []struct { name string @@ -932,7 +929,7 @@ func TestComputeTagHygiene(t *testing.T) { name: "Policy requires tag hygiene, control compliant (Policy.Since >= Control.Since)", tagPolicy: &policyRequiresTagHygieneNow, controls: slsa.Controls{tagHygieneControlEnabledNow}, // Policy.Since == Control.Since - expectedControls: []slsa.ControlName{slsa.TagHygiene}, + expectedControls: []slsa.ControlName{slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, { @@ -993,7 +990,7 @@ func TestComputeReviewEnforced(t *testing.T) { policyNotRequiresReview := ProtectedBranch{RequireReview: false, Since: now} // Controls - reviewControlEnabledNow := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: now} + reviewControlEnabledNow := &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW.String(), Since: now} tests := []struct { name string @@ -1007,7 +1004,7 @@ func TestComputeReviewEnforced(t *testing.T) { name: "Policy requires review, control compliant (Policy.Since >= Control.Since)", branchPolicy: &policyRequiresReviewNow, controls: slsa.Controls{reviewControlEnabledNow}, // Policy.Since == Control.Since - expectedControls: []slsa.ControlName{slsa.ReviewEnforced}, + expectedControls: []slsa.ControlName{slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, expectError: false, }, { @@ -1156,16 +1153,6 @@ func TestComputeSlsaLevel(t *testing.T) { now := timestamppb.Now() earlier := timestamppb.New(time.Now().Add(-time.Hour)) - // Controls - continuityEnforcedNow := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: now} - provenanceAvailableNow := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: now} - reviewEnforcedNow := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: now} - tagHygieneNow := &provenance.Control{Name: slsa.TagHygiene.String(), Since: now} - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlier} - provenanceAvailableEarlier := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: earlier} - reviewEnforcedEarlier := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: earlier} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlier} - // Branch Policies policyL4Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: now} policyL3Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: now} @@ -1183,44 +1170,44 @@ func TestComputeSlsaLevel(t *testing.T) { { name: "Controls L4-eligible (since 'earlier'), Policy L4 (since 'now'): success", branchPolicy: &policyL4Now, - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, + controls: controlsForLevel(slsa.SlsaSourceLevel4, earlier), expectedLevels: []slsa.ControlName{slsa.ControlName(slsa.SlsaSourceLevel4)}, expectError: false, }, { name: "Controls L3-eligible (since 'earlier'), Policy L2 (since 'now'): success", - branchPolicy: &policyL2Now, // Policy L2, Since 'now' - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, tagHygieneEarlier}, // Eligible L3 since 'earlier' + branchPolicy: &policyL2Now, + controls: controlsForLevel(slsa.SlsaSourceLevel3, earlier), expectedLevels: []slsa.ControlName{slsa.ControlName(slsa.SlsaSourceLevel2)}, expectError: false, }, { name: "Controls L1-eligible, Policy L2: fail (eligibility)", - branchPolicy: &policyL2Now, // Policy L2 - controls: slsa.Controls{}, // Eligible L1 + branchPolicy: &policyL2Now, + controls: slsa.Controls{}, expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", }, { name: "Eligible L3 (since 'earlier'), Policy L3 (since 'now'): compliant Policy.Since", - branchPolicy: &policyL3Now, // Policy L3, Since 'now' - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, tagHygieneEarlier}, // Eligible L3 since 'earlier' - expectedLevels: []slsa.ControlName{slsa.ControlName(slsa.SlsaSourceLevel3)}, // Policy.Since ('now') is not before EligibleSince ('earlier') + branchPolicy: &policyL3Now, + controls: controlsForLevel(slsa.SlsaSourceLevel3, earlier), + expectedLevels: []slsa.ControlName{slsa.ControlName(slsa.SlsaSourceLevel3)}, expectError: false, }, { name: "Controls L4-eligible (since 'now'), Policy L4 (since 'earlier'): fail (Policy.Since too early)", branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: earlier}, - controls: slsa.Controls{continuityEnforcedNow, provenanceAvailableNow, reviewEnforcedNow, tagHygieneNow}, + controls: controlsForLevel(slsa.SlsaSourceLevel4, now), expectedLevels: []slsa.ControlName{}, expectError: true, - expectedErrorContains: "policy sets target level SLSA_SOURCE_LEVEL_4 since", // ...but it has only been eligible for that level since... + expectedErrorContains: "policy sets target level SLSA_SOURCE_LEVEL_4 since", }, { name: "Controls L3-eligible (since 'now'), Policy L3 (since 'earlier'): fail (Policy.Since too early)", - branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: earlier}, // Policy L3, Since 'earlier' - controls: slsa.Controls{continuityEnforcedNow, provenanceAvailableNow, tagHygieneNow}, // Eligible L3 since 'now' + branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: earlier}, + controls: controlsForLevel(slsa.SlsaSourceLevel3, now), expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy sets target level SLSA_SOURCE_LEVEL_3 since", @@ -1228,27 +1215,23 @@ func TestComputeSlsaLevel(t *testing.T) { { name: "Controls L2-eligible (since 'now'), Policy L2 (since 'earlier'): fail (Policy.Since too early)", branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), Since: earlier}, - controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneNow}, + controls: controlsForLevel(slsa.SlsaSourceLevel2, now), expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy sets target level SLSA_SOURCE_LEVEL_2 since", }, { name: "Policy L?'UNKNOWN' (controls L3-eligible): fail (policy target unknown)", - branchPolicy: &policyUnknownLevel, // Policy "UNKNOWN_LEVEL" - controls: slsa.Controls{continuityEnforcedNow, provenanceAvailableNow}, // Eligible L3 + branchPolicy: &policyUnknownLevel, + controls: controlsForLevel(slsa.SlsaSourceLevel3, now), expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy sets target level UNKNOWN_LEVEL", }, - // This single case covers eligibility failure where target > eligible. - // It replaces the two previous similar cases: - // "computeEligibleSince returns nil (controls insufficient for target level)" which was L2 controls for L3 policy - // "Controls for L1, Policy L3, computeEligibleSince for L3 returns nil" which was L1 controls for L3 policy { name: "Controls L1-eligible, Policy L3: fail (eligibility)", - branchPolicy: &policyL3Now, // Policy L3 - controls: slsa.Controls{}, // Eligible L1 + branchPolicy: &policyL3Now, + controls: slsa.Controls{}, expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", @@ -1278,22 +1261,23 @@ func TestComputeSlsaLevel(t *testing.T) { } } +// controlsForLevelWithOverride creates controls for a level, then overrides the +// Since timestamp for a specific control name. +func controlsForLevelWithOverride(level slsa.SlsaSourceLevel, baseSince *timestamppb.Timestamp, overrides map[slsa.ControlName]*timestamppb.Timestamp) slsa.Controls { + controls := controlsForLevel(level, baseSince) + for i, c := range controls { + if since, ok := overrides[slsa.ControlName(c.GetName())]; ok { + controls[i] = &provenance.Control{Name: c.GetName(), Since: since} + } + } + return controls +} + func TestComputeEligibleSince(t *testing.T) { time1 := timestamppb.Now() time2 := timestamppb.New(time.Now().Add(time.Hour)) zeroTime := timestamppb.New(time.Time{}) - continuityEnforcedT1 := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: time1} - provenanceAvailableT1 := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: time1} - reviewEnforcedT1 := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: time1} - tagHygieneT1 := &provenance.Control{Name: slsa.TagHygiene.String(), Since: time1} - continuityEnforcedT2 := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: time2} - provenanceAvailableT2 := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: time2} - reviewEnforcedT2 := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: time2} - tagHygieneZero := &provenance.Control{Name: slsa.TagHygiene.String(), Since: zeroTime} - continuityEnforcedZero := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: zeroTime} - provenanceAvailableZero := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: zeroTime} - tests := []struct { name string controls slsa.Controls @@ -1303,57 +1287,66 @@ func TestComputeEligibleSince(t *testing.T) { expectedError string }{ { - name: "L4 eligible (prov, review later)", - controls: slsa.Controls{continuityEnforcedT1, provenanceAvailableT2, reviewEnforcedT2, tagHygieneT1}, + name: "L4 eligible (two controls later)", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel4, time1, map[slsa.ControlName]*timestamppb.Timestamp{ + slsa.SLSA_SOURCE_SCS_PROVENANCE: time2, + slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW: time2, + }), level: slsa.SlsaSourceLevel4, expectedTime: time2, expectError: false, }, { - name: "L4 eligible (continuity later)", - controls: slsa.Controls{continuityEnforcedT2, provenanceAvailableT1, reviewEnforcedT1, tagHygieneT1}, + name: "L4 eligible (one control later)", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel4, time1, map[slsa.ControlName]*timestamppb.Timestamp{ + slsa.SLSA_SOURCE_SCS_CONTINUITY: time2, + }), level: slsa.SlsaSourceLevel4, expectedTime: time2, expectError: false, }, { - name: "L3 eligible (ProvLater), L3 requested: expect Prov.Since", - controls: slsa.Controls{continuityEnforcedT1, provenanceAvailableT2, tagHygieneT1}, + name: "L3 eligible (ProvLater), L3 requested: expect Prov.Since", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, time1, map[slsa.ControlName]*timestamppb.Timestamp{ + slsa.SLSA_SOURCE_SCS_PROVENANCE: time2, + }), level: slsa.SlsaSourceLevel3, expectedTime: time2, expectError: false, }, { - name: "L3 eligible (ContLater), L3 requested: expect Cont.Since", - controls: slsa.Controls{continuityEnforcedT2, provenanceAvailableT1, tagHygieneT1}, + name: "L3 eligible (ContLater), L3 requested: expect Cont.Since", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, time1, map[slsa.ControlName]*timestamppb.Timestamp{ + slsa.SLSA_SOURCE_SCS_CONTINUITY: time2, + }), level: slsa.SlsaSourceLevel3, expectedTime: time2, expectError: false, }, { - name: "L2 eligible (Cont&HygieneOnly), L2 requested: expect Cont.Since", // Was: "Eligible for SLSA Level 2" - controls: slsa.Controls{continuityEnforcedT1, tagHygieneT1}, + name: "L2 eligible, L2 requested: expect base time", + controls: controlsForLevel(slsa.SlsaSourceLevel2, time1), level: slsa.SlsaSourceLevel2, expectedTime: time1, expectError: false, }, { - name: "L1 eligible (NoControls), L1 requested: expect ZeroTime", // Was: "Eligible for SLSA Level 1" - controls: slsa.Controls{}, + name: "L1 eligible, L1 requested: expect ZeroTime", + controls: controlsForLevel(slsa.SlsaSourceLevel1, zeroTime), level: slsa.SlsaSourceLevel1, expectedTime: zeroTime, expectError: false, }, { - name: "L3 eligible, L2 requested: expect Cont.Since", - controls: slsa.Controls{continuityEnforcedT1, provenanceAvailableT2, tagHygieneT1}, + name: "L3 eligible, L2 requested: expect base time", + controls: controlsForLevel(slsa.SlsaSourceLevel3, time1), level: slsa.SlsaSourceLevel2, expectedTime: time1, expectError: false, }, { name: "L2 eligible, L3 requested: expect nil, no error", - controls: slsa.Controls{continuityEnforcedT1, tagHygieneT1}, + controls: controlsForLevel(slsa.SlsaSourceLevel2, time1), level: slsa.SlsaSourceLevel3, expectedTime: nil, expectError: false, @@ -1366,22 +1359,26 @@ func TestComputeEligibleSince(t *testing.T) { expectError: false, }, { - name: "L3 eligible (ContZero, ProvNonZero, TagNoZero), L3 requested: expect Prov.Since", - controls: slsa.Controls{continuityEnforcedZero, provenanceAvailableT2, tagHygieneT1}, + name: "L3 eligible (ContZero, ProvNonZero), L3 requested: expect Prov.Since", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, zeroTime, map[slsa.ControlName]*timestamppb.Timestamp{ + slsa.SLSA_SOURCE_SCS_PROVENANCE: time2, + }), level: slsa.SlsaSourceLevel3, expectedTime: time2, expectError: false, }, { - name: "L3 eligible (ContNonZero, ProvZero, TagNoZero), L3 requested: expect Cont.Since", - controls: slsa.Controls{continuityEnforcedT1, provenanceAvailableZero, tagHygieneT1}, + name: "L3 eligible (ContNonZero, ProvZero), L3 requested: expect Cont.Since", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, zeroTime, map[slsa.ControlName]*timestamppb.Timestamp{ + slsa.SLSA_SOURCE_SCS_CONTINUITY: time1, + }), level: slsa.SlsaSourceLevel3, expectedTime: time1, expectError: false, }, { name: "L3 eligible (AllZero), L3 requested: expect ZeroTime", - controls: slsa.Controls{continuityEnforcedZero, provenanceAvailableZero, tagHygieneZero}, + controls: controlsForLevel(slsa.SlsaSourceLevel3, zeroTime), level: slsa.SlsaSourceLevel3, expectedTime: zeroTime, expectError: false, diff --git a/pkg/sourcetool/tool_test.go b/pkg/sourcetool/tool_test.go index 008bbac..ae3c687 100644 --- a/pkg/sourcetool/tool_test.go +++ b/pkg/sourcetool/tool_test.go @@ -26,7 +26,7 @@ func TestGetBranchControls(t *testing.T) { Branch: "main", Controls: []slsa.ControlStatus{ { - Name: slsa.ContinuityEnforced, + Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, State: slsa.StateNotEnabled, Message: "Continuity enforced", }, From 278862d2a5ee690f6cd935bb872d94a811446e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Thu, 5 Mar 2026 17:50:51 -0600 Subject: [PATCH 04/41] Streamline GetBranchControls API in tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/checklevel.go | 3 +++ internal/cmd/status.go | 2 +- pkg/policy/policy.go | 4 +++- pkg/sourcetool/implementation.go | 6 ++--- .../fake_tool_implementation.go | 22 +++++++++---------- pkg/sourcetool/tool.go | 14 +++++++----- pkg/sourcetool/tool_test.go | 6 ++--- 7 files changed, 32 insertions(+), 25 deletions(-) diff --git a/internal/cmd/checklevel.go b/internal/cmd/checklevel.go index 9e845aa..ccc44a4 100644 --- a/internal/cmd/checklevel.go +++ b/internal/cmd/checklevel.go @@ -87,6 +87,9 @@ func doCheckLevel(cla *checkLevelOpts) error { if err != nil { return err } + + fmt.Printf("Controls salieron:\n%v\n", controlStatus) + pe := policy.NewPolicyEvaluator() pe.UseLocalPolicy = cla.useLocalPolicy verifiedLevels, policyPath, err := pe.EvaluateControl(ctx, cla.GetRepository(), cla.GetBranch(), controlStatus) diff --git a/internal/cmd/status.go b/internal/cmd/status.go index 8adc923..270edb7 100644 --- a/internal/cmd/status.go +++ b/internal/cmd/status.go @@ -110,7 +110,7 @@ sourcetool status myorg/myrepo@mybranch } // Get the active repository controls - controls, err := srctool.GetBranchControls(cmd.Context(), opts.GetRepository(), opts.GetBranch()) + controls, err := srctool.GetBranchControls(cmd.Context(), opts.GetBranch()) if err != nil { return fmt.Errorf("fetching active controls: %w", err) } diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index ccf3bbb..3fa430c 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -363,7 +363,9 @@ func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls s "policy sets target level %s which requires %v, but branch is only eligible for %s because it only has %v", branchPolicy.GetTargetSlsaSourceLevel(), slsa.GetRequiredControlsForLevel(slsa.SlsaSourceLevel(branchPolicy.GetTargetSlsaSourceLevel())), - eligibleLevel, controls.Names()) + eligibleLevel, + controls.Names(), + ) } // Check to see when this branch became eligible for the current target level. diff --git a/pkg/sourcetool/implementation.go b/pkg/sourcetool/implementation.go index ecc519c..428beaa 100644 --- a/pkg/sourcetool/implementation.go +++ b/pkg/sourcetool/implementation.go @@ -37,7 +37,7 @@ type toolImplementation interface { SearchPullRequest(context.Context, *auth.Authenticator, *models.Repository, string) (*models.PullRequest, error) GetVcsBackend(*models.Repository) (models.VcsBackend, error) GetAttestationReader(*models.Repository) (models.AttestationStorageReader, error) - GetBranchControls(context.Context, models.VcsBackend, *models.Repository, *models.Branch) (*slsa.ControlSetStatus, error) + GetBranchControls(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSetStatus, error) ConfigureControls(models.VcsBackend, *models.Repository, []*models.Branch, []models.ControlConfiguration) error GetPolicyStatus(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.ControlStatus, error) CreateRepositoryFork(context.Context, *auth.Authenticator, *models.Repository, string) error @@ -53,9 +53,9 @@ func (impl *defaultToolImplementation) ConfigureControls( } func (impl *defaultToolImplementation) GetBranchControls( - ctx context.Context, backend models.VcsBackend, r *models.Repository, branch *models.Branch, + ctx context.Context, backend models.VcsBackend, branch *models.Branch, ) (*slsa.ControlSetStatus, error) { - return backend.GetBranchControls(ctx, r, branch) + return backend.GetBranchControls(ctx, branch.Repository, branch) } // GetAttestationReader returns the att reader object diff --git a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go index d6e6457..4762a97 100644 --- a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go +++ b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go @@ -92,13 +92,12 @@ type FakeToolImplementation struct { result1 models.AttestationStorageReader result2 error } - GetBranchControlsStub func(context.Context, models.VcsBackend, *models.Repository, *models.Branch) (*slsa.ControlSetStatus, error) + GetBranchControlsStub func(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSetStatus, error) getBranchControlsMutex sync.RWMutex getBranchControlsArgsForCall []struct { arg1 context.Context arg2 models.VcsBackend - arg3 *models.Repository - arg4 *models.Branch + arg3 *models.Branch } getBranchControlsReturns struct { result1 *slsa.ControlSetStatus @@ -560,21 +559,20 @@ func (fake *FakeToolImplementation) GetAttestationReaderReturnsOnCall(i int, res }{result1, result2} } -func (fake *FakeToolImplementation) GetBranchControls(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Repository, arg4 *models.Branch) (*slsa.ControlSetStatus, error) { +func (fake *FakeToolImplementation) GetBranchControls(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Branch) (*slsa.ControlSetStatus, error) { fake.getBranchControlsMutex.Lock() ret, specificReturn := fake.getBranchControlsReturnsOnCall[len(fake.getBranchControlsArgsForCall)] fake.getBranchControlsArgsForCall = append(fake.getBranchControlsArgsForCall, struct { arg1 context.Context arg2 models.VcsBackend - arg3 *models.Repository - arg4 *models.Branch - }{arg1, arg2, arg3, arg4}) + arg3 *models.Branch + }{arg1, arg2, arg3}) stub := fake.GetBranchControlsStub fakeReturns := fake.getBranchControlsReturns - fake.recordInvocation("GetBranchControls", []interface{}{arg1, arg2, arg3, arg4}) + fake.recordInvocation("GetBranchControls", []interface{}{arg1, arg2, arg3}) fake.getBranchControlsMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1, ret.result2 @@ -588,17 +586,17 @@ func (fake *FakeToolImplementation) GetBranchControlsCallCount() int { return len(fake.getBranchControlsArgsForCall) } -func (fake *FakeToolImplementation) GetBranchControlsCalls(stub func(context.Context, models.VcsBackend, *models.Repository, *models.Branch) (*slsa.ControlSetStatus, error)) { +func (fake *FakeToolImplementation) GetBranchControlsCalls(stub func(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSetStatus, error)) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = stub } -func (fake *FakeToolImplementation) GetBranchControlsArgsForCall(i int) (context.Context, models.VcsBackend, *models.Repository, *models.Branch) { +func (fake *FakeToolImplementation) GetBranchControlsArgsForCall(i int) (context.Context, models.VcsBackend, *models.Branch) { fake.getBranchControlsMutex.RLock() defer fake.getBranchControlsMutex.RUnlock() argsForCall := fake.getBranchControlsArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } func (fake *FakeToolImplementation) GetBranchControlsReturns(result1 *slsa.ControlSetStatus, result2 error) { diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index 300d74c..96851b8 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -52,21 +52,25 @@ type Tool struct { } // GetRepoControls returns the controls that are enabled in a repository branch. -func (t *Tool) GetBranchControls(ctx context.Context, r *models.Repository, branch *models.Branch) (*slsa.ControlSetStatus, error) { - backend, err := t.impl.GetVcsBackend(r) +func (t *Tool) GetBranchControls(ctx context.Context, branch *models.Branch) (*slsa.ControlSetStatus, error) { + if branch.Repository == nil { + return nil, fmt.Errorf("repositoryu not specified in branch") + } + + backend, err := t.impl.GetVcsBackend(branch.Repository) if err != nil { return nil, fmt.Errorf("getting VCS backend: %w", err) } // Get the control status in the branch. Backends are expected to // return the full SLSA Source control catalog - controls, err := t.impl.GetBranchControls(ctx, backend, r, branch) + controls, err := t.impl.GetBranchControls(ctx, backend, branch) if err != nil { return nil, fmt.Errorf("getting branch controls: %w", err) } // We also abstract the repository policy as a control to report its status - status, err := t.impl.GetPolicyStatus(ctx, t.Authenticator, &t.Options, r) + status, err := t.impl.GetPolicyStatus(ctx, t.Authenticator, &t.Options, branch.Repository) if err != nil { return nil, fmt.Errorf("reading policy status: %w", err) } @@ -185,7 +189,7 @@ func (t *Tool) CreateBranchPolicy(ctx context.Context, r *models.Repository, bra return nil, fmt.Errorf("getting backend: %w", err) } - controls, err := t.impl.GetBranchControls(ctx, backend, r, branches[0]) + controls, err := t.impl.GetBranchControls(ctx, backend, branches[0]) if err != nil { return nil, fmt.Errorf("getting branch controls: %w", err) } diff --git a/pkg/sourcetool/tool_test.go b/pkg/sourcetool/tool_test.go index ae3c687..3892722 100644 --- a/pkg/sourcetool/tool_test.go +++ b/pkg/sourcetool/tool_test.go @@ -35,11 +35,11 @@ func TestGetBranchControls(t *testing.T) { tool := &Tool{ impl: i, } - res, err := tool.GetBranchControls(t.Context(), &models.Repository{}, &models.Branch{}) + res, err := tool.GetBranchControls(t.Context(), &models.Branch{Repository: &models.Repository{}}) + require.NoError(t, err) require.NotNil(t, res) // This always has one more as we add the synyhetic policy check require.Len(t, res.Controls, 2) - require.NoError(t, err) }) t.Run("GetActiveControls-fails", func(t *testing.T) { t.Parallel() @@ -48,7 +48,7 @@ func TestGetBranchControls(t *testing.T) { tool := &Tool{ impl: i, } - _, err := tool.GetBranchControls(t.Context(), &models.Repository{}, &models.Branch{}) + _, err := tool.GetBranchControls(t.Context(), &models.Branch{}) require.Error(t, err) }) } From 22acaa208ad8661c8cdc6491180e2161b61fa7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Thu, 5 Mar 2026 17:55:13 -0600 Subject: [PATCH 05/41] Streamline get controls on backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/sourcetool/backends/vcs/github/github.go | 17 +++++-- pkg/sourcetool/implementation.go | 2 +- pkg/sourcetool/models/models.go | 4 +- .../models/modelsfakes/fake_vcs_backend.go | 48 +++++++++---------- 4 files changed, 37 insertions(+), 34 deletions(-) diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index ef2245c..68b6df0 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -107,7 +107,10 @@ func (b *Backend) getGitHubConnection(repository *models.Repository, ref string) return ghcontrol.NewGhConnectionWithClient(owner, name, ref, client), nil } -func (b *Backend) GetBranchControls(ctx context.Context, r *models.Repository, branch *models.Branch) (*slsa.ControlSetStatus, error) { +func (b *Backend) GetBranchControls(ctx context.Context, branch *models.Branch) (*slsa.ControlSetStatus, error) { + if branch.Repository == nil { + return nil, fmt.Errorf("branch has no repository") + } // get latest commit ghc, err := b.getGitHubConnection(branch.Repository, branch.FullRef()) if err != nil { @@ -120,11 +123,15 @@ func (b *Backend) GetBranchControls(ctx context.Context, r *models.Repository, b return nil, fmt.Errorf("fetching latest commit from %q: %w", branch.FullRef(), err) } - return b.GetBranchControlsAtCommit(ctx, r, branch, &models.Commit{SHA: commit}) + return b.GetBranchControlsAtCommit(ctx, branch, &models.Commit{SHA: commit}) } // GetBranchControlsAtCommit -func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repository, branch *models.Branch, commit *models.Commit) (*slsa.ControlSetStatus, error) { +func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*slsa.ControlSetStatus, error) { + if branch.Repository == nil { + return nil, fmt.Errorf("branch has no repository") + } + if commit == nil { return nil, errors.New("commit is not set") } @@ -213,7 +220,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos switchProvCtlToInProgress := false var provenanceMessage string if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_PROVENANCE); c == nil { - pr, err := b.FindWorkflowPR(ctx, r) + pr, err := b.FindWorkflowPR(ctx, branch.Repository) if err != nil { return nil, fmt.Errorf("looking for provenance workflow pull request: %w", err) } @@ -232,7 +239,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos status.Controls[i].State = slsa.StateInProgress status.Controls[i].Message = provenanceMessage } - action := b.getRecommendedAction(r, branch, status.Controls[i].Name, status.Controls[i].State) + action := b.getRecommendedAction(branch.Repository, branch, status.Controls[i].Name, status.Controls[i].State) status.Controls[i].RecommendedAction = action } diff --git a/pkg/sourcetool/implementation.go b/pkg/sourcetool/implementation.go index 428beaa..18b639b 100644 --- a/pkg/sourcetool/implementation.go +++ b/pkg/sourcetool/implementation.go @@ -55,7 +55,7 @@ func (impl *defaultToolImplementation) ConfigureControls( func (impl *defaultToolImplementation) GetBranchControls( ctx context.Context, backend models.VcsBackend, branch *models.Branch, ) (*slsa.ControlSetStatus, error) { - return backend.GetBranchControls(ctx, branch.Repository, branch) + return backend.GetBranchControls(ctx, branch) } // GetAttestationReader returns the att reader object diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index bc11404..5805745 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -39,8 +39,8 @@ type AttestationStorageReader interface { // //counterfeiter:generate . VcsBackend type VcsBackend interface { - GetBranchControls(context.Context, *Repository, *Branch) (*slsa.ControlSetStatus, error) - GetBranchControlsAtCommit(context.Context, *Repository, *Branch, *Commit) (*slsa.ControlSetStatus, error) + GetBranchControls(context.Context, *Branch) (*slsa.ControlSetStatus, error) + GetBranchControlsAtCommit(context.Context, *Branch, *Commit) (*slsa.ControlSetStatus, error) GetTagControls(context.Context, *Tag) (*slsa.Controls, error) ControlConfigurationDescr(*Branch, ControlConfiguration) string ConfigureControls(*Repository, []*Branch, []ControlConfiguration) error diff --git a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go index 8528ebd..0543dba 100644 --- a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go +++ b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go @@ -54,12 +54,11 @@ type FakeVcsBackend struct { result3 models.ControlPreRemediationFn result4 error } - GetBranchControlsStub func(context.Context, *models.Repository, *models.Branch) (*slsa.ControlSetStatus, error) + GetBranchControlsStub func(context.Context, *models.Branch) (*slsa.ControlSetStatus, error) getBranchControlsMutex sync.RWMutex getBranchControlsArgsForCall []struct { arg1 context.Context - arg2 *models.Repository - arg3 *models.Branch + arg2 *models.Branch } getBranchControlsReturns struct { result1 *slsa.ControlSetStatus @@ -69,13 +68,12 @@ type FakeVcsBackend struct { result1 *slsa.ControlSetStatus result2 error } - GetBranchControlsAtCommitStub func(context.Context, *models.Repository, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error) + GetBranchControlsAtCommitStub func(context.Context, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error) getBranchControlsAtCommitMutex sync.RWMutex getBranchControlsAtCommitArgsForCall []struct { arg1 context.Context - arg2 *models.Repository - arg3 *models.Branch - arg4 *models.Commit + arg2 *models.Branch + arg3 *models.Commit } getBranchControlsAtCommitReturns struct { result1 *slsa.ControlSetStatus @@ -330,20 +328,19 @@ func (fake *FakeVcsBackend) ControlPrecheckReturnsOnCall(i int, result1 bool, re }{result1, result2, result3, result4} } -func (fake *FakeVcsBackend) GetBranchControls(arg1 context.Context, arg2 *models.Repository, arg3 *models.Branch) (*slsa.ControlSetStatus, error) { +func (fake *FakeVcsBackend) GetBranchControls(arg1 context.Context, arg2 *models.Branch) (*slsa.ControlSetStatus, error) { fake.getBranchControlsMutex.Lock() ret, specificReturn := fake.getBranchControlsReturnsOnCall[len(fake.getBranchControlsArgsForCall)] fake.getBranchControlsArgsForCall = append(fake.getBranchControlsArgsForCall, struct { arg1 context.Context - arg2 *models.Repository - arg3 *models.Branch - }{arg1, arg2, arg3}) + arg2 *models.Branch + }{arg1, arg2}) stub := fake.GetBranchControlsStub fakeReturns := fake.getBranchControlsReturns - fake.recordInvocation("GetBranchControls", []interface{}{arg1, arg2, arg3}) + fake.recordInvocation("GetBranchControls", []interface{}{arg1, arg2}) fake.getBranchControlsMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3) + return stub(arg1, arg2) } if specificReturn { return ret.result1, ret.result2 @@ -357,17 +354,17 @@ func (fake *FakeVcsBackend) GetBranchControlsCallCount() int { return len(fake.getBranchControlsArgsForCall) } -func (fake *FakeVcsBackend) GetBranchControlsCalls(stub func(context.Context, *models.Repository, *models.Branch) (*slsa.ControlSetStatus, error)) { +func (fake *FakeVcsBackend) GetBranchControlsCalls(stub func(context.Context, *models.Branch) (*slsa.ControlSetStatus, error)) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = stub } -func (fake *FakeVcsBackend) GetBranchControlsArgsForCall(i int) (context.Context, *models.Repository, *models.Branch) { +func (fake *FakeVcsBackend) GetBranchControlsArgsForCall(i int) (context.Context, *models.Branch) { fake.getBranchControlsMutex.RLock() defer fake.getBranchControlsMutex.RUnlock() argsForCall := fake.getBranchControlsArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 + return argsForCall.arg1, argsForCall.arg2 } func (fake *FakeVcsBackend) GetBranchControlsReturns(result1 *slsa.ControlSetStatus, result2 error) { @@ -396,21 +393,20 @@ func (fake *FakeVcsBackend) GetBranchControlsReturnsOnCall(i int, result1 *slsa. }{result1, result2} } -func (fake *FakeVcsBackend) GetBranchControlsAtCommit(arg1 context.Context, arg2 *models.Repository, arg3 *models.Branch, arg4 *models.Commit) (*slsa.ControlSetStatus, error) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommit(arg1 context.Context, arg2 *models.Branch, arg3 *models.Commit) (*slsa.ControlSetStatus, error) { fake.getBranchControlsAtCommitMutex.Lock() ret, specificReturn := fake.getBranchControlsAtCommitReturnsOnCall[len(fake.getBranchControlsAtCommitArgsForCall)] fake.getBranchControlsAtCommitArgsForCall = append(fake.getBranchControlsAtCommitArgsForCall, struct { arg1 context.Context - arg2 *models.Repository - arg3 *models.Branch - arg4 *models.Commit - }{arg1, arg2, arg3, arg4}) + arg2 *models.Branch + arg3 *models.Commit + }{arg1, arg2, arg3}) stub := fake.GetBranchControlsAtCommitStub fakeReturns := fake.getBranchControlsAtCommitReturns - fake.recordInvocation("GetBranchControlsAtCommit", []interface{}{arg1, arg2, arg3, arg4}) + fake.recordInvocation("GetBranchControlsAtCommit", []interface{}{arg1, arg2, arg3}) fake.getBranchControlsAtCommitMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1, ret.result2 @@ -424,17 +420,17 @@ func (fake *FakeVcsBackend) GetBranchControlsAtCommitCallCount() int { return len(fake.getBranchControlsAtCommitArgsForCall) } -func (fake *FakeVcsBackend) GetBranchControlsAtCommitCalls(stub func(context.Context, *models.Repository, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error)) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommitCalls(stub func(context.Context, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error)) { fake.getBranchControlsAtCommitMutex.Lock() defer fake.getBranchControlsAtCommitMutex.Unlock() fake.GetBranchControlsAtCommitStub = stub } -func (fake *FakeVcsBackend) GetBranchControlsAtCommitArgsForCall(i int) (context.Context, *models.Repository, *models.Branch, *models.Commit) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommitArgsForCall(i int) (context.Context, *models.Branch, *models.Commit) { fake.getBranchControlsAtCommitMutex.RLock() defer fake.getBranchControlsAtCommitMutex.RUnlock() argsForCall := fake.getBranchControlsAtCommitArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } func (fake *FakeVcsBackend) GetBranchControlsAtCommitReturns(result1 *slsa.ControlSetStatus, result2 error) { From 7f64f4acc5d5ee1dd5a25ab846f20490845011c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Thu, 5 Mar 2026 17:58:46 -0600 Subject: [PATCH 06/41] Add GetBranchControlsAtCommit to tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/sourcetool/implementation.go | 7 ++ .../fake_tool_implementation.go | 83 +++++++++++++++++++ pkg/sourcetool/tool.go | 29 +++++++ 3 files changed, 119 insertions(+) diff --git a/pkg/sourcetool/implementation.go b/pkg/sourcetool/implementation.go index 18b639b..0625dd9 100644 --- a/pkg/sourcetool/implementation.go +++ b/pkg/sourcetool/implementation.go @@ -38,6 +38,7 @@ type toolImplementation interface { GetVcsBackend(*models.Repository) (models.VcsBackend, error) GetAttestationReader(*models.Repository) (models.AttestationStorageReader, error) GetBranchControls(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSetStatus, error) + GetBranchControlsAtCommit(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error) ConfigureControls(models.VcsBackend, *models.Repository, []*models.Branch, []models.ControlConfiguration) error GetPolicyStatus(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.ControlStatus, error) CreateRepositoryFork(context.Context, *auth.Authenticator, *models.Repository, string) error @@ -58,6 +59,12 @@ func (impl *defaultToolImplementation) GetBranchControls( return backend.GetBranchControls(ctx, branch) } +func (impl *defaultToolImplementation) GetBranchControlsAtCommit( + ctx context.Context, backend models.VcsBackend, branch *models.Branch, commit *models.Commit, +) (*slsa.ControlSetStatus, error) { + return backend.GetBranchControlsAtCommit(ctx, branch, commit) +} + // GetAttestationReader returns the att reader object func (impl *defaultToolImplementation) GetAttestationReader(_ *models.Repository) (models.AttestationStorageReader, error) { // We only have the notes backend for now diff --git a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go index 4762a97..628580e 100644 --- a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go +++ b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go @@ -107,6 +107,22 @@ type FakeToolImplementation struct { result1 *slsa.ControlSetStatus result2 error } + GetBranchControlsAtCommitStub func(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error) + getBranchControlsAtCommitMutex sync.RWMutex + getBranchControlsAtCommitArgsForCall []struct { + arg1 context.Context + arg2 models.VcsBackend + arg3 *models.Branch + arg4 *models.Commit + } + getBranchControlsAtCommitReturns struct { + result1 *slsa.ControlSetStatus + result2 error + } + getBranchControlsAtCommitReturnsOnCall map[int]struct { + result1 *slsa.ControlSetStatus + result2 error + } GetPolicyStatusStub func(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.ControlStatus, error) getPolicyStatusMutex sync.RWMutex getPolicyStatusArgsForCall []struct { @@ -625,6 +641,73 @@ func (fake *FakeToolImplementation) GetBranchControlsReturnsOnCall(i int, result }{result1, result2} } +func (fake *FakeToolImplementation) GetBranchControlsAtCommit(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Branch, arg4 *models.Commit) (*slsa.ControlSetStatus, error) { + fake.getBranchControlsAtCommitMutex.Lock() + ret, specificReturn := fake.getBranchControlsAtCommitReturnsOnCall[len(fake.getBranchControlsAtCommitArgsForCall)] + fake.getBranchControlsAtCommitArgsForCall = append(fake.getBranchControlsAtCommitArgsForCall, struct { + arg1 context.Context + arg2 models.VcsBackend + arg3 *models.Branch + arg4 *models.Commit + }{arg1, arg2, arg3, arg4}) + stub := fake.GetBranchControlsAtCommitStub + fakeReturns := fake.getBranchControlsAtCommitReturns + fake.recordInvocation("GetBranchControlsAtCommit", []interface{}{arg1, arg2, arg3, arg4}) + fake.getBranchControlsAtCommitMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeToolImplementation) GetBranchControlsAtCommitCallCount() int { + fake.getBranchControlsAtCommitMutex.RLock() + defer fake.getBranchControlsAtCommitMutex.RUnlock() + return len(fake.getBranchControlsAtCommitArgsForCall) +} + +func (fake *FakeToolImplementation) GetBranchControlsAtCommitCalls(stub func(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error)) { + fake.getBranchControlsAtCommitMutex.Lock() + defer fake.getBranchControlsAtCommitMutex.Unlock() + fake.GetBranchControlsAtCommitStub = stub +} + +func (fake *FakeToolImplementation) GetBranchControlsAtCommitArgsForCall(i int) (context.Context, models.VcsBackend, *models.Branch, *models.Commit) { + fake.getBranchControlsAtCommitMutex.RLock() + defer fake.getBranchControlsAtCommitMutex.RUnlock() + argsForCall := fake.getBranchControlsAtCommitArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeToolImplementation) GetBranchControlsAtCommitReturns(result1 *slsa.ControlSetStatus, result2 error) { + fake.getBranchControlsAtCommitMutex.Lock() + defer fake.getBranchControlsAtCommitMutex.Unlock() + fake.GetBranchControlsAtCommitStub = nil + fake.getBranchControlsAtCommitReturns = struct { + result1 *slsa.ControlSetStatus + result2 error + }{result1, result2} +} + +func (fake *FakeToolImplementation) GetBranchControlsAtCommitReturnsOnCall(i int, result1 *slsa.ControlSetStatus, result2 error) { + fake.getBranchControlsAtCommitMutex.Lock() + defer fake.getBranchControlsAtCommitMutex.Unlock() + fake.GetBranchControlsAtCommitStub = nil + if fake.getBranchControlsAtCommitReturnsOnCall == nil { + fake.getBranchControlsAtCommitReturnsOnCall = make(map[int]struct { + result1 *slsa.ControlSetStatus + result2 error + }) + } + fake.getBranchControlsAtCommitReturnsOnCall[i] = struct { + result1 *slsa.ControlSetStatus + result2 error + }{result1, result2} +} + func (fake *FakeToolImplementation) GetPolicyStatus(arg1 context.Context, arg2 *auth.Authenticator, arg3 *options.Options, arg4 *models.Repository) (*slsa.ControlStatus, error) { fake.getPolicyStatusMutex.Lock() ret, specificReturn := fake.getPolicyStatusReturnsOnCall[len(fake.getPolicyStatusArgsForCall)] diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index 96851b8..7157505 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -80,6 +80,35 @@ func (t *Tool) GetBranchControls(ctx context.Context, branch *models.Branch) (*s return controls, err } +// GetRepoControls returns the controls that are enabled in a repository branch. +func (t *Tool) GetBranchControlsAtCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*slsa.ControlSetStatus, error) { + if branch.Repository == nil { + return nil, fmt.Errorf("repositoryu not specified in branch") + } + + backend, err := t.impl.GetVcsBackend(branch.Repository) + if err != nil { + return nil, fmt.Errorf("getting VCS backend: %w", err) + } + + // Get the control status in the branch. Backends are expected to + // return the full SLSA Source control catalog + controls, err := t.impl.GetBranchControlsAtCommit(ctx, backend, branch, commit) + if err != nil { + return nil, fmt.Errorf("getting branch controls: %w", err) + } + + // We also abstract the repository policy as a control to report its status + status, err := t.impl.GetPolicyStatus(ctx, t.Authenticator, &t.Options, branch.Repository) + if err != nil { + return nil, fmt.Errorf("reading policy status: %w", err) + } + + controls.Controls = append(controls.Controls, *status) + + return controls, err +} + // OnboardRepository configures a repository to set up the required controls // to meet SLSA Source L3. func (t *Tool) OnboardRepository(ctx context.Context, repo *models.Repository, branches []*models.Branch) error { From 45be338572afd7edad53d13ed6cec9d23928aaa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Thu, 5 Mar 2026 22:00:26 -0600 Subject: [PATCH 07/41] Unify control and set types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/audit_test.go | 4 +- internal/cmd/checklevel.go | 102 +++--- internal/cmd/status.go | 6 +- pkg/attest/provenance.go | 4 +- pkg/ghcontrol/checklevel.go | 48 ++- pkg/ghcontrol/checklevel_test.go | 10 +- pkg/policy/policy.go | 61 ++-- pkg/policy/policy_test.go | 321 +++++++++--------- pkg/slsa/controls.go | 4 + pkg/slsa/levels.go | 11 + pkg/slsa/slsa_types.go | 162 +++++---- pkg/sourcetool/backends/vcs/github/github.go | 20 +- pkg/sourcetool/implementation.go | 10 +- pkg/sourcetool/models/models.go | 2 +- .../models/modelsfakes/fake_vcs_backend.go | 20 +- .../fake_tool_implementation.go | 20 +- pkg/sourcetool/tool.go | 10 +- pkg/sourcetool/tool_test.go | 4 +- 18 files changed, 444 insertions(+), 375 deletions(-) diff --git a/internal/cmd/audit_test.go b/internal/cmd/audit_test.go index d0c9a81..d1ec026 100644 --- a/internal/cmd/audit_test.go +++ b/internal/cmd/audit_test.go @@ -180,7 +180,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { }, GhPriorCommit: "def456", GhControlStatus: &ghcontrol.GhControlStatus{ - Controls: slsa.Controls{}, + Controls: &slsa.ControlSetStatus{}, }, }, mode: AuditModeFull, @@ -192,7 +192,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { VerifiedLevels: []string{"SLSA_SOURCE_LEVEL_3"}, PrevCommitMatches: &matches, ProvControls: []*provenance.Control{{Name: "test_control"}}, - GhControls: slsa.Controls{}, + GhControls: slsa.ControlSetStatus{}, PrevCommit: "def456", GhPriorCommit: "def456", Link: "https://github.com/test-owner/test-repo/commit/def456", diff --git a/internal/cmd/checklevel.go b/internal/cmd/checklevel.go index ccc44a4..f96749f 100644 --- a/internal/cmd/checklevel.go +++ b/internal/cmd/checklevel.go @@ -4,7 +4,6 @@ package cmd import ( - "context" "errors" "fmt" "os" @@ -12,8 +11,10 @@ import ( "github.com/spf13/cobra" "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/policy" + "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type checkLevelOpts struct { @@ -71,54 +72,65 @@ This is meant to be run within the corresponding GitHub Actions workflow.`, return err } - return doCheckLevel(&opts) - }, - } - opts.AddFlags(checklevelCmd) - parentCmd.AddCommand(checklevelCmd) -} + authenticator, err := CheckAuth() + if err != nil { + return err + } -func doCheckLevel(cla *checkLevelOpts) error { - ghconnection := ghcontrol.NewGhConnection(cla.owner, cla.repository, ghcontrol.BranchToFullRef(cla.branch)).WithAuthToken(githubToken) - ghconnection.Options.AllowMergeCommits = cla.allowMergeCommits + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + ) + if err != nil { + return err + } - ctx := context.Background() - controlStatus, err := ghconnection.GetBranchControlsAtCommit(ctx, cla.commit, ghconnection.GetFullRef()) - if err != nil { - return err - } + controlStatus, err := srctool.GetBranchControlsAtCommit(cmd.Context(), opts.GetBranch(), &models.Commit{SHA: opts.commit}) + if err != nil { + return err + } - fmt.Printf("Controls salieron:\n%v\n", controlStatus) + // fmt.Printf("Controles: %+v\n", controlStatus) - pe := policy.NewPolicyEvaluator() - pe.UseLocalPolicy = cla.useLocalPolicy - verifiedLevels, policyPath, err := pe.EvaluateControl(ctx, cla.GetRepository(), cla.GetBranch(), controlStatus) - if err != nil { - return err - } - fmt.Print(verifiedLevels) + pe := policy.NewPolicyEvaluator() + pe.UseLocalPolicy = opts.useLocalPolicy + verifiedLevels, policyPath, err := pe.EvaluateControl(cmd.Context(), opts.GetRepository(), opts.GetBranch(), controlStatus) + if err != nil { + return err + } + for _, level := range verifiedLevels { + if slsa.IsSlsaSourceLevel(level) { + fmt.Print(level) + break + } + } - unsignedVsa, err := attest.CreateUnsignedSourceVsa(ghconnection.GetRepoUri(), ghconnection.GetFullRef(), cla.commit, verifiedLevels, policyPath) - if err != nil { - return err - } - if cla.outputUnsignedVsa != "" { - if err := os.WriteFile(cla.outputUnsignedVsa, []byte(unsignedVsa), 0o644); err != nil { //nolint:gosec - return err - } - } + // unsignedVsa, err := attest.CreateUnsignedSourceVsa(ghconnection.GetRepoUri(), ghconnection.GetFullRef(), opts.commit, verifiedLevels, policyPath) + unsignedVsa, err := attest.CreateUnsignedSourceVsa(opts.GetRepository().GetHttpURL(), opts.GetBranch().FullRef(), opts.commit, verifiedLevels, policyPath) + if err != nil { + return err + } + if opts.outputUnsignedVsa != "" { + if err := os.WriteFile(opts.outputUnsignedVsa, []byte(unsignedVsa), 0o644); err != nil { //nolint:gosec + return err + } + } - if cla.outputVsa != "" { - // This will output in the sigstore bundle format. - signedVsa, err := attest.Sign(unsignedVsa) - if err != nil { - return err - } - err = os.WriteFile(cla.outputVsa, []byte(signedVsa), 0o644) //nolint:gosec - if err != nil { - return err - } - } + if opts.outputVsa != "" { + // This will output in the sigstore bundle format. + signedVsa, err := attest.Sign(unsignedVsa) + if err != nil { + return err + } + err = os.WriteFile(opts.outputVsa, []byte(signedVsa), 0o644) //nolint:gosec + if err != nil { + return err + } + } - return nil + return nil + }, + } + opts.AddFlags(checklevelCmd) + parentCmd.AddCommand(checklevelCmd) } diff --git a/internal/cmd/status.go b/internal/cmd/status.go index 270edb7..ff080ae 100644 --- a/internal/cmd/status.go +++ b/internal/cmd/status.go @@ -116,7 +116,7 @@ sourcetool status myorg/myrepo@mybranch } // Compute the maximum level possible: - toplevel := policy.ComputeEligibleSlsaLevel(*controls.GetActiveControls()) + toplevel := policy.ComputeEligibleSlsaLevel(controls.GetActiveControls()) title := fmt.Sprintf( "\nSLSA Source Status for %s/%s@%s", opts.owner, opts.repository, @@ -126,10 +126,10 @@ sourcetool status myorg/myrepo@mybranch fmt.Println(w(title)) fmt.Println(strings.Repeat("=", len(title))) - var policyControlStatus *slsa.ControlStatus + var policyControlStatus *slsa.Control for _, c := range controls.Controls { if c.Name == slsa.PolicyAvailable { - policyControlStatus = &c + policyControlStatus = c continue } fmt.Printf("%-35s ", c.Name) diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index a08879c..74608a3 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -141,7 +141,7 @@ func (pa ProvenanceAttestor) createCurrentProvenance(ctx context.Context, commit curProvPred.ActivityType = controlStatus.ActivityType curProvPred.Branch = ref curProvPred.CreatedOn = timestamppb.New(curTime) - curProvPred.Controls = controlStatus.Controls + curProvPred.Controls = controlStatus.Controls.ToProvenanceControls() // At the very least provenance is available starting now. :) // ... indeed, but don't set the `since`` date because doing so breaks @@ -319,7 +319,7 @@ func (pa ProvenanceAttestor) CreateTagProvenance(ctx context.Context, commit, re Actor: actor, Tag: ref, CreatedOn: timestamppb.Now(), - Controls: controlStatus.Controls, + Controls: controlStatus.Controls.ToProvenanceControls(), VsaSummaries: []*provenance.VsaSummary{ { SourceRefs: vsaRefs, diff --git a/pkg/ghcontrol/checklevel.go b/pkg/ghcontrol/checklevel.go index f888b28..d42fce7 100644 --- a/pkg/ghcontrol/checklevel.go +++ b/pkg/ghcontrol/checklevel.go @@ -12,9 +12,7 @@ import ( "time" "github.com/google/go-github/v69/github" - "google.golang.org/protobuf/types/known/timestamppb" - "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) @@ -81,13 +79,13 @@ type GhControlStatus struct { ActivityType string // The controls that are enabled according to the GitHub API. // May not include other controls like if we have provenance. - Controls slsa.Controls + Controls *slsa.ControlSetStatus } // Adds the control, but only if it existed when the commit was pushed. -func (cs *GhControlStatus) AddControl(newControls ...*provenance.Control) { +func (cs *GhControlStatus) AddControl(newControls ...*slsa.Control) { for _, newControl := range newControls { - if newControl != nil && cs.CommitPushTime.After(newControl.GetSince().AsTime()) { + if cs.CommitPushTime.After(*newControl.GetSince()) { cs.Controls.AddControl(newControl) } } @@ -101,7 +99,7 @@ func (ghc *GitHubConnection) ruleMeetsRequiresReview(rule *github.PullRequestBra } // Computes the continuity control returning nil if it's not enabled. -func (ghc *GitHubConnection) computeRepoContinuityControl(ctx context.Context, rules *github.BranchRules) (*provenance.Control, error) { +func (ghc *GitHubConnection) computeRepoContinuityControl(ctx context.Context, rules *github.BranchRules) (*slsa.Control, error) { oldestDeletion, err := ghc.getOldestActiveRule(ctx, rules.Deletion) if err != nil { return nil, fmt.Errorf("looking for oldest branch delete protection: %w", err) @@ -122,7 +120,7 @@ func (ghc *GitHubConnection) computeRepoContinuityControl(ctx context.Context, r newestRule = oldestNoFf } - return &provenance.Control{Name: string(slsa.SLSA_SOURCE_SCS_CONTINUITY), Since: timestamppb.New(newestRule.UpdatedAt.Time)}, nil + return &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, Since: &newestRule.UpdatedAt.Time}, nil } // enforcesTagHygiene checks a repository ruleset to understand if it is @@ -141,7 +139,7 @@ func enforcesTagHygiene(ruleset *github.RepositoryRuleset) bool { } // computeTagHygieneControl -func (ghc *GitHubConnection) computeTagHygieneControl(ctx context.Context, allRulesets []*github.RepositoryRuleset) (*provenance.Control, error) { +func (ghc *GitHubConnection) computeTagHygieneControl(ctx context.Context, allRulesets []*github.RepositoryRuleset) (*slsa.Control, error) { var validRuleset *github.RepositoryRuleset for _, ruleset := range allRulesets { if *ruleset.Target != github.RulesetTargetTag { @@ -171,14 +169,14 @@ func (ghc *GitHubConnection) computeTagHygieneControl(ctx context.Context, allRu return nil, nil } - return &provenance.Control{ - Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS.String(), - Since: timestamppb.New(validRuleset.UpdatedAt.Time), + return &slsa.Control{ + Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, + Since: &validRuleset.UpdatedAt.Time, }, nil } // Computes the review control returning nil if it's not enabled. -func (ghc *GitHubConnection) computeReviewControl(ctx context.Context, rules []*github.PullRequestBranchRule) (*provenance.Control, error) { +func (ghc *GitHubConnection) computeReviewControl(ctx context.Context, rules []*github.PullRequestBranchRule) (*slsa.Control, error) { var oldestActive *github.RepositoryRuleset for _, rule := range rules { if ghc.ruleMeetsRequiresReview(rule) { @@ -195,19 +193,19 @@ func (ghc *GitHubConnection) computeReviewControl(ctx context.Context, rules []* } if oldestActive != nil { - return &provenance.Control{ - Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW.String(), - Since: timestamppb.New(oldestActive.UpdatedAt.Time), + return &slsa.Control{ + Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, + Since: &oldestActive.UpdatedAt.Time, }, nil } return nil, nil } -func (ghc *GitHubConnection) computeRequiredChecks(ctx context.Context, ghCheckRules []*github.RequiredStatusChecksBranchRule) ([]*provenance.Control, error) { +func (ghc *GitHubConnection) computeRequiredChecks(ctx context.Context, ghCheckRules []*github.RequiredStatusChecksBranchRule) ([]*slsa.Control, error) { // Only return the checks we're happy about. // For now that's only stuff from the GitHub Actions app. - requiredChecks := []*provenance.Control{} + requiredChecks := []*slsa.Control{} for _, ghCheckRule := range ghCheckRules { ruleset, _, err := ghc.Client().Repositories.GetRuleset(ctx, ghc.Owner(), ghc.Repo(), ghCheckRule.RulesetID, false) if err != nil { @@ -223,9 +221,9 @@ func (ghc *GitHubConnection) computeRequiredChecks(ctx context.Context, ghCheckR // Ignore untrusted integration id. continue } - requiredChecks = append(requiredChecks, &provenance.Control{ - Name: CheckNameToControlName(check.Context).String(), - Since: timestamppb.New(ruleset.UpdatedAt.Time), + requiredChecks = append(requiredChecks, &slsa.Control{ + Name: CheckNameToControlName(check.Context), + Since: &ruleset.UpdatedAt.Time, }) } } @@ -350,13 +348,13 @@ func (ghc *GitHubConnection) getOldestActiveRule(ctx context.Context, rules []*g // GetBranchControls returns a list of the controls enabled at present for a branch. // This function does not take into account a commit date, it just returns those controls // that are active when called. -func (ghc *GitHubConnection) GetBranchControls(ctx context.Context, ref string) (*slsa.Controls, error) { +func (ghc *GitHubConnection) GetBranchControls(ctx context.Context, ref string) (*slsa.ControlSetStatus, error) { branch := GetBranchFromRef(ref) if branch == "" { return nil, fmt.Errorf("ref %s is not a branch", ref) } - controls := &slsa.Controls{} + controls := &slsa.ControlSetStatus{} // Do the branch specific stuff. branchRules, _, err := ghc.Client().Repositories.GetRulesForBranch(ctx, ghc.Owner(), ghc.Repo(), branch) @@ -411,7 +409,7 @@ func (ghc *GitHubConnection) GetBranchControlsAtCommit(ctx context.Context, comm CommitPushTime: activity.Timestamp, ActivityType: activity.ActivityType, ActorLogin: activity.Actor.Login, - Controls: slsa.Controls{}, + Controls: &slsa.ControlSetStatus{}, } activeControls, err := ghc.GetBranchControls(ctx, ref) @@ -421,7 +419,7 @@ func (ghc *GitHubConnection) GetBranchControlsAtCommit(ctx context.Context, comm // Add the controls to the control status object. This will // discard any that were not active when the commit merged. - for _, c := range *activeControls { + for _, c := range activeControls.Controls { controlStatus.AddControl(c) } @@ -431,7 +429,7 @@ func (ghc *GitHubConnection) GetBranchControlsAtCommit(ctx context.Context, comm func (ghc *GitHubConnection) GetTagControls(ctx context.Context, commit, ref string) (*GhControlStatus, error) { controlStatus := GhControlStatus{ CommitPushTime: time.Now(), - Controls: slsa.Controls{}, + Controls: &slsa.ControlSetStatus{}, } allRulesets, _, err := ghc.Client().Repositories.GetAllRulesets(ctx, ghc.Owner(), ghc.Repo(), true) diff --git a/pkg/ghcontrol/checklevel_test.go b/pkg/ghcontrol/checklevel_test.go index d1abcc0..09399c5 100644 --- a/pkg/ghcontrol/checklevel_test.go +++ b/pkg/ghcontrol/checklevel_test.go @@ -269,7 +269,7 @@ func TestBuiltinBranchControls(t *testing.T) { control := controlStatus.Controls.GetControl(tt.expectedControl) if control == nil { t.Fatalf("expected controls to contain %v, got %+v", tt.expectedControl, controlStatus.Controls) - } else if !control.GetSince().AsTime().Equal(priorTime) { + } else if !control.GetSince().Equal(priorTime) { t.Fatalf("expected control.Since %v, got %v", priorTime, control.GetSince()) } }) @@ -310,7 +310,7 @@ func TestBuiltinBranchControlsEnabledLater(t *testing.T) { t.Fatalf("Error getting branch controls: %v", err) } - if len(controlStatus.Controls) != 0 { + if len(controlStatus.Controls.Controls) != 0 { t.Errorf("execpted no controls, got: %+v", controlStatus.Controls) } }) @@ -350,10 +350,10 @@ func TestGetBranchControlsRequiredChecks(t *testing.T) { t.Fatalf("Error getting branch controls: %v", err) } - controlNames := make([]slsa.ControlName, 0, len(controlStatus.Controls)) - for _, control := range controlStatus.Controls { + controlNames := make([]slsa.ControlName, 0, len(controlStatus.Controls.Controls)) + for _, control := range controlStatus.Controls.Controls { controlNames = append(controlNames, slsa.ControlName(control.GetName())) - if !control.GetSince().AsTime().Equal(priorTime) { + if !control.GetSince().Equal(priorTime) { t.Errorf("Expected control.Since %v, got %v", priorTime, control.GetSince()) } } diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 3fa430c..126e8ea 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -258,8 +258,8 @@ func (pe *PolicyEvaluator) CreateLocalPolicy(ctx context.Context, repo *models.R // Unless there is previous provenance metadata, then we can compute // a higher level if provPred != nil { - eligibleLevel = ComputeEligibleSlsaLevel(provPred.GetControls()) - eligibleSince, err = ComputeEligibleSince(provPred.GetControls(), eligibleLevel) + eligibleLevel = ComputeEligibleSlsaLevel(slsa.NewControlSetFromProvanenaceControls(provPred.GetControls())) + eligibleSince, err = ComputeEligibleSince(slsa.NewControlSetFromProvanenaceControls(provPred.GetControls()), eligibleLevel) if err != nil { return "", fmt.Errorf("could not compute eligible since: %w", err) } @@ -278,11 +278,11 @@ func (pe *PolicyEvaluator) CreateLocalPolicy(ctx context.Context, repo *models.R } // If the controls returned - controls := slsa.Controls(provPred.GetControls()) + controls := slsa.NewControlSetFromProvanenaceControls(provPred.GetControls()) tagHygiene := controls.GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS) if tagHygiene != nil { p.ProtectedTag = &ProtectedTag{ - Since: tagHygiene.GetSince(), + Since: timestamppb.New(*tagHygiene.GetSince()), TagHygiene: true, } } @@ -302,14 +302,17 @@ func (pe *PolicyEvaluator) CreateLocalPolicy(ctx context.Context, repo *models.R return path, nil } -func computeEligibleForLevel(controls slsa.Controls, level slsa.SlsaSourceLevel) bool { +func computeEligibleForLevel(controls *slsa.ControlSetStatus, level slsa.SlsaSourceLevel) bool { requiredControls := slsa.GetRequiredControlsForLevel(level) return controls.AreControlsAvailable(requiredControls) } // Computes the eligible SLSA level, and when they started being eligible for it, // if only they had a policy. Also returns a rationale for why it's eligible for this level. -func ComputeEligibleSlsaLevel(controls slsa.Controls) slsa.SlsaSourceLevel { +func ComputeEligibleSlsaLevel(controls *slsa.ControlSetStatus) slsa.SlsaSourceLevel { + if controls == nil { + return slsa.SlsaSourceLevel1 + } // Go from highest to lowest. for _, level := range []slsa.SlsaSourceLevel{ slsa.SlsaSourceLevel4, slsa.SlsaSourceLevel3, slsa.SlsaSourceLevel2, @@ -335,7 +338,7 @@ func laterTime(time1, time2 time.Time) time.Time { } // Computes the time since these controls have been eligible for the level, nil if not eligible. -func ComputeEligibleSince(controls slsa.Controls, level slsa.SlsaSourceLevel) (*time.Time, error) { +func ComputeEligibleSince(controls *slsa.ControlSetStatus, level slsa.SlsaSourceLevel) (*time.Time, error) { requiredControls := slsa.GetRequiredControlsForLevel(level) var newestTime time.Time for _, rc := range requiredControls { @@ -343,19 +346,23 @@ func ComputeEligibleSince(controls slsa.Controls, level slsa.SlsaSourceLevel) (* if ac == nil { return nil, nil } + since := ac.GetSince() + if since == nil { + continue + } if newestTime.Equal(time.Time{}) { - newestTime = ac.GetSince().AsTime() + newestTime = *since } else { - newestTime = laterTime(newestTime, ac.GetSince().AsTime()) + newestTime = laterTime(newestTime, *since) } } return &newestTime, nil } // Every function that determines properties to include in the result & VSA implements this interface. -type computePolicyResult func(*ProtectedBranch, *ProtectedTag, slsa.Controls) ([]slsa.ControlName, error) +type computePolicyResult func(*ProtectedBranch, *ProtectedTag, *slsa.ControlSetStatus) ([]slsa.ControlName, error) -func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls slsa.Controls) ([]slsa.ControlName, error) { +func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSetStatus) ([]slsa.ControlName, error) { eligibleLevel := ComputeEligibleSlsaLevel(controls) if !slsa.IsLevelHigherOrEqualTo(eligibleLevel, slsa.SlsaSourceLevel(branchPolicy.GetTargetSlsaSourceLevel())) { @@ -384,7 +391,7 @@ func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls s return []slsa.ControlName{slsa.ControlName(branchPolicy.GetTargetSlsaSourceLevel())}, nil } -func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls slsa.Controls) ([]slsa.ControlName, error) { +func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSetStatus) ([]slsa.ControlName, error) { if !branchPolicy.GetRequireReview() { return []slsa.ControlName{}, nil } @@ -394,7 +401,7 @@ func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, contr return []slsa.ControlName{}, fmt.Errorf("policy requires review, but that control is not enabled") } - if branchPolicy.GetSince().AsTime().Before(reviewControl.GetSince().AsTime()) { + if reviewControl.GetSince() != nil && branchPolicy.GetSince().AsTime().Before(*reviewControl.GetSince()) { return []slsa.ControlName{}, fmt.Errorf("policy requires review since %v, but that control has only been enabled since %v", branchPolicy.GetSince(), reviewControl.GetSince()) } @@ -403,7 +410,7 @@ func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, contr // computeTagHygiene checks if the current state of the protected refs // matches what we see in the policy policy has SLSA_SOURCE_SCS_PROTECTED_REFS -func computeTagHygiene(_ *ProtectedBranch, tagPolicy *ProtectedTag, controls slsa.Controls) ([]slsa.ControlName, error) { +func computeTagHygiene(_ *ProtectedBranch, tagPolicy *ProtectedTag, controls *slsa.ControlSetStatus) ([]slsa.ControlName, error) { if tagPolicy == nil { // There is no tag policy, so the control isn't met, but it's not an error. return []slsa.ControlName{}, nil @@ -423,14 +430,14 @@ func computeTagHygiene(_ *ProtectedBranch, tagPolicy *ProtectedTag, controls sls } // Tags were protected later than the policy date. Fail. mmmh.. - if tagPolicy.GetSince().AsTime().Before(tagHygiene.GetSince().AsTime()) { + if tagHygiene.GetSince() != nil && tagPolicy.GetSince().AsTime().Before(*tagHygiene.GetSince()) { return []slsa.ControlName{}, fmt.Errorf("policy requires tag hygiene since %v, but that control has only been enabled since %v", tagPolicy.GetSince(), tagHygiene.GetSince()) } return []slsa.ControlName{slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, nil } -func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls slsa.Controls) ([]slsa.ControlName, error) { +func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSetStatus) ([]slsa.ControlName, error) { controlNames := []slsa.ControlName{} for _, rc := range branchPolicy.GetOrgStatusCheckControls() { if !strings.HasPrefix(rc.GetPropertyName(), slsa.AllowedOrgPropPrefix) { @@ -439,7 +446,7 @@ func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls control := controls.GetControl(ghcontrol.CheckNameToControlName(rc.GetCheckName())) if control != nil { - if rc.GetSince().AsTime().Before(control.GetSince().AsTime()) { + if control.GetSince() != nil && rc.GetSince().AsTime().Before(*control.GetSince()) { return []slsa.ControlName{}, fmt.Errorf("policy requires check '%v' since %v, but that control has only been enabled since %v", rc.GetCheckName(), rc.GetSince(), control.GetSince()) } controlNames = append(controlNames, slsa.ControlName(rc.GetPropertyName())) @@ -451,8 +458,13 @@ func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls } // Returns a list of controls to include in the vsa's 'verifiedLevels' field when creating a VSA for a branch. -func evaluateBranchControls(branchPolicy *ProtectedBranch, tagPolicy *ProtectedTag, controls slsa.Controls) (slsa.SourceVerifiedLevels, error) { - policyComputers := []computePolicyResult{computeSlsaLevel, computeReviewEnforced, computeTagHygiene, computeOrgControls} +func evaluateBranchControls(branchPolicy *ProtectedBranch, tagPolicy *ProtectedTag, controls *slsa.ControlSetStatus) (slsa.SourceVerifiedLevels, error) { + policyComputers := []computePolicyResult{ + computeSlsaLevel, // Add the SLSA Level to the VSA + computeReviewEnforced, // Stamp if reviews are enforced + computeTagHygiene, // Stamp the tag hygiene + computeOrgControls, // Add other organizational controls + } verifiedLevels := slsa.SourceVerifiedLevels{} @@ -474,7 +486,7 @@ func evaluateTagProv(tagPolicy *ProtectedTag, tagProvPred *provenance.TagProvena // As long as all the controls for tag protection are currently in force then we'll // include the verifiedLevels. - computedControls, err := computeTagHygiene(nil, tagPolicy, tagProvPred.GetControls()) + computedControls, err := computeTagHygiene(nil, tagPolicy, slsa.NewControlSetFromProvanenaceControls(tagProvPred.GetControls())) if err != nil { return slsa.SourceVerifiedLevels{}, fmt.Errorf("error computing tag immutability enforced: %w", err) } @@ -537,7 +549,7 @@ func NewPolicyEvaluator() *PolicyEvaluator { } // EvaluateControl checks the control against the policy and returns the resulting source level and policy path. -func (pe *PolicyEvaluator) EvaluateControl(ctx context.Context, repo *models.Repository, branch *models.Branch, controlStatus *ghcontrol.GhControlStatus) (slsa.SourceVerifiedLevels, string, error) { +func (pe *PolicyEvaluator) EvaluateControl(ctx context.Context, repo *models.Repository, branch *models.Branch, controlStatus *slsa.ControlSetStatus) (slsa.SourceVerifiedLevels, string, error) { // We want to check to ensure the repo hasn't enabled/disabled the rules since // setting the 'since' field in their policy. rp, policyPath, err := pe.GetPolicy(ctx, repo) @@ -551,15 +563,16 @@ func (pe *PolicyEvaluator) EvaluateControl(ctx context.Context, repo *models.Rep policyPath = "DEFAULT" } - if controlStatus.CommitPushTime.Before(branchPolicy.GetSince().AsTime()) { + if controlStatus.Time.Before(branchPolicy.GetSince().AsTime()) { // This commit was pushed before they had an explicit policy. return slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, policyPath, nil } - verifiedLevels, err := evaluateBranchControls(branchPolicy, rp.GetProtectedTag(), controlStatus.Controls) + verifiedLevels, err := evaluateBranchControls(branchPolicy, rp.GetProtectedTag(), controlStatus) if err != nil { return verifiedLevels, policyPath, fmt.Errorf("error evaluating policy %s: %w", policyPath, err) } + return verifiedLevels, policyPath, nil } @@ -581,7 +594,7 @@ func (pe *PolicyEvaluator) EvaluateSourceProv(ctx context.Context, repo *models. policyPath = "DEFAULT" } - verifiedLevels, err := evaluateBranchControls(branchPolicy, rp.GetProtectedTag(), provPred.GetControls()) + verifiedLevels, err := evaluateBranchControls(branchPolicy, rp.GetProtectedTag(), slsa.NewControlSetFromProvanenaceControls(provPred.GetControls())) if err != nil { return slsa.SourceVerifiedLevels{}, policyPath, fmt.Errorf("error evaluating policy %s: %w", policyPath, err) } diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index 9d595a1..d4068e2 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -39,15 +39,19 @@ const ( ) var ( - fixedTime = timestamppb.New(time.Unix(1678886400, 0)) // March 15, 2023 00:00:00 UTC - earlierFixedTime = timestamppb.New(fixedTime.AsTime().Add(-time.Hour)) - laterFixedTime = timestamppb.New(fixedTime.AsTime().Add(time.Hour)) + // fixedTime = timestamppb.New(time.Unix(1678886400, 0)) // March 15, 2023 00:00:00 UTC + // earlierFixedTime = timestamppb.New(fixedTime.AsTime().Add(-time.Hour)) + // laterFixedTime = timestamppb.New(fixedTime.AsTime().Add(time.Hour)) + + fixedTime = time.Unix(1678886400, 0) // March 15, 2023 00:00:00 UTC + earlierFixedTime = fixedTime.Add(-time.Hour) + laterFixedTime = fixedTime.Add(time.Hour) ) func createTestBranchPolicy(branch string) ProtectedBranch { return ProtectedBranch{ Name: branch, - Since: fixedTime, + Since: timestamppb.New(fixedTime), TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: true, } @@ -60,7 +64,7 @@ func createTestPolicy(pb *ProtectedBranch) RepoPolicy { pb, }, ProtectedTag: &ProtectedTag{ - Since: fixedTime, + Since: timestamppb.New(fixedTime), TagHygiene: true, }, } @@ -148,35 +152,35 @@ func validateMockServerRequestPath(t *testing.T, r *http.Request, expectedPolicy } } -// controlsForLevel creates a slsa.Controls collection with all required controls +// controlsForLevel creates a slsa.ControlSetStatus collection with all required controls // for the given SLSA level, each with the given Since timestamp. -func controlsForLevel(level slsa.SlsaSourceLevel, since *timestamppb.Timestamp) slsa.Controls { +func controlsForLevel(level slsa.SlsaSourceLevel, since *time.Time) *slsa.ControlSetStatus { required := slsa.GetRequiredControlsForLevel(level) - controls := slsa.Controls{} + controls := &slsa.ControlSetStatus{} for _, name := range required { - controls.AddControl(&provenance.Control{Name: name.String(), Since: since}) + controls.AddControl(&slsa.Control{Name: name, Since: since, State: slsa.StateActive}) } return controls } // controlsForLevelWith creates controls for a level plus additional controls. -func controlsForLevelWith(level slsa.SlsaSourceLevel, since *timestamppb.Timestamp, extra ...*provenance.Control) slsa.Controls { +func controlsForLevelWith(level slsa.SlsaSourceLevel, since *time.Time, extra ...*slsa.Control) *slsa.ControlSetStatus { controls := controlsForLevel(level, since) controls.AddControl(extra...) return controls } func TestEvaluateSourceProv_Success(t *testing.T) { - orgTestControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_test", Since: earlierFixedTime} + orgTestControl := &slsa.Control{Name: "GH_REQUIRED_CHECK_test", Since: &earlierFixedTime, State: slsa.StateActive} // Valid Provenance Predicate with L3 controls + review + tag hygiene + org control - l3Controls := controlsForLevelWith(slsa.SlsaSourceLevel3, earlierFixedTime, - &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW.String(), Since: earlierFixedTime}, + l3Controls := controlsForLevelWith(slsa.SlsaSourceLevel3, &earlierFixedTime, + &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, Since: &earlierFixedTime, State: slsa.StateActive}, orgTestControl, ) validProvPredicateL3Controls := provenance.SourceProvenancePred{ - Controls: l3Controls, + Controls: l3Controls.ToProvenanceControls(), } provenanceStatement := createStatementForTest(t, &validProvPredicateL3Controls, provenance.SourceProvPredicateType) @@ -184,17 +188,17 @@ func TestEvaluateSourceProv_Success(t *testing.T) { Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, - Since: fixedTime, + Since: timestamppb.New(fixedTime), OrgStatusCheckControls: []*OrgStatusCheckControl{ { CheckName: "test", PropertyName: "ORG_SOURCE_TESTED", - Since: fixedTime, + Since: timestamppb.New(fixedTime), }, }, } rp := createTestPolicy(&pb) - rp.ProtectedTag.Since = fixedTime + rp.ProtectedTag.Since = timestamppb.New(fixedTime) rp.ProtectedTag.TagHygiene = true expectedPolicyFilePath := createTempPolicyFile(t, &rp) @@ -221,23 +225,23 @@ func TestEvaluateSourceProv_Failure(t *testing.T) { // Policies policyL3ReviewTagsNow := RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ - {Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, Since: fixedTime}, + {Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, Since: timestamppb.New(fixedTime)}, }, - ProtectedTag: &ProtectedTag{Since: fixedTime, TagHygiene: true}, + ProtectedTag: &ProtectedTag{Since: timestamppb.New(fixedTime), TagHygiene: true}, } policyL1NoExtrasNow := RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ - {Name: "otherbranch", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), Since: fixedTime}, + {Name: "otherbranch", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), Since: timestamppb.New(fixedTime)}, }, ProtectedTag: nil, } // Valid Provenance Predicates validProvPredicateL3Controls := provenance.SourceProvenancePred{ - Controls: controlsForLevel(slsa.SlsaSourceLevel3, earlierFixedTime), + Controls: controlsForLevel(slsa.SlsaSourceLevel3, &earlierFixedTime).ToProvenanceControls(), } validProvPredicateL2Controls := provenance.SourceProvenancePred{ - Controls: controlsForLevel(slsa.SlsaSourceLevel2, earlierFixedTime), + Controls: controlsForLevel(slsa.SlsaSourceLevel2, &earlierFixedTime).ToProvenanceControls(), } tests := []struct { @@ -345,7 +349,7 @@ func createVsaSummary(ref string, verifiedLevels []slsa.ControlName) *provenance func TestEvaluateTagProv_Success(t *testing.T) { // Controls for mock provenance - tagHygieneEarlier := provenance.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS.String(), Since: earlierFixedTime} + tagHygieneEarlier := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, Since: &earlierFixedTime, State: slsa.StateActive} origL2ReviewedSummary := createVsaSummary("refs/heads/orig", []slsa.ControlName{ slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, }) @@ -362,7 +366,7 @@ func TestEvaluateTagProv_Success(t *testing.T) { { name: "Policy has protected_tag setting, and enabled", protectedTagPolicy: &ProtectedTag{ - Since: fixedTime, + Since: timestamppb.New(fixedTime), TagHygiene: true, }, vsaSummaries: []*provenance.VsaSummary{origL2ReviewedSummary}, @@ -371,7 +375,7 @@ func TestEvaluateTagProv_Success(t *testing.T) { { name: "Policy has protected_tag setting, and multiple summaries", protectedTagPolicy: &ProtectedTag{ - Since: fixedTime, + Since: timestamppb.New(fixedTime), TagHygiene: true, }, vsaSummaries: []*provenance.VsaSummary{origL2ReviewedSummary, mainL3Summary}, @@ -383,7 +387,7 @@ func TestEvaluateTagProv_Success(t *testing.T) { { name: "Policy has protected_tag setting, and it's not enabled", protectedTagPolicy: &ProtectedTag{ - Since: fixedTime, + Since: timestamppb.New(fixedTime), TagHygiene: false, }, vsaSummaries: []*provenance.VsaSummary{origL2ReviewedSummary}, @@ -392,7 +396,7 @@ func TestEvaluateTagProv_Success(t *testing.T) { { name: "Policy has protected_tag setting, and it's earlier than the control", protectedTagPolicy: &ProtectedTag{ - Since: earlierFixedTime, + Since: timestamppb.New(earlierFixedTime), TagHygiene: false, }, vsaSummaries: []*provenance.VsaSummary{origL2ReviewedSummary}, @@ -408,9 +412,10 @@ func TestEvaluateTagProv_Success(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + s := slsa.ControlSetStatus{Controls: []*slsa.Control{tagHygieneEarlier}} // Valid Provenance Predicate (attest.SourceProvenancePred) tagProvPred := provenance.TagProvenancePred{ - Controls: slsa.Controls{&tagHygieneEarlier}, + Controls: s.ToProvenanceControls(), VsaSummaries: tt.vsaSummaries, } @@ -420,7 +425,7 @@ func TestEvaluateTagProv_Success(t *testing.T) { Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: true, - Since: fixedTime, + Since: timestamppb.New(fixedTime), } rp := createTestPolicy(&pb) rp.ProtectedTag = tt.protectedTagPolicy @@ -448,9 +453,9 @@ func TestEvaluateTagProv_Success(t *testing.T) { } func TestEvaluateControl_Success(t *testing.T) { - orgTestControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_test", Since: earlierFixedTime} - reviewControl := &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW.String(), Since: earlierFixedTime} - l3ControlsWithExtras := controlsForLevelWith(slsa.SlsaSourceLevel3, earlierFixedTime, reviewControl, orgTestControl) + orgTestControl := &slsa.Control{Name: "GH_REQUIRED_CHECK_test", Since: &earlierFixedTime, State: slsa.StateActive} + reviewControl := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, Since: &earlierFixedTime, State: slsa.StateActive} + l3ControlsWithExtras := controlsForLevelWith(slsa.SlsaSourceLevel3, &earlierFixedTime, reviewControl, orgTestControl) // Policies fullPolicy := RepoPolicy{ @@ -459,31 +464,31 @@ func TestEvaluateControl_Success(t *testing.T) { Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, - Since: fixedTime, + Since: timestamppb.New(fixedTime), OrgStatusCheckControls: []*OrgStatusCheckControl{ { CheckName: "test", - Since: fixedTime, + Since: timestamppb.New(fixedTime), PropertyName: "ORG_SOURCE_TESTED", }, }, }, }, ProtectedTag: &ProtectedTag{ - Since: fixedTime, + Since: timestamppb.New(fixedTime), TagHygiene: true, }, } basicPolicy := RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ - {Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), Since: fixedTime}, + {Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), Since: timestamppb.New(fixedTime)}, }, } tests := []struct { name string policyContent any // RepoPolicy or string for malformed - controlStatus *ghcontrol.GhControlStatus + controlStatus *slsa.ControlSetStatus ghConnBranch string // Branch for GitHub connection expectedLevels slsa.SourceVerifiedLevels expectedPolicyPath string @@ -491,9 +496,9 @@ func TestEvaluateControl_Success(t *testing.T) { { name: "Commit time before policy Since -> SLSA Level 1", policyContent: &fullPolicy, - controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: earlierFixedTime.AsTime(), - Controls: l3ControlsWithExtras, + controlStatus: &slsa.ControlSetStatus{ + Time: earlierFixedTime, + Controls: l3ControlsWithExtras.Controls, }, ghConnBranch: "main", expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, @@ -502,9 +507,9 @@ func TestEvaluateControl_Success(t *testing.T) { { name: "Commit time after policy Since, controls meet policy -> Expected levels", policyContent: &fullPolicy, - controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: laterFixedTime.AsTime(), - Controls: l3ControlsWithExtras, + controlStatus: &slsa.ControlSetStatus{ + Time: laterFixedTime, + Controls: l3ControlsWithExtras.Controls, }, ghConnBranch: "main", expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, "ORG_SOURCE_TESTED"}, @@ -513,9 +518,9 @@ func TestEvaluateControl_Success(t *testing.T) { { name: "Branch not in policy, commit after default policy since -> Default policy (SLSA L1)", policyContent: &basicPolicy, - controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: laterFixedTime.AsTime(), - Controls: l3ControlsWithExtras, + controlStatus: &slsa.ControlSetStatus{ + Time: laterFixedTime, + Controls: l3ControlsWithExtras.Controls, }, ghConnBranch: "develop", expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, @@ -538,11 +543,11 @@ func TestEvaluateControl_Success(t *testing.T) { } } - verifiedLevels, policyPath, err := pe.EvaluateControl(ctx, &models.Repository{ - Hostname: "github.com", Path: "local/local", - }, &models.Branch{ - Name: tt.ghConnBranch, - }, tt.controlStatus) + verifiedLevels, policyPath, err := pe.EvaluateControl( + ctx, + &models.Repository{Hostname: "github.com", Path: "local/local"}, + &models.Branch{Name: tt.ghConnBranch}, tt.controlStatus, + ) if err != nil { t.Errorf("EvaluateControl() error = %v, want nil", err) } @@ -563,6 +568,7 @@ func TestEvaluateControl_Success(t *testing.T) { func TestEvaluateControl_Failure(t *testing.T) { now := timestamppb.Now() earlier := timestamppb.New(time.Now().Add(-time.Hour)) + rearlier := earlier.AsTime() later := timestamppb.New(time.Now().Add(time.Hour)) // Policies @@ -575,16 +581,16 @@ func TestEvaluateControl_Failure(t *testing.T) { tests := []struct { name string policyContent any - controlStatus *ghcontrol.GhControlStatus + controlStatus *slsa.ControlSetStatus ghConnBranch string expectedErrorContains string }{ { name: "Commit time after policy Since, controls DO NOT meet policy -> Error", policyContent: &policyL3Review, - controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: later.AsTime(), - Controls: controlsForLevel(slsa.SlsaSourceLevel2, earlier), + controlStatus: &slsa.ControlSetStatus{ + Time: later.AsTime(), + Controls: controlsForLevel(slsa.SlsaSourceLevel2, &rearlier).Controls, }, ghConnBranch: "main", expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_2", @@ -592,9 +598,8 @@ func TestEvaluateControl_Failure(t *testing.T) { { name: "Malformed JSON -> Error", policyContent: "not json", - controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: later.AsTime(), - Controls: slsa.Controls{}, + controlStatus: &slsa.ControlSetStatus{ + Time: later.AsTime(), }, ghConnBranch: "main", expectedErrorContains: "syntax error (line 1:1): invalid value", // Error from json.Unmarshal @@ -717,27 +722,27 @@ const ( func TestComputeEligibleSlsaLevel(t *testing.T) { tests := []struct { name string - controls slsa.Controls + controls *slsa.ControlSetStatus expectedLevel slsa.SlsaSourceLevel }{ { name: "SLSA Level 4", - controls: controlsForLevel(slsa.SlsaSourceLevel4, fixedTime), + controls: controlsForLevel(slsa.SlsaSourceLevel4, &fixedTime), expectedLevel: slsa.SlsaSourceLevel4, }, { name: "SLSA Level 3", - controls: controlsForLevel(slsa.SlsaSourceLevel3, fixedTime), + controls: controlsForLevel(slsa.SlsaSourceLevel3, &fixedTime), expectedLevel: slsa.SlsaSourceLevel3, }, { name: "SLSA Level 2", - controls: controlsForLevel(slsa.SlsaSourceLevel2, fixedTime), + controls: controlsForLevel(slsa.SlsaSourceLevel2, &fixedTime), expectedLevel: slsa.SlsaSourceLevel2, }, { name: "SLSA Level 1 - partial controls only", - controls: slsa.Controls{&provenance.Control{Name: slsa.SLSA_SOURCE_SCS_CONTINUITY.String(), Since: fixedTime}}, + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{&slsa.Control{Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, Since: &fixedTime, State: slsa.StateActive}}}, expectedLevel: slsa.SlsaSourceLevel1, }, { @@ -758,28 +763,28 @@ func TestComputeEligibleSlsaLevel(t *testing.T) { } func TestEvaluateBranchControls(t *testing.T) { - reviewEarlier := &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW.String(), Since: earlierFixedTime} - tagHygieneNow := &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS.String(), Since: fixedTime} + reviewEarlier := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, Since: &earlierFixedTime, State: slsa.StateActive} + tagHygieneNow := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, Since: &fixedTime, State: slsa.StateActive} // Branch Policies - policyL3Review := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, Since: fixedTime} - policyL4 := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: fixedTime} - policyL1NoExtras := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), RequireReview: false, Since: fixedTime} - policyL2Review := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: true, Since: fixedTime} - policyL2NoReview := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: false, Since: fixedTime} + policyL3Review := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, Since: timestamppb.New(fixedTime)} + policyL4 := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: timestamppb.New(fixedTime)} + policyL1NoExtras := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), RequireReview: false, Since: timestamppb.New(fixedTime)} + policyL2Review := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: true, Since: timestamppb.New(fixedTime)} + policyL2NoReview := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: false, Since: timestamppb.New(fixedTime)} // Tag policies - tagHygienePolicy := ProtectedTag{Since: fixedTime, TagHygiene: true} - noTagHygienePolicy := ProtectedTag{Since: fixedTime, TagHygiene: false} + tagHygienePolicy := ProtectedTag{Since: timestamppb.New(fixedTime), TagHygiene: true} + noTagHygienePolicy := ProtectedTag{Since: timestamppb.New(fixedTime), TagHygiene: false} // Policy Since 'earlier' for testing control.Since > policy.Since - policyL1Earlier := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), RequireReview: false, Since: earlierFixedTime} + policyL1Earlier := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), RequireReview: false, Since: timestamppb.New(earlierFixedTime)} tests := []struct { name string branchPolicy *ProtectedBranch tagPolicy *ProtectedTag - controls slsa.Controls + controls *slsa.ControlSetStatus expectedLevels slsa.SourceVerifiedLevels expectError bool expectedErrorContains string @@ -788,7 +793,7 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Success - L3, Review, Tags", branchPolicy: &policyL3Review, tagPolicy: &tagHygienePolicy, - controls: controlsForLevelWith(slsa.SlsaSourceLevel3, earlierFixedTime, reviewEarlier), + controls: controlsForLevelWith(slsa.SlsaSourceLevel3, &earlierFixedTime, reviewEarlier), expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, @@ -796,7 +801,7 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Success - L1", branchPolicy: &policyL1NoExtras, tagPolicy: &noTagHygienePolicy, - controls: controlsForLevel(slsa.SlsaSourceLevel1, earlierFixedTime), + controls: controlsForLevel(slsa.SlsaSourceLevel1, &earlierFixedTime), expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, expectError: false, }, @@ -804,7 +809,7 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Success - L2 & Review", branchPolicy: &policyL2Review, tagPolicy: &noTagHygienePolicy, - controls: controlsForLevelWith(slsa.SlsaSourceLevel2, earlierFixedTime, reviewEarlier), + controls: controlsForLevelWith(slsa.SlsaSourceLevel2, &earlierFixedTime, reviewEarlier), expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, expectError: false, }, @@ -812,8 +817,8 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Success - L2 & Tags", branchPolicy: &policyL2NoReview, tagPolicy: &tagHygienePolicy, - controls: controlsForLevelWith(slsa.SlsaSourceLevel2, earlierFixedTime, - &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS.String(), Since: earlierFixedTime}, + controls: controlsForLevelWith(slsa.SlsaSourceLevel2, &earlierFixedTime, + &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, Since: &earlierFixedTime, State: slsa.StateActive}, ), expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, @@ -822,7 +827,7 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Success - L4", branchPolicy: &policyL4, tagPolicy: &tagHygienePolicy, - controls: controlsForLevel(slsa.SlsaSourceLevel4, earlierFixedTime), + controls: controlsForLevel(slsa.SlsaSourceLevel4, &earlierFixedTime), expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel4), slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, @@ -830,7 +835,7 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Error - computeSlsaLevel Fails (Policy L3, Controls L1)", branchPolicy: &policyL3Review, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{}, + controls: &slsa.ControlSetStatus{}, expectedLevels: slsa.SourceVerifiedLevels{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", @@ -839,7 +844,7 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Error - computeReviewEnforced Fails (Policy L2+Review, Review control missing)", branchPolicy: &policyL2Review, tagPolicy: &noTagHygienePolicy, - controls: controlsForLevel(slsa.SlsaSourceLevel2, earlierFixedTime), + controls: controlsForLevel(slsa.SlsaSourceLevel2, &earlierFixedTime), expectedLevels: slsa.SourceVerifiedLevels{}, expectError: true, expectedErrorContains: "policy requires review, but that control is not enabled", @@ -847,8 +852,8 @@ func TestEvaluateBranchControls(t *testing.T) { { name: "Error - computeTagHygiene Fails (Policy L1+Tags, Tag control Since later than Policy Since)", branchPolicy: &policyL1Earlier, - tagPolicy: &ProtectedTag{Since: earlierFixedTime, TagHygiene: true}, - controls: controlsForLevelWith(slsa.SlsaSourceLevel1, earlierFixedTime, tagHygieneNow), + tagPolicy: &ProtectedTag{Since: timestamppb.New(earlierFixedTime), TagHygiene: true}, + controls: controlsForLevelWith(slsa.SlsaSourceLevel1, &earlierFixedTime, tagHygieneNow), expectedLevels: slsa.SourceVerifiedLevels{}, expectError: true, expectedErrorContains: "policy requires tag hygiene since", @@ -857,7 +862,7 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Success - Mixed Requirements (L3, Review, No Tags)", branchPolicy: &policyL3Review, tagPolicy: &noTagHygienePolicy, - controls: controlsForLevelWith(slsa.SlsaSourceLevel3, earlierFixedTime, reviewEarlier), + controls: controlsForLevelWith(slsa.SlsaSourceLevel3, &earlierFixedTime, reviewEarlier), expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, expectError: false, }, @@ -907,6 +912,7 @@ func TestEvaluateBranchControls(t *testing.T) { func TestComputeTagHygiene(t *testing.T) { now := timestamppb.Now() + tnow := now.AsTime() earlier := timestamppb.New(time.Now().Add(-time.Hour)) // Branch Policies @@ -915,12 +921,12 @@ func TestComputeTagHygiene(t *testing.T) { policyNotRequiresTagHygiene := ProtectedTag{TagHygiene: false, Since: now} // Controls - tagHygieneControlEnabledNow := &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS.String(), Since: now} + tagHygieneControlEnabledNow := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, Since: &tnow, State: slsa.StateActive} tests := []struct { name string tagPolicy *ProtectedTag - controls slsa.Controls + controls *slsa.ControlSetStatus expectedControls []slsa.ControlName expectError bool expectedErrorContains string @@ -928,29 +934,29 @@ func TestComputeTagHygiene(t *testing.T) { { name: "Policy requires tag hygiene, control compliant (Policy.Since >= Control.Since)", tagPolicy: &policyRequiresTagHygieneNow, - controls: slsa.Controls{tagHygieneControlEnabledNow}, // Policy.Since == Control.Since + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}, // Policy.Since == Control.Since expectedControls: []slsa.ControlName{slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, { name: "Policy does not require tag hygiene - control state irrelevant", tagPolicy: &policyNotRequiresTagHygiene, - controls: slsa.Controls{}, // Control state explicitly shown as irrelevant + controls: &slsa.ControlSetStatus{}, // Control state explicitly shown as irrelevant expectedControls: []slsa.ControlName{}, expectError: false, }, { name: "Policy requires tag hygiene, control not present: fail", tagPolicy: &policyRequiresTagHygieneNow, - controls: slsa.Controls{}, // Tag Hygiene control missing + controls: &slsa.ControlSetStatus{}, // Tag Hygiene control missing expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires tag hygiene, but that control is not enabled", }, { name: "Policy requires tag hygiene, control enabled, Policy.Since < Control.Since: fail", - tagPolicy: &policyRequiresTagHygieneEarlier, // Policy.Since is 'earlier' - controls: slsa.Controls{tagHygieneControlEnabledNow}, // Control.Since is 'now' + tagPolicy: &policyRequiresTagHygieneEarlier, // Policy.Since is 'earlier' + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}, // Control.Since is 'now' expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires tag hygiene since", // ...but that control has only been enabled since... @@ -982,6 +988,7 @@ func TestComputeTagHygiene(t *testing.T) { func TestComputeReviewEnforced(t *testing.T) { now := timestamppb.Now() + tnow := now.AsTime() earlier := timestamppb.New(time.Now().Add(-time.Hour)) // Branch Policies @@ -990,12 +997,12 @@ func TestComputeReviewEnforced(t *testing.T) { policyNotRequiresReview := ProtectedBranch{RequireReview: false, Since: now} // Controls - reviewControlEnabledNow := &provenance.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW.String(), Since: now} + reviewControlEnabledNow := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, Since: &tnow, State: slsa.StateActive} tests := []struct { name string branchPolicy *ProtectedBranch - controls slsa.Controls + controls *slsa.ControlSetStatus expectedControls []slsa.ControlName expectError bool expectedErrorContains string @@ -1003,29 +1010,29 @@ func TestComputeReviewEnforced(t *testing.T) { { name: "Policy requires review, control compliant (Policy.Since >= Control.Since)", branchPolicy: &policyRequiresReviewNow, - controls: slsa.Controls{reviewControlEnabledNow}, // Policy.Since == Control.Since + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{reviewControlEnabledNow}}, // Policy.Since == Control.Since expectedControls: []slsa.ControlName{slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, expectError: false, }, { name: "Policy does not require review - control state irrelevant", branchPolicy: &policyNotRequiresReview, - controls: slsa.Controls{}, // Control state explicitly shown as irrelevant + controls: &slsa.ControlSetStatus{}, // Control state explicitly shown as irrelevant expectedControls: []slsa.ControlName{}, expectError: false, }, { name: "Policy requires review, control not present: fail", branchPolicy: &policyRequiresReviewNow, - controls: slsa.Controls{}, // Review control missing + controls: &slsa.ControlSetStatus{}, // Review control missing expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires review, but that control is not enabled", }, { name: "Policy requires review, control enabled, Policy.Since < Control.Since: fail", - branchPolicy: &policyRequiresReviewEarlier, // Policy.Since is 'earlier' - controls: slsa.Controls{reviewControlEnabledNow}, // Control.Since is 'now' + branchPolicy: &policyRequiresReviewEarlier, // Policy.Since is 'earlier' + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{reviewControlEnabledNow}}, // Control.Since is 'now' expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires review since", // ...but that control has only been enabled since... @@ -1057,6 +1064,7 @@ func TestComputeReviewEnforced(t *testing.T) { func TestComputeOrgControls(t *testing.T) { now := timestamppb.Now() + tnow := now.AsTime() earlier := timestamppb.New(time.Now().Add(-time.Hour)) // StatusCheckControls @@ -1066,14 +1074,14 @@ func TestComputeOrgControls(t *testing.T) { invalidPropertyNameControlPolicy := &OrgStatusCheckControl{PropertyName: "SLSA_TESTED", Since: now, CheckName: "test"} // Controls - testedControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_test", Since: now} - lintedControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_run-the-linter", Since: now} - notListedControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_not-configured-in-policy", Since: now} + testedControl := &slsa.Control{Name: "GH_REQUIRED_CHECK_test", Since: &tnow, State: slsa.StateActive} + lintedControl := &slsa.Control{Name: "GH_REQUIRED_CHECK_run-the-linter", Since: &tnow, State: slsa.StateActive} + notListedControl := &slsa.Control{Name: "GH_REQUIRED_CHECK_not-configured-in-policy", Since: &tnow, State: slsa.StateActive} tests := []struct { name string orgCheckPolicies []*OrgStatusCheckControl - controls slsa.Controls + controls *slsa.ControlSetStatus expectedControls []slsa.ControlName expectError bool expectedErrorContains string @@ -1081,28 +1089,28 @@ func TestComputeOrgControls(t *testing.T) { { name: "Single check handled", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy}, - controls: slsa.Controls{testedControl}, + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{testedControl}}, expectedControls: []slsa.ControlName{"ORG_SOURCE_TESTED"}, expectError: false, }, { name: "Multiple checks handled", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy, lintedControlPolicy}, - controls: slsa.Controls{testedControl, lintedControl}, + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{testedControl, lintedControl}}, expectedControls: []slsa.ControlName{"ORG_SOURCE_TESTED", "ORG_SOURCE_LINTED"}, expectError: false, }, { name: "Not configured control should not be returned", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy}, - controls: slsa.Controls{testedControl, notListedControl}, + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{testedControl, notListedControl}}, expectedControls: []slsa.ControlName{"ORG_SOURCE_TESTED"}, expectError: false, }, { name: "Policy requires control but it is not present", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy, lintedControlPolicy}, - controls: slsa.Controls{lintedControl}, + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{lintedControl}}, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires check 'test', but", @@ -1110,7 +1118,7 @@ func TestComputeOrgControls(t *testing.T) { { name: "Control not enabled long enough fails", orgCheckPolicies: []*OrgStatusCheckControl{earlierTestedControlPolicy}, - controls: slsa.Controls{testedControl}, + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{testedControl}}, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires check 'test' since", @@ -1118,7 +1126,7 @@ func TestComputeOrgControls(t *testing.T) { { name: "Invalid property name fails", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy, invalidPropertyNameControlPolicy}, - controls: slsa.Controls{testedControl}, + controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{testedControl}}, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy specifies an invalid property name", @@ -1150,19 +1158,22 @@ func TestComputeOrgControls(t *testing.T) { } func TestComputeSlsaLevel(t *testing.T) { - now := timestamppb.Now() - earlier := timestamppb.New(time.Now().Add(-time.Hour)) + //now := timestamppb.Now() + rnow := time.Now() + now := &rnow + rearlier := time.Now().Add(-time.Hour) + earlier := &rearlier // Branch Policies - policyL4Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: now} - policyL3Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: now} - policyL2Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), Since: now} - policyUnknownLevel := ProtectedBranch{TargetSlsaSourceLevel: "UNKNOWN_LEVEL", Since: now} + policyL4Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: timestamppb.New(rnow)} + policyL3Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: timestamppb.New(rnow)} + policyL2Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), Since: timestamppb.New(rnow)} + policyUnknownLevel := ProtectedBranch{TargetSlsaSourceLevel: "UNKNOWN_LEVEL", Since: timestamppb.New(rnow)} tests := []struct { name string branchPolicy *ProtectedBranch - controls slsa.Controls + controls *slsa.ControlSetStatus expectedLevels []slsa.ControlName expectError bool expectedErrorContains string @@ -1184,7 +1195,7 @@ func TestComputeSlsaLevel(t *testing.T) { { name: "Controls L1-eligible, Policy L2: fail (eligibility)", branchPolicy: &policyL2Now, - controls: slsa.Controls{}, + controls: &slsa.ControlSetStatus{}, expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", @@ -1198,7 +1209,7 @@ func TestComputeSlsaLevel(t *testing.T) { }, { name: "Controls L4-eligible (since 'now'), Policy L4 (since 'earlier'): fail (Policy.Since too early)", - branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: earlier}, + branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: timestamppb.New(*earlier)}, controls: controlsForLevel(slsa.SlsaSourceLevel4, now), expectedLevels: []slsa.ControlName{}, expectError: true, @@ -1206,7 +1217,7 @@ func TestComputeSlsaLevel(t *testing.T) { }, { name: "Controls L3-eligible (since 'now'), Policy L3 (since 'earlier'): fail (Policy.Since too early)", - branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: earlier}, + branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: timestamppb.New(*earlier)}, controls: controlsForLevel(slsa.SlsaSourceLevel3, now), expectedLevels: []slsa.ControlName{}, expectError: true, @@ -1214,7 +1225,7 @@ func TestComputeSlsaLevel(t *testing.T) { }, { name: "Controls L2-eligible (since 'now'), Policy L2 (since 'earlier'): fail (Policy.Since too early)", - branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), Since: earlier}, + branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), Since: timestamppb.New(*earlier)}, controls: controlsForLevel(slsa.SlsaSourceLevel2, now), expectedLevels: []slsa.ControlName{}, expectError: true, @@ -1231,7 +1242,7 @@ func TestComputeSlsaLevel(t *testing.T) { { name: "Controls L1-eligible, Policy L3: fail (eligibility)", branchPolicy: &policyL3Now, - controls: slsa.Controls{}, + controls: &slsa.ControlSetStatus{}, expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", @@ -1263,124 +1274,124 @@ func TestComputeSlsaLevel(t *testing.T) { // controlsForLevelWithOverride creates controls for a level, then overrides the // Since timestamp for a specific control name. -func controlsForLevelWithOverride(level slsa.SlsaSourceLevel, baseSince *timestamppb.Timestamp, overrides map[slsa.ControlName]*timestamppb.Timestamp) slsa.Controls { +func controlsForLevelWithOverride(level slsa.SlsaSourceLevel, baseSince *time.Time, overrides map[slsa.ControlName]time.Time) *slsa.ControlSetStatus { controls := controlsForLevel(level, baseSince) - for i, c := range controls { + for i, c := range controls.Controls { if since, ok := overrides[slsa.ControlName(c.GetName())]; ok { - controls[i] = &provenance.Control{Name: c.GetName(), Since: since} + controls.Controls[i] = &slsa.Control{Name: c.GetName(), Since: &since, State: slsa.StateActive} } } return controls } func TestComputeEligibleSince(t *testing.T) { - time1 := timestamppb.Now() - time2 := timestamppb.New(time.Now().Add(time.Hour)) - zeroTime := timestamppb.New(time.Time{}) + time1 := time.Now() + time2 := time.Now().Add(time.Hour) + zeroTime := time.Time{} tests := []struct { name string - controls slsa.Controls + controls *slsa.ControlSetStatus level slsa.SlsaSourceLevel - expectedTime *timestamppb.Timestamp + expectedTime *time.Time expectError bool expectedError string }{ { name: "L4 eligible (two controls later)", - controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel4, time1, map[slsa.ControlName]*timestamppb.Timestamp{ + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel4, &time1, map[slsa.ControlName]time.Time{ slsa.SLSA_SOURCE_SCS_PROVENANCE: time2, slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW: time2, }), level: slsa.SlsaSourceLevel4, - expectedTime: time2, + expectedTime: &time2, expectError: false, }, { name: "L4 eligible (one control later)", - controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel4, time1, map[slsa.ControlName]*timestamppb.Timestamp{ + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel4, &time1, map[slsa.ControlName]time.Time{ slsa.SLSA_SOURCE_SCS_CONTINUITY: time2, }), level: slsa.SlsaSourceLevel4, - expectedTime: time2, + expectedTime: &time2, expectError: false, }, { name: "L3 eligible (ProvLater), L3 requested: expect Prov.Since", - controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, time1, map[slsa.ControlName]*timestamppb.Timestamp{ + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, &time1, map[slsa.ControlName]time.Time{ slsa.SLSA_SOURCE_SCS_PROVENANCE: time2, }), level: slsa.SlsaSourceLevel3, - expectedTime: time2, + expectedTime: &time2, expectError: false, }, { name: "L3 eligible (ContLater), L3 requested: expect Cont.Since", - controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, time1, map[slsa.ControlName]*timestamppb.Timestamp{ + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, &time1, map[slsa.ControlName]time.Time{ slsa.SLSA_SOURCE_SCS_CONTINUITY: time2, }), level: slsa.SlsaSourceLevel3, - expectedTime: time2, + expectedTime: &time2, expectError: false, }, { name: "L2 eligible, L2 requested: expect base time", - controls: controlsForLevel(slsa.SlsaSourceLevel2, time1), + controls: controlsForLevel(slsa.SlsaSourceLevel2, &time1), level: slsa.SlsaSourceLevel2, - expectedTime: time1, + expectedTime: &time1, expectError: false, }, { name: "L1 eligible, L1 requested: expect ZeroTime", - controls: controlsForLevel(slsa.SlsaSourceLevel1, zeroTime), + controls: controlsForLevel(slsa.SlsaSourceLevel1, &zeroTime), level: slsa.SlsaSourceLevel1, - expectedTime: zeroTime, + expectedTime: &zeroTime, expectError: false, }, { name: "L3 eligible, L2 requested: expect base time", - controls: controlsForLevel(slsa.SlsaSourceLevel3, time1), + controls: controlsForLevel(slsa.SlsaSourceLevel3, &time1), level: slsa.SlsaSourceLevel2, - expectedTime: time1, + expectedTime: &time1, expectError: false, }, { name: "L2 eligible, L3 requested: expect nil, no error", - controls: controlsForLevel(slsa.SlsaSourceLevel2, time1), + controls: controlsForLevel(slsa.SlsaSourceLevel2, &time1), level: slsa.SlsaSourceLevel3, expectedTime: nil, expectError: false, }, { name: "Unknown level requested: expect nil, error", - controls: slsa.Controls{}, + controls: &slsa.ControlSetStatus{}, level: slsa.SlsaSourceLevel("UNKNOWN_LEVEL"), - expectedTime: zeroTime, + expectedTime: &zeroTime, expectError: false, }, { name: "L3 eligible (ContZero, ProvNonZero), L3 requested: expect Prov.Since", - controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, zeroTime, map[slsa.ControlName]*timestamppb.Timestamp{ + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, &zeroTime, map[slsa.ControlName]time.Time{ slsa.SLSA_SOURCE_SCS_PROVENANCE: time2, }), level: slsa.SlsaSourceLevel3, - expectedTime: time2, + expectedTime: &time2, expectError: false, }, { name: "L3 eligible (ContNonZero, ProvZero), L3 requested: expect Cont.Since", - controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, zeroTime, map[slsa.ControlName]*timestamppb.Timestamp{ + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, &zeroTime, map[slsa.ControlName]time.Time{ slsa.SLSA_SOURCE_SCS_CONTINUITY: time1, }), level: slsa.SlsaSourceLevel3, - expectedTime: time1, + expectedTime: &time1, expectError: false, }, { name: "L3 eligible (AllZero), L3 requested: expect ZeroTime", - controls: controlsForLevel(slsa.SlsaSourceLevel3, zeroTime), + controls: controlsForLevel(slsa.SlsaSourceLevel3, &zeroTime), level: slsa.SlsaSourceLevel3, - expectedTime: zeroTime, + expectedTime: &zeroTime, expectError: false, }, } @@ -1408,7 +1419,7 @@ func TestComputeEligibleSince(t *testing.T) { } else { if gotTime == nil { t.Errorf("computeEligibleSince() gotTime = nil, want %v", tt.expectedTime) - } else if !gotTime.Equal(tt.expectedTime.AsTime()) { + } else if !gotTime.Equal(*tt.expectedTime) { t.Errorf("computeEligibleSince() gotTime = %v, want %v", *gotTime, tt.expectedTime) } } @@ -1476,7 +1487,7 @@ func TestGetPolicy_Local_NotFoundCases(t *testing.T) { branchName: "develop", policyToCreate: RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ - {Name: "feature", Since: fixedTime, TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2)}, + {Name: "feature", Since: timestamppb.New(fixedTime), TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2)}, }, }, }, @@ -1620,7 +1631,7 @@ func TestGetPolicy_Remote_NotFoundCases(t *testing.T) { mockHTTPStatus: http.StatusOK, mockPolicyContent: &RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ - {Name: "main", Since: fixedTime, TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3)}, + {Name: "main", Since: timestamppb.New(fixedTime), TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3)}, }, }, expectedPolicyPath: mockPolicyPath, diff --git a/pkg/slsa/controls.go b/pkg/slsa/controls.go index f2bf12b..2bca6c4 100644 --- a/pkg/slsa/controls.go +++ b/pkg/slsa/controls.go @@ -13,6 +13,10 @@ type ( ControlSet []ControlName ) +func (c ControlName) String() string { + return string(c) +} + const ( // Control constants DEPRECATED_ContinuityEnforced ControlName = "CONTINUITY_ENFORCED" diff --git a/pkg/slsa/levels.go b/pkg/slsa/levels.go index 065764b..bbba9a9 100644 --- a/pkg/slsa/levels.go +++ b/pkg/slsa/levels.go @@ -3,6 +3,17 @@ package slsa +type ( + SlsaSourceLevel ControlName +) + +const ( + SlsaSourceLevel1 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_1" + SlsaSourceLevel2 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_2" + SlsaSourceLevel3 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_3" + SlsaSourceLevel4 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_4" +) + type Level struct { Level uint8 Controls ControlSet diff --git a/pkg/slsa/slsa_types.go b/pkg/slsa/slsa_types.go index 78189da..a946cdb 100644 --- a/pkg/slsa/slsa_types.go +++ b/pkg/slsa/slsa_types.go @@ -7,25 +7,11 @@ import ( "slices" "time" - "google.golang.org/protobuf/types/known/timestamppb" - "github.com/slsa-framework/source-tool/pkg/provenance" + "google.golang.org/protobuf/types/known/timestamppb" ) -type ( - SlsaSourceLevel ControlName -) - -func (c ControlName) String() string { - return string(c) -} - const ( - SlsaSourceLevel1 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_1" - SlsaSourceLevel2 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_2" - SlsaSourceLevel3 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_3" - SlsaSourceLevel4 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_4" - SourceBranchesAnnotation = "source_branches" SourceRefsAnnotation = "source_refs" AllowedOrgPropPrefix = "ORG_SOURCE_" @@ -48,50 +34,6 @@ func IsLevelHigherOrEqualTo(level1, level2 SlsaSourceLevel) bool { return level1 >= level2 } -type Controls []*provenance.Control - -// Adds the control to the list. Ignores nil controls. -// Does not check for duplicate controls. -func (controls *Controls) AddControl(newControls ...*provenance.Control) { - if controls == nil { - controls = &Controls{} - } - for _, c := range newControls { - if c == nil { - continue - } - *controls = append(*controls, c) - } -} - -// Gets the control with the corresponding name, returns nil if not found. -func (controls *Controls) GetControl(name ControlName) *provenance.Control { - for _, control := range *controls { - if control.GetName() == name.String() { - return control - } - } - return nil -} - -func (controls *Controls) AreControlsAvailable(names []ControlName) bool { - for _, name := range names { - if controls.GetControl(name) == nil { - return false - } - } - return true -} - -// Returns the names of the controls. -func (controls *Controls) Names() []ControlName { - names := make([]ControlName, len(*controls)) - for i := range *controls { - names[i] = ControlName((*controls)[i].GetName()) - } - return names -} - // These can be any string, not just SlsaLevels type SourceVerifiedLevels []ControlName @@ -126,16 +68,32 @@ func ControlNamesToStrings(controlNames []ControlName) []string { return strs } +func NewControlSetFromProvanenaceControls(provControls []*provenance.Control) *ControlSetStatus { + set := &ControlSetStatus{ + Controls: []*Control{}, + } + + for _, ctl := range provControls { + t := ctl.Since.AsTime() + set.Controls = append(set.Controls, &Control{ + Name: ControlName(ctl.Name), + State: StateActive, + Since: &t, + }) + } + return set +} + // NewControlStatus returns a new control status object initialized with // all existing controls in not_enabled state. func NewControlSetStatus() *ControlSetStatus { status := &ControlSetStatus{ Time: time.Now(), - Controls: []ControlStatus{}, + Controls: []*Control{}, } for _, c := range AllLevelControls { - status.Controls = append(status.Controls, ControlStatus{ + status.Controls = append(status.Controls, &Control{ Name: c, State: StateNotEnabled, }) @@ -150,11 +108,11 @@ type ControlSetStatus struct { RepoUri string Branch string Time time.Time - Controls []ControlStatus + Controls []*Control } -// ControlStatus captures the status of a control as seen from a VCS system -type ControlStatus struct { +// Control captures the status of a control as seen from a VCS system +type Control struct { Name ControlName State ControlState `json:"control_state"` Since *time.Time `json:"since,omitempty"` @@ -162,6 +120,14 @@ type ControlStatus struct { RecommendedAction *ControlRecommendedAction } +func (cs *Control) GetName() ControlName { + return cs.Name +} + +func (cs *Control) GetSince() *time.Time { + return cs.Since +} + // ControlRecommendedAction captures the recommended action to complete // a control's implementation. type ControlRecommendedAction struct { @@ -171,16 +137,14 @@ type ControlRecommendedAction struct { // GetActiveControls returns a Controls collection with all the controls // which are active in the set. -func (cs *ControlSetStatus) GetActiveControls() *Controls { - ret := Controls{} +func (cs *ControlSetStatus) GetActiveControls() *ControlSetStatus { + ret := ControlSetStatus{} if cs == nil { return &ret } for _, c := range cs.Controls { if c.State == StateActive { - ret.AddControl(&provenance.Control{ - Name: c.Name.String(), Since: timestamppb.New(*c.Since), - }) + ret.AddControl(c) } } return &ret @@ -195,3 +159,63 @@ func (cs *ControlSetStatus) SetControlState(ctrlName ControlName, state ControlS } } } + +// Adds the control to the list. Ignores nil controls. +// Does not check for duplicate controls. +func (cs *ControlSetStatus) AddControl(newControls ...*Control) { + if cs == nil { + cs = &ControlSetStatus{} + } + for _, c := range newControls { + if c == nil { + continue + } + cs.Controls = append(cs.Controls, c) + } +} + +// Gets the control with the corresponding name, returns nil if not found. +func (cs *ControlSetStatus) GetControl(name ControlName) *Control { + for _, control := range cs.Controls { + if control.GetName() == name { + return control + } + } + return nil +} + +// This checks if the controls are present in the array. But As we merged the +// controls array with the struct we also check if they are all active +func (cs *ControlSetStatus) AreControlsAvailable(names []ControlName) bool { + for _, name := range names { + ctl := cs.GetControl(name) + if ctl == nil || ctl.State != StateActive { + return false + } + } + return true +} + +// Returns the names of the controls. +func (cs *ControlSetStatus) Names() []ControlName { + names := make([]ControlName, len(cs.Controls)) + for i := range cs.Controls { + names[i] = cs.Controls[i].GetName() + } + return names +} + +func (cs *ControlSetStatus) ToProvenanceControls() []*provenance.Control { + var ret = []*provenance.Control{} + for _, ctl := range cs.Controls { + if ctl.State != StateActive { + continue + } + c := &provenance.Control{ + Name: ctl.Name.String(), + Since: timestamppb.New(*ctl.Since), + } + ret = append(ret, c) + } + return ret +} diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index 68b6df0..dd3a670 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -13,7 +13,6 @@ import ( "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/auth" "github.com/slsa-framework/source-tool/pkg/ghcontrol" - "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) @@ -159,13 +158,13 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. return nil, fmt.Errorf("attempting to read provenance from commit %q: %w", commit.SHA, err) } if attestation != nil { - activeControls.AddControl(&provenance.Control{ - Name: slsa.SLSA_SOURCE_SCS_PROVENANCE.String(), + activeControls.AddControl(&slsa.Control{ + Name: slsa.SLSA_SOURCE_SCS_PROVENANCE, }) // If we got the provenance attestaion, we assume we also have a VSA - activeControls.AddControl(&provenance.Control{ - Name: slsa.SLSA_SOURCE_SCS_VSA.String(), + activeControls.AddControl(&slsa.Control{ + Name: slsa.SLSA_SOURCE_SCS_VSA, }) } else { log.Printf("No provenance attestation found on %s", commit.SHA) @@ -186,8 +185,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. // Check if it's one of the active controls if c := activeControls.GetControl(ctrl.Name); c != nil { - t := c.GetSince().AsTime() - status.Controls[i].Since = &t + status.Controls[i].Since = ctrl.Since status.Controls[i].State = slsa.StateActive status.Controls[i].Message = b.controlImplementationMessage(slsa.ControlName(c.GetName())) } @@ -196,8 +194,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. // Without force push, content cannot be expunged. if ctrl.Name == slsa.SLSA_SOURCE_ORG_SAFE_EXPUNGE { if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS); c != nil { - t := c.GetSince().AsTime() - status.Controls[i].Since = &t + status.Controls[i].Since = ctrl.Since status.Controls[i].State = slsa.StateActive status.Controls[i].Message = b.controlImplementationMessage(ctrl.Name) } @@ -206,8 +203,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. // Enable ORG_CONTINUITY when SCS branch continuity is active. if ctrl.Name == slsa.SLSA_SOURCE_ORG_CONTINUITY { if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_CONTINUITY); c != nil { - t := c.GetSince().AsTime() - status.Controls[i].Since = &t + status.Controls[i].Since = ctrl.Since status.Controls[i].State = slsa.StateActive status.Controls[i].Message = b.controlImplementationMessage(ctrl.Name) } @@ -266,7 +262,7 @@ func (b *Backend) controlImplementationMessage(ctrlName slsa.ControlName) string } } -func (b *Backend) GetTagControls(context.Context, *models.Tag) (*slsa.Controls, error) { +func (b *Backend) GetTagControls(context.Context, *models.Tag) (*slsa.ControlSetStatus, error) { return nil, fmt.Errorf("not yet implemented") } diff --git a/pkg/sourcetool/implementation.go b/pkg/sourcetool/implementation.go index 0625dd9..d493b50 100644 --- a/pkg/sourcetool/implementation.go +++ b/pkg/sourcetool/implementation.go @@ -40,7 +40,7 @@ type toolImplementation interface { GetBranchControls(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSetStatus, error) GetBranchControlsAtCommit(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error) ConfigureControls(models.VcsBackend, *models.Repository, []*models.Branch, []models.ControlConfiguration) error - GetPolicyStatus(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.ControlStatus, error) + GetPolicyStatus(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.Control, error) CreateRepositoryFork(context.Context, *auth.Authenticator, *models.Repository, string) error } @@ -232,7 +232,7 @@ func (impl *defaultToolImplementation) SearchPullRequest(ctx context.Context, a // GetPolicyStatus returns the status of the policy as a slsa ControlStatus func (impl *defaultToolImplementation) GetPolicyStatus( ctx context.Context, a *auth.Authenticator, opts *options.Options, r *models.Repository, -) (*slsa.ControlStatus, error) { +) (*slsa.Control, error) { // First: Look for the policy. If found then we are done pcy, _, err := policy.NewPolicyEvaluator().GetPolicy(ctx, r) if err != nil { @@ -244,7 +244,7 @@ func (impl *defaultToolImplementation) GetPolicyStatus( if len(pcy.GetProtectedBranches()) > 0 { t = pcy.GetProtectedBranches()[0].GetSince().AsTime() } - return &slsa.ControlStatus{ + return &slsa.Control{ Name: slsa.PolicyAvailable, State: slsa.StateActive, Since: &t, @@ -276,7 +276,7 @@ func (impl *defaultToolImplementation) GetPolicyStatus( // No pull request found. Not implemented if prNr == nil { - return &slsa.ControlStatus{ + return &slsa.Control{ Name: slsa.PolicyAvailable, State: slsa.StateNotEnabled, Since: nil, @@ -288,7 +288,7 @@ func (impl *defaultToolImplementation) GetPolicyStatus( }, nil } - return &slsa.ControlStatus{ + return &slsa.Control{ Name: slsa.PolicyAvailable, State: slsa.StateInProgress, Since: prNr.Time, diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index 5805745..f7df48e 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -41,7 +41,7 @@ type AttestationStorageReader interface { type VcsBackend interface { GetBranchControls(context.Context, *Branch) (*slsa.ControlSetStatus, error) GetBranchControlsAtCommit(context.Context, *Branch, *Commit) (*slsa.ControlSetStatus, error) - GetTagControls(context.Context, *Tag) (*slsa.Controls, error) + GetTagControls(context.Context, *Tag) (*slsa.ControlSetStatus, error) ControlConfigurationDescr(*Branch, ControlConfiguration) string ConfigureControls(*Repository, []*Branch, []ControlConfiguration) error GetLatestCommit(context.Context, *Repository, *Branch) (*Commit, error) diff --git a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go index 0543dba..88a30ad 100644 --- a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go +++ b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go @@ -98,18 +98,18 @@ type FakeVcsBackend struct { result1 *models.Commit result2 error } - GetTagControlsStub func(context.Context, *models.Tag) (*slsa.Controls, error) + GetTagControlsStub func(context.Context, *models.Tag) (*slsa.ControlSetStatus, error) getTagControlsMutex sync.RWMutex getTagControlsArgsForCall []struct { arg1 context.Context arg2 *models.Tag } getTagControlsReturns struct { - result1 *slsa.Controls + result1 *slsa.ControlSetStatus result2 error } getTagControlsReturnsOnCall map[int]struct { - result1 *slsa.Controls + result1 *slsa.ControlSetStatus result2 error } invocations map[string][][]interface{} @@ -525,7 +525,7 @@ func (fake *FakeVcsBackend) GetLatestCommitReturnsOnCall(i int, result1 *models. }{result1, result2} } -func (fake *FakeVcsBackend) GetTagControls(arg1 context.Context, arg2 *models.Tag) (*slsa.Controls, error) { +func (fake *FakeVcsBackend) GetTagControls(arg1 context.Context, arg2 *models.Tag) (*slsa.ControlSetStatus, error) { fake.getTagControlsMutex.Lock() ret, specificReturn := fake.getTagControlsReturnsOnCall[len(fake.getTagControlsArgsForCall)] fake.getTagControlsArgsForCall = append(fake.getTagControlsArgsForCall, struct { @@ -551,7 +551,7 @@ func (fake *FakeVcsBackend) GetTagControlsCallCount() int { return len(fake.getTagControlsArgsForCall) } -func (fake *FakeVcsBackend) GetTagControlsCalls(stub func(context.Context, *models.Tag) (*slsa.Controls, error)) { +func (fake *FakeVcsBackend) GetTagControlsCalls(stub func(context.Context, *models.Tag) (*slsa.ControlSetStatus, error)) { fake.getTagControlsMutex.Lock() defer fake.getTagControlsMutex.Unlock() fake.GetTagControlsStub = stub @@ -564,28 +564,28 @@ func (fake *FakeVcsBackend) GetTagControlsArgsForCall(i int) (context.Context, * return argsForCall.arg1, argsForCall.arg2 } -func (fake *FakeVcsBackend) GetTagControlsReturns(result1 *slsa.Controls, result2 error) { +func (fake *FakeVcsBackend) GetTagControlsReturns(result1 *slsa.ControlSetStatus, result2 error) { fake.getTagControlsMutex.Lock() defer fake.getTagControlsMutex.Unlock() fake.GetTagControlsStub = nil fake.getTagControlsReturns = struct { - result1 *slsa.Controls + result1 *slsa.ControlSetStatus result2 error }{result1, result2} } -func (fake *FakeVcsBackend) GetTagControlsReturnsOnCall(i int, result1 *slsa.Controls, result2 error) { +func (fake *FakeVcsBackend) GetTagControlsReturnsOnCall(i int, result1 *slsa.ControlSetStatus, result2 error) { fake.getTagControlsMutex.Lock() defer fake.getTagControlsMutex.Unlock() fake.GetTagControlsStub = nil if fake.getTagControlsReturnsOnCall == nil { fake.getTagControlsReturnsOnCall = make(map[int]struct { - result1 *slsa.Controls + result1 *slsa.ControlSetStatus result2 error }) } fake.getTagControlsReturnsOnCall[i] = struct { - result1 *slsa.Controls + result1 *slsa.ControlSetStatus result2 error }{result1, result2} } diff --git a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go index 628580e..8f503f2 100644 --- a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go +++ b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go @@ -123,7 +123,7 @@ type FakeToolImplementation struct { result1 *slsa.ControlSetStatus result2 error } - GetPolicyStatusStub func(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.ControlStatus, error) + GetPolicyStatusStub func(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.Control, error) getPolicyStatusMutex sync.RWMutex getPolicyStatusArgsForCall []struct { arg1 context.Context @@ -132,11 +132,11 @@ type FakeToolImplementation struct { arg4 *models.Repository } getPolicyStatusReturns struct { - result1 *slsa.ControlStatus + result1 *slsa.Control result2 error } getPolicyStatusReturnsOnCall map[int]struct { - result1 *slsa.ControlStatus + result1 *slsa.Control result2 error } GetVcsBackendStub func(*models.Repository) (models.VcsBackend, error) @@ -708,7 +708,7 @@ func (fake *FakeToolImplementation) GetBranchControlsAtCommitReturnsOnCall(i int }{result1, result2} } -func (fake *FakeToolImplementation) GetPolicyStatus(arg1 context.Context, arg2 *auth.Authenticator, arg3 *options.Options, arg4 *models.Repository) (*slsa.ControlStatus, error) { +func (fake *FakeToolImplementation) GetPolicyStatus(arg1 context.Context, arg2 *auth.Authenticator, arg3 *options.Options, arg4 *models.Repository) (*slsa.Control, error) { fake.getPolicyStatusMutex.Lock() ret, specificReturn := fake.getPolicyStatusReturnsOnCall[len(fake.getPolicyStatusArgsForCall)] fake.getPolicyStatusArgsForCall = append(fake.getPolicyStatusArgsForCall, struct { @@ -736,7 +736,7 @@ func (fake *FakeToolImplementation) GetPolicyStatusCallCount() int { return len(fake.getPolicyStatusArgsForCall) } -func (fake *FakeToolImplementation) GetPolicyStatusCalls(stub func(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.ControlStatus, error)) { +func (fake *FakeToolImplementation) GetPolicyStatusCalls(stub func(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.Control, error)) { fake.getPolicyStatusMutex.Lock() defer fake.getPolicyStatusMutex.Unlock() fake.GetPolicyStatusStub = stub @@ -749,28 +749,28 @@ func (fake *FakeToolImplementation) GetPolicyStatusArgsForCall(i int) (context.C return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 } -func (fake *FakeToolImplementation) GetPolicyStatusReturns(result1 *slsa.ControlStatus, result2 error) { +func (fake *FakeToolImplementation) GetPolicyStatusReturns(result1 *slsa.Control, result2 error) { fake.getPolicyStatusMutex.Lock() defer fake.getPolicyStatusMutex.Unlock() fake.GetPolicyStatusStub = nil fake.getPolicyStatusReturns = struct { - result1 *slsa.ControlStatus + result1 *slsa.Control result2 error }{result1, result2} } -func (fake *FakeToolImplementation) GetPolicyStatusReturnsOnCall(i int, result1 *slsa.ControlStatus, result2 error) { +func (fake *FakeToolImplementation) GetPolicyStatusReturnsOnCall(i int, result1 *slsa.Control, result2 error) { fake.getPolicyStatusMutex.Lock() defer fake.getPolicyStatusMutex.Unlock() fake.GetPolicyStatusStub = nil if fake.getPolicyStatusReturnsOnCall == nil { fake.getPolicyStatusReturnsOnCall = make(map[int]struct { - result1 *slsa.ControlStatus + result1 *slsa.Control result2 error }) } fake.getPolicyStatusReturnsOnCall[i] = struct { - result1 *slsa.ControlStatus + result1 *slsa.Control result2 error }{result1, result2} } diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index 7157505..71b7471 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -75,7 +75,7 @@ func (t *Tool) GetBranchControls(ctx context.Context, branch *models.Branch) (*s return nil, fmt.Errorf("reading policy status: %w", err) } - controls.Controls = append(controls.Controls, *status) + controls.Controls = append(controls.Controls, status) return controls, err } @@ -104,7 +104,7 @@ func (t *Tool) GetBranchControlsAtCommit(ctx context.Context, branch *models.Bra return nil, fmt.Errorf("reading policy status: %w", err) } - controls.Controls = append(controls.Controls, *status) + controls.Controls = append(controls.Controls, status) return controls, err } @@ -237,8 +237,8 @@ func (t *Tool) createPolicy(r *models.Repository, branch *models.Branch, control // Unless there is previous provenance metadata, then we can compute // a higher level if controls != nil { - eligibleLevel = policy.ComputeEligibleSlsaLevel(*controls.GetActiveControls()) - eligibleSince, err = policy.ComputeEligibleSince(*controls.GetActiveControls(), eligibleLevel) + eligibleLevel = policy.ComputeEligibleSlsaLevel(controls.GetActiveControls()) + eligibleSince, err = policy.ComputeEligibleSince(controls.GetActiveControls(), eligibleLevel) if err != nil { return nil, fmt.Errorf("could not compute eligible_since: %w", err) } @@ -259,7 +259,7 @@ func (t *Tool) createPolicy(r *models.Repository, branch *models.Branch, control tagHygiene := controls.GetActiveControls().GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS) if tagHygiene != nil { p.ProtectedTag = &policy.ProtectedTag{ - Since: tagHygiene.GetSince(), + Since: timestamppb.New(*tagHygiene.GetSince()), TagHygiene: true, } } diff --git a/pkg/sourcetool/tool_test.go b/pkg/sourcetool/tool_test.go index 3892722..a69e85b 100644 --- a/pkg/sourcetool/tool_test.go +++ b/pkg/sourcetool/tool_test.go @@ -20,11 +20,11 @@ func TestGetBranchControls(t *testing.T) { t.Run("GetActiveControls-success", func(t *testing.T) { t.Parallel() i := &sourcetoolfakes.FakeToolImplementation{} - i.GetPolicyStatusReturns(&slsa.ControlStatus{}, nil) + i.GetPolicyStatusReturns(&slsa.Control{}, nil) i.GetBranchControlsReturns(&slsa.ControlSetStatus{ RepoUri: "github.com/ok/repo", Branch: "main", - Controls: []slsa.ControlStatus{ + Controls: []*slsa.Control{ { Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, State: slsa.StateNotEnabled, From 5d3433271d65c442abcb0827afa030c805734867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Thu, 5 Mar 2026 22:02:06 -0600 Subject: [PATCH 08/41] Rename ControlSet to ControlNameSet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/slsa/controls.go | 8 ++++---- pkg/slsa/levels.go | 12 ++++++------ pkg/slsa/slsa_types.go | 2 +- pkg/sourcetool/backends/vcs/github/github.go | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/slsa/controls.go b/pkg/slsa/controls.go index 2bca6c4..f8b36ad 100644 --- a/pkg/slsa/controls.go +++ b/pkg/slsa/controls.go @@ -9,8 +9,8 @@ type ( ControlName string ControlState string - // ControlSet is a list of controls - ControlSet []ControlName + // ControlNameSet is a list of control names + ControlNameSet []ControlName ) func (c ControlName) String() string { @@ -49,7 +49,7 @@ const ( ) // AllLevelControls is a set holding all controls of the SLSA Source spec -var AllLevelControls = ControlSet{ +var AllLevelControls = ControlNameSet{ SLSA_SOURCE_ORG_SCS, SLSA_SOURCE_ORG_ACCESS_CONTROL, SLSA_SOURCE_ORG_SAFE_EXPUNGE, @@ -66,7 +66,7 @@ var AllLevelControls = ControlSet{ SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, } -func (cs ControlSet) GetControl(ctrl ControlName) ControlName { +func (cs ControlNameSet) GetControl(ctrl ControlName) ControlName { if slices.Contains(cs, ctrl) { return ctrl } diff --git a/pkg/slsa/levels.go b/pkg/slsa/levels.go index bbba9a9..e89e107 100644 --- a/pkg/slsa/levels.go +++ b/pkg/slsa/levels.go @@ -16,12 +16,12 @@ const ( type Level struct { Level uint8 - Controls ControlSet + Controls ControlNameSet } -var Level0 = ControlSet{} +var Level0 = ControlNameSet{} -var Level1 = ControlSet{ +var Level1 = ControlNameSet{ SLSA_SOURCE_ORG_SCS, SLSA_SOURCE_SCS_REPO_ID, SLSA_SOURCE_SCS_REVISION_ID, @@ -29,7 +29,7 @@ var Level1 = ControlSet{ SLSA_SOURCE_SCS_VSA, } -var Level2 = ControlSet{ +var Level2 = ControlNameSet{ SLSA_SOURCE_ORG_SCS, SLSA_SOURCE_ORG_ACCESS_CONTROL, SLSA_SOURCE_ORG_SAFE_EXPUNGE, @@ -43,7 +43,7 @@ var Level2 = ControlSet{ SLSA_SOURCE_SCS_PROVENANCE, } -var Level3 = ControlSet{ +var Level3 = ControlNameSet{ SLSA_SOURCE_ORG_SCS, SLSA_SOURCE_ORG_ACCESS_CONTROL, SLSA_SOURCE_ORG_SAFE_EXPUNGE, @@ -59,7 +59,7 @@ var Level3 = ControlSet{ SLSA_SOURCE_SCS_PROTECTED_REFS, } -var Level4 = ControlSet{ +var Level4 = ControlNameSet{ SLSA_SOURCE_ORG_SCS, SLSA_SOURCE_ORG_ACCESS_CONTROL, SLSA_SOURCE_ORG_SAFE_EXPUNGE, diff --git a/pkg/slsa/slsa_types.go b/pkg/slsa/slsa_types.go index a946cdb..0e74e42 100644 --- a/pkg/slsa/slsa_types.go +++ b/pkg/slsa/slsa_types.go @@ -38,7 +38,7 @@ func IsLevelHigherOrEqualTo(level1, level2 SlsaSourceLevel) bool { type SourceVerifiedLevels []ControlName // Returns the list of control names that must be set for the given slsa level. -func GetRequiredControlsForLevel(level SlsaSourceLevel) ControlSet { +func GetRequiredControlsForLevel(level SlsaSourceLevel) ControlNameSet { switch level { case SlsaSourceLevel1: return Level1 diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index dd3a670..8886034 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -19,7 +19,7 @@ import ( // InherentControls are the controls that are always true because we are // in git and/org GitHub. -var InherentControls = slsa.ControlSet{ +var InherentControls = slsa.ControlNameSet{ // GitHub uses git slsa.SLSA_SOURCE_ORG_SCS, From 8a999d1e10c02fb55fa916ba36c54affa81c85d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Thu, 5 Mar 2026 22:07:29 -0600 Subject: [PATCH 09/41] Rename slsa.ControlSetStatus to slsa.ControlSet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/audit_test.go | 4 +- pkg/ghcontrol/checklevel.go | 10 +-- pkg/policy/policy.go | 20 ++--- pkg/policy/policy_test.go | 78 +++++++++---------- pkg/slsa/slsa_types.go | 30 +++---- pkg/sourcetool/backends/vcs/github/github.go | 10 +-- pkg/sourcetool/implementation.go | 8 +- pkg/sourcetool/models/models.go | 6 +- .../models/modelsfakes/fake_vcs_backend.go | 60 +++++++------- .../fake_tool_implementation.go | 40 +++++----- pkg/sourcetool/tool.go | 6 +- pkg/sourcetool/tool_test.go | 2 +- 12 files changed, 137 insertions(+), 137 deletions(-) diff --git a/internal/cmd/audit_test.go b/internal/cmd/audit_test.go index d1ec026..20a8d6c 100644 --- a/internal/cmd/audit_test.go +++ b/internal/cmd/audit_test.go @@ -180,7 +180,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { }, GhPriorCommit: "def456", GhControlStatus: &ghcontrol.GhControlStatus{ - Controls: &slsa.ControlSetStatus{}, + Controls: &slsa.ControlSet{}, }, }, mode: AuditModeFull, @@ -192,7 +192,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { VerifiedLevels: []string{"SLSA_SOURCE_LEVEL_3"}, PrevCommitMatches: &matches, ProvControls: []*provenance.Control{{Name: "test_control"}}, - GhControls: slsa.ControlSetStatus{}, + GhControls: slsa.ControlSet{}, PrevCommit: "def456", GhPriorCommit: "def456", Link: "https://github.com/test-owner/test-repo/commit/def456", diff --git a/pkg/ghcontrol/checklevel.go b/pkg/ghcontrol/checklevel.go index d42fce7..2b62235 100644 --- a/pkg/ghcontrol/checklevel.go +++ b/pkg/ghcontrol/checklevel.go @@ -79,7 +79,7 @@ type GhControlStatus struct { ActivityType string // The controls that are enabled according to the GitHub API. // May not include other controls like if we have provenance. - Controls *slsa.ControlSetStatus + Controls *slsa.ControlSet } // Adds the control, but only if it existed when the commit was pushed. @@ -348,13 +348,13 @@ func (ghc *GitHubConnection) getOldestActiveRule(ctx context.Context, rules []*g // GetBranchControls returns a list of the controls enabled at present for a branch. // This function does not take into account a commit date, it just returns those controls // that are active when called. -func (ghc *GitHubConnection) GetBranchControls(ctx context.Context, ref string) (*slsa.ControlSetStatus, error) { +func (ghc *GitHubConnection) GetBranchControls(ctx context.Context, ref string) (*slsa.ControlSet, error) { branch := GetBranchFromRef(ref) if branch == "" { return nil, fmt.Errorf("ref %s is not a branch", ref) } - controls := &slsa.ControlSetStatus{} + controls := &slsa.ControlSet{} // Do the branch specific stuff. branchRules, _, err := ghc.Client().Repositories.GetRulesForBranch(ctx, ghc.Owner(), ghc.Repo(), branch) @@ -409,7 +409,7 @@ func (ghc *GitHubConnection) GetBranchControlsAtCommit(ctx context.Context, comm CommitPushTime: activity.Timestamp, ActivityType: activity.ActivityType, ActorLogin: activity.Actor.Login, - Controls: &slsa.ControlSetStatus{}, + Controls: &slsa.ControlSet{}, } activeControls, err := ghc.GetBranchControls(ctx, ref) @@ -429,7 +429,7 @@ func (ghc *GitHubConnection) GetBranchControlsAtCommit(ctx context.Context, comm func (ghc *GitHubConnection) GetTagControls(ctx context.Context, commit, ref string) (*GhControlStatus, error) { controlStatus := GhControlStatus{ CommitPushTime: time.Now(), - Controls: &slsa.ControlSetStatus{}, + Controls: &slsa.ControlSet{}, } allRulesets, _, err := ghc.Client().Repositories.GetAllRulesets(ctx, ghc.Owner(), ghc.Repo(), true) diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 126e8ea..549ecfe 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -302,14 +302,14 @@ func (pe *PolicyEvaluator) CreateLocalPolicy(ctx context.Context, repo *models.R return path, nil } -func computeEligibleForLevel(controls *slsa.ControlSetStatus, level slsa.SlsaSourceLevel) bool { +func computeEligibleForLevel(controls *slsa.ControlSet, level slsa.SlsaSourceLevel) bool { requiredControls := slsa.GetRequiredControlsForLevel(level) return controls.AreControlsAvailable(requiredControls) } // Computes the eligible SLSA level, and when they started being eligible for it, // if only they had a policy. Also returns a rationale for why it's eligible for this level. -func ComputeEligibleSlsaLevel(controls *slsa.ControlSetStatus) slsa.SlsaSourceLevel { +func ComputeEligibleSlsaLevel(controls *slsa.ControlSet) slsa.SlsaSourceLevel { if controls == nil { return slsa.SlsaSourceLevel1 } @@ -338,7 +338,7 @@ func laterTime(time1, time2 time.Time) time.Time { } // Computes the time since these controls have been eligible for the level, nil if not eligible. -func ComputeEligibleSince(controls *slsa.ControlSetStatus, level slsa.SlsaSourceLevel) (*time.Time, error) { +func ComputeEligibleSince(controls *slsa.ControlSet, level slsa.SlsaSourceLevel) (*time.Time, error) { requiredControls := slsa.GetRequiredControlsForLevel(level) var newestTime time.Time for _, rc := range requiredControls { @@ -360,9 +360,9 @@ func ComputeEligibleSince(controls *slsa.ControlSetStatus, level slsa.SlsaSource } // Every function that determines properties to include in the result & VSA implements this interface. -type computePolicyResult func(*ProtectedBranch, *ProtectedTag, *slsa.ControlSetStatus) ([]slsa.ControlName, error) +type computePolicyResult func(*ProtectedBranch, *ProtectedTag, *slsa.ControlSet) ([]slsa.ControlName, error) -func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSetStatus) ([]slsa.ControlName, error) { +func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSet) ([]slsa.ControlName, error) { eligibleLevel := ComputeEligibleSlsaLevel(controls) if !slsa.IsLevelHigherOrEqualTo(eligibleLevel, slsa.SlsaSourceLevel(branchPolicy.GetTargetSlsaSourceLevel())) { @@ -391,7 +391,7 @@ func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls * return []slsa.ControlName{slsa.ControlName(branchPolicy.GetTargetSlsaSourceLevel())}, nil } -func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSetStatus) ([]slsa.ControlName, error) { +func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSet) ([]slsa.ControlName, error) { if !branchPolicy.GetRequireReview() { return []slsa.ControlName{}, nil } @@ -410,7 +410,7 @@ func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, contr // computeTagHygiene checks if the current state of the protected refs // matches what we see in the policy policy has SLSA_SOURCE_SCS_PROTECTED_REFS -func computeTagHygiene(_ *ProtectedBranch, tagPolicy *ProtectedTag, controls *slsa.ControlSetStatus) ([]slsa.ControlName, error) { +func computeTagHygiene(_ *ProtectedBranch, tagPolicy *ProtectedTag, controls *slsa.ControlSet) ([]slsa.ControlName, error) { if tagPolicy == nil { // There is no tag policy, so the control isn't met, but it's not an error. return []slsa.ControlName{}, nil @@ -437,7 +437,7 @@ func computeTagHygiene(_ *ProtectedBranch, tagPolicy *ProtectedTag, controls *sl return []slsa.ControlName{slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, nil } -func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSetStatus) ([]slsa.ControlName, error) { +func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSet) ([]slsa.ControlName, error) { controlNames := []slsa.ControlName{} for _, rc := range branchPolicy.GetOrgStatusCheckControls() { if !strings.HasPrefix(rc.GetPropertyName(), slsa.AllowedOrgPropPrefix) { @@ -458,7 +458,7 @@ func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls } // Returns a list of controls to include in the vsa's 'verifiedLevels' field when creating a VSA for a branch. -func evaluateBranchControls(branchPolicy *ProtectedBranch, tagPolicy *ProtectedTag, controls *slsa.ControlSetStatus) (slsa.SourceVerifiedLevels, error) { +func evaluateBranchControls(branchPolicy *ProtectedBranch, tagPolicy *ProtectedTag, controls *slsa.ControlSet) (slsa.SourceVerifiedLevels, error) { policyComputers := []computePolicyResult{ computeSlsaLevel, // Add the SLSA Level to the VSA computeReviewEnforced, // Stamp if reviews are enforced @@ -549,7 +549,7 @@ func NewPolicyEvaluator() *PolicyEvaluator { } // EvaluateControl checks the control against the policy and returns the resulting source level and policy path. -func (pe *PolicyEvaluator) EvaluateControl(ctx context.Context, repo *models.Repository, branch *models.Branch, controlStatus *slsa.ControlSetStatus) (slsa.SourceVerifiedLevels, string, error) { +func (pe *PolicyEvaluator) EvaluateControl(ctx context.Context, repo *models.Repository, branch *models.Branch, controlStatus *slsa.ControlSet) (slsa.SourceVerifiedLevels, string, error) { // We want to check to ensure the repo hasn't enabled/disabled the rules since // setting the 'since' field in their policy. rp, policyPath, err := pe.GetPolicy(ctx, repo) diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index d4068e2..7c773e0 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -152,11 +152,11 @@ func validateMockServerRequestPath(t *testing.T, r *http.Request, expectedPolicy } } -// controlsForLevel creates a slsa.ControlSetStatus collection with all required controls +// controlsForLevel creates a slsa.ControlSet collection with all required controls // for the given SLSA level, each with the given Since timestamp. -func controlsForLevel(level slsa.SlsaSourceLevel, since *time.Time) *slsa.ControlSetStatus { +func controlsForLevel(level slsa.SlsaSourceLevel, since *time.Time) *slsa.ControlSet { required := slsa.GetRequiredControlsForLevel(level) - controls := &slsa.ControlSetStatus{} + controls := &slsa.ControlSet{} for _, name := range required { controls.AddControl(&slsa.Control{Name: name, Since: since, State: slsa.StateActive}) } @@ -164,7 +164,7 @@ func controlsForLevel(level slsa.SlsaSourceLevel, since *time.Time) *slsa.Contro } // controlsForLevelWith creates controls for a level plus additional controls. -func controlsForLevelWith(level slsa.SlsaSourceLevel, since *time.Time, extra ...*slsa.Control) *slsa.ControlSetStatus { +func controlsForLevelWith(level slsa.SlsaSourceLevel, since *time.Time, extra ...*slsa.Control) *slsa.ControlSet { controls := controlsForLevel(level, since) controls.AddControl(extra...) return controls @@ -412,7 +412,7 @@ func TestEvaluateTagProv_Success(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := slsa.ControlSetStatus{Controls: []*slsa.Control{tagHygieneEarlier}} + s := slsa.ControlSet{Controls: []*slsa.Control{tagHygieneEarlier}} // Valid Provenance Predicate (attest.SourceProvenancePred) tagProvPred := provenance.TagProvenancePred{ Controls: s.ToProvenanceControls(), @@ -488,7 +488,7 @@ func TestEvaluateControl_Success(t *testing.T) { tests := []struct { name string policyContent any // RepoPolicy or string for malformed - controlStatus *slsa.ControlSetStatus + controlStatus *slsa.ControlSet ghConnBranch string // Branch for GitHub connection expectedLevels slsa.SourceVerifiedLevels expectedPolicyPath string @@ -496,7 +496,7 @@ func TestEvaluateControl_Success(t *testing.T) { { name: "Commit time before policy Since -> SLSA Level 1", policyContent: &fullPolicy, - controlStatus: &slsa.ControlSetStatus{ + controlStatus: &slsa.ControlSet{ Time: earlierFixedTime, Controls: l3ControlsWithExtras.Controls, }, @@ -507,7 +507,7 @@ func TestEvaluateControl_Success(t *testing.T) { { name: "Commit time after policy Since, controls meet policy -> Expected levels", policyContent: &fullPolicy, - controlStatus: &slsa.ControlSetStatus{ + controlStatus: &slsa.ControlSet{ Time: laterFixedTime, Controls: l3ControlsWithExtras.Controls, }, @@ -518,7 +518,7 @@ func TestEvaluateControl_Success(t *testing.T) { { name: "Branch not in policy, commit after default policy since -> Default policy (SLSA L1)", policyContent: &basicPolicy, - controlStatus: &slsa.ControlSetStatus{ + controlStatus: &slsa.ControlSet{ Time: laterFixedTime, Controls: l3ControlsWithExtras.Controls, }, @@ -581,14 +581,14 @@ func TestEvaluateControl_Failure(t *testing.T) { tests := []struct { name string policyContent any - controlStatus *slsa.ControlSetStatus + controlStatus *slsa.ControlSet ghConnBranch string expectedErrorContains string }{ { name: "Commit time after policy Since, controls DO NOT meet policy -> Error", policyContent: &policyL3Review, - controlStatus: &slsa.ControlSetStatus{ + controlStatus: &slsa.ControlSet{ Time: later.AsTime(), Controls: controlsForLevel(slsa.SlsaSourceLevel2, &rearlier).Controls, }, @@ -598,7 +598,7 @@ func TestEvaluateControl_Failure(t *testing.T) { { name: "Malformed JSON -> Error", policyContent: "not json", - controlStatus: &slsa.ControlSetStatus{ + controlStatus: &slsa.ControlSet{ Time: later.AsTime(), }, ghConnBranch: "main", @@ -722,7 +722,7 @@ const ( func TestComputeEligibleSlsaLevel(t *testing.T) { tests := []struct { name string - controls *slsa.ControlSetStatus + controls *slsa.ControlSet expectedLevel slsa.SlsaSourceLevel }{ { @@ -742,7 +742,7 @@ func TestComputeEligibleSlsaLevel(t *testing.T) { }, { name: "SLSA Level 1 - partial controls only", - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{&slsa.Control{Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, Since: &fixedTime, State: slsa.StateActive}}}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{&slsa.Control{Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, Since: &fixedTime, State: slsa.StateActive}}}, expectedLevel: slsa.SlsaSourceLevel1, }, { @@ -784,7 +784,7 @@ func TestEvaluateBranchControls(t *testing.T) { name string branchPolicy *ProtectedBranch tagPolicy *ProtectedTag - controls *slsa.ControlSetStatus + controls *slsa.ControlSet expectedLevels slsa.SourceVerifiedLevels expectError bool expectedErrorContains string @@ -835,7 +835,7 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Error - computeSlsaLevel Fails (Policy L3, Controls L1)", branchPolicy: &policyL3Review, tagPolicy: &noTagHygienePolicy, - controls: &slsa.ControlSetStatus{}, + controls: &slsa.ControlSet{}, expectedLevels: slsa.SourceVerifiedLevels{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", @@ -926,7 +926,7 @@ func TestComputeTagHygiene(t *testing.T) { tests := []struct { name string tagPolicy *ProtectedTag - controls *slsa.ControlSetStatus + controls *slsa.ControlSet expectedControls []slsa.ControlName expectError bool expectedErrorContains string @@ -934,21 +934,21 @@ func TestComputeTagHygiene(t *testing.T) { { name: "Policy requires tag hygiene, control compliant (Policy.Since >= Control.Since)", tagPolicy: &policyRequiresTagHygieneNow, - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}, // Policy.Since == Control.Since + controls: &slsa.ControlSet{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}, // Policy.Since == Control.Since expectedControls: []slsa.ControlName{slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, { name: "Policy does not require tag hygiene - control state irrelevant", tagPolicy: &policyNotRequiresTagHygiene, - controls: &slsa.ControlSetStatus{}, // Control state explicitly shown as irrelevant + controls: &slsa.ControlSet{}, // Control state explicitly shown as irrelevant expectedControls: []slsa.ControlName{}, expectError: false, }, { name: "Policy requires tag hygiene, control not present: fail", tagPolicy: &policyRequiresTagHygieneNow, - controls: &slsa.ControlSetStatus{}, // Tag Hygiene control missing + controls: &slsa.ControlSet{}, // Tag Hygiene control missing expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires tag hygiene, but that control is not enabled", @@ -956,7 +956,7 @@ func TestComputeTagHygiene(t *testing.T) { { name: "Policy requires tag hygiene, control enabled, Policy.Since < Control.Since: fail", tagPolicy: &policyRequiresTagHygieneEarlier, // Policy.Since is 'earlier' - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}, // Control.Since is 'now' + controls: &slsa.ControlSet{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}, // Control.Since is 'now' expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires tag hygiene since", // ...but that control has only been enabled since... @@ -1002,7 +1002,7 @@ func TestComputeReviewEnforced(t *testing.T) { tests := []struct { name string branchPolicy *ProtectedBranch - controls *slsa.ControlSetStatus + controls *slsa.ControlSet expectedControls []slsa.ControlName expectError bool expectedErrorContains string @@ -1010,21 +1010,21 @@ func TestComputeReviewEnforced(t *testing.T) { { name: "Policy requires review, control compliant (Policy.Since >= Control.Since)", branchPolicy: &policyRequiresReviewNow, - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{reviewControlEnabledNow}}, // Policy.Since == Control.Since + controls: &slsa.ControlSet{Controls: []*slsa.Control{reviewControlEnabledNow}}, // Policy.Since == Control.Since expectedControls: []slsa.ControlName{slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, expectError: false, }, { name: "Policy does not require review - control state irrelevant", branchPolicy: &policyNotRequiresReview, - controls: &slsa.ControlSetStatus{}, // Control state explicitly shown as irrelevant + controls: &slsa.ControlSet{}, // Control state explicitly shown as irrelevant expectedControls: []slsa.ControlName{}, expectError: false, }, { name: "Policy requires review, control not present: fail", branchPolicy: &policyRequiresReviewNow, - controls: &slsa.ControlSetStatus{}, // Review control missing + controls: &slsa.ControlSet{}, // Review control missing expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires review, but that control is not enabled", @@ -1032,7 +1032,7 @@ func TestComputeReviewEnforced(t *testing.T) { { name: "Policy requires review, control enabled, Policy.Since < Control.Since: fail", branchPolicy: &policyRequiresReviewEarlier, // Policy.Since is 'earlier' - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{reviewControlEnabledNow}}, // Control.Since is 'now' + controls: &slsa.ControlSet{Controls: []*slsa.Control{reviewControlEnabledNow}}, // Control.Since is 'now' expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires review since", // ...but that control has only been enabled since... @@ -1081,7 +1081,7 @@ func TestComputeOrgControls(t *testing.T) { tests := []struct { name string orgCheckPolicies []*OrgStatusCheckControl - controls *slsa.ControlSetStatus + controls *slsa.ControlSet expectedControls []slsa.ControlName expectError bool expectedErrorContains string @@ -1089,28 +1089,28 @@ func TestComputeOrgControls(t *testing.T) { { name: "Single check handled", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy}, - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{testedControl}}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{testedControl}}, expectedControls: []slsa.ControlName{"ORG_SOURCE_TESTED"}, expectError: false, }, { name: "Multiple checks handled", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy, lintedControlPolicy}, - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{testedControl, lintedControl}}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{testedControl, lintedControl}}, expectedControls: []slsa.ControlName{"ORG_SOURCE_TESTED", "ORG_SOURCE_LINTED"}, expectError: false, }, { name: "Not configured control should not be returned", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy}, - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{testedControl, notListedControl}}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{testedControl, notListedControl}}, expectedControls: []slsa.ControlName{"ORG_SOURCE_TESTED"}, expectError: false, }, { name: "Policy requires control but it is not present", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy, lintedControlPolicy}, - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{lintedControl}}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{lintedControl}}, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires check 'test', but", @@ -1118,7 +1118,7 @@ func TestComputeOrgControls(t *testing.T) { { name: "Control not enabled long enough fails", orgCheckPolicies: []*OrgStatusCheckControl{earlierTestedControlPolicy}, - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{testedControl}}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{testedControl}}, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires check 'test' since", @@ -1126,7 +1126,7 @@ func TestComputeOrgControls(t *testing.T) { { name: "Invalid property name fails", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy, invalidPropertyNameControlPolicy}, - controls: &slsa.ControlSetStatus{Controls: []*slsa.Control{testedControl}}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{testedControl}}, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy specifies an invalid property name", @@ -1173,7 +1173,7 @@ func TestComputeSlsaLevel(t *testing.T) { tests := []struct { name string branchPolicy *ProtectedBranch - controls *slsa.ControlSetStatus + controls *slsa.ControlSet expectedLevels []slsa.ControlName expectError bool expectedErrorContains string @@ -1195,7 +1195,7 @@ func TestComputeSlsaLevel(t *testing.T) { { name: "Controls L1-eligible, Policy L2: fail (eligibility)", branchPolicy: &policyL2Now, - controls: &slsa.ControlSetStatus{}, + controls: &slsa.ControlSet{}, expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", @@ -1242,7 +1242,7 @@ func TestComputeSlsaLevel(t *testing.T) { { name: "Controls L1-eligible, Policy L3: fail (eligibility)", branchPolicy: &policyL3Now, - controls: &slsa.ControlSetStatus{}, + controls: &slsa.ControlSet{}, expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", @@ -1274,7 +1274,7 @@ func TestComputeSlsaLevel(t *testing.T) { // controlsForLevelWithOverride creates controls for a level, then overrides the // Since timestamp for a specific control name. -func controlsForLevelWithOverride(level slsa.SlsaSourceLevel, baseSince *time.Time, overrides map[slsa.ControlName]time.Time) *slsa.ControlSetStatus { +func controlsForLevelWithOverride(level slsa.SlsaSourceLevel, baseSince *time.Time, overrides map[slsa.ControlName]time.Time) *slsa.ControlSet { controls := controlsForLevel(level, baseSince) for i, c := range controls.Controls { if since, ok := overrides[slsa.ControlName(c.GetName())]; ok { @@ -1291,7 +1291,7 @@ func TestComputeEligibleSince(t *testing.T) { tests := []struct { name string - controls *slsa.ControlSetStatus + controls *slsa.ControlSet level slsa.SlsaSourceLevel expectedTime *time.Time expectError bool @@ -1364,7 +1364,7 @@ func TestComputeEligibleSince(t *testing.T) { }, { name: "Unknown level requested: expect nil, error", - controls: &slsa.ControlSetStatus{}, + controls: &slsa.ControlSet{}, level: slsa.SlsaSourceLevel("UNKNOWN_LEVEL"), expectedTime: &zeroTime, expectError: false, diff --git a/pkg/slsa/slsa_types.go b/pkg/slsa/slsa_types.go index 0e74e42..6b31c4a 100644 --- a/pkg/slsa/slsa_types.go +++ b/pkg/slsa/slsa_types.go @@ -68,8 +68,8 @@ func ControlNamesToStrings(controlNames []ControlName) []string { return strs } -func NewControlSetFromProvanenaceControls(provControls []*provenance.Control) *ControlSetStatus { - set := &ControlSetStatus{ +func NewControlSetFromProvanenaceControls(provControls []*provenance.Control) *ControlSet { + set := &ControlSet{ Controls: []*Control{}, } @@ -86,8 +86,8 @@ func NewControlSetFromProvanenaceControls(provControls []*provenance.Control) *C // NewControlStatus returns a new control status object initialized with // all existing controls in not_enabled state. -func NewControlSetStatus() *ControlSetStatus { - status := &ControlSetStatus{ +func NewControlSet() *ControlSet { + status := &ControlSet{ Time: time.Now(), Controls: []*Control{}, } @@ -102,9 +102,9 @@ func NewControlSetStatus() *ControlSetStatus { return status } -// ControlSetStatus is a snapshot of the status of SLSA controls in a branch at +// ControlSet is a snapshot of the status of SLSA controls in a branch at // a point in time. -type ControlSetStatus struct { +type ControlSet struct { RepoUri string Branch string Time time.Time @@ -137,8 +137,8 @@ type ControlRecommendedAction struct { // GetActiveControls returns a Controls collection with all the controls // which are active in the set. -func (cs *ControlSetStatus) GetActiveControls() *ControlSetStatus { - ret := ControlSetStatus{} +func (cs *ControlSet) GetActiveControls() *ControlSet { + ret := ControlSet{} if cs == nil { return &ret } @@ -151,7 +151,7 @@ func (cs *ControlSetStatus) GetActiveControls() *ControlSetStatus { } // SetControlState sets the state of a control in the set by name. -func (cs *ControlSetStatus) SetControlState(ctrlName ControlName, state ControlState) { +func (cs *ControlSet) SetControlState(ctrlName ControlName, state ControlState) { for i := range cs.Controls { if cs.Controls[i].Name == ctrlName { cs.Controls[i].State = state @@ -162,9 +162,9 @@ func (cs *ControlSetStatus) SetControlState(ctrlName ControlName, state ControlS // Adds the control to the list. Ignores nil controls. // Does not check for duplicate controls. -func (cs *ControlSetStatus) AddControl(newControls ...*Control) { +func (cs *ControlSet) AddControl(newControls ...*Control) { if cs == nil { - cs = &ControlSetStatus{} + cs = &ControlSet{} } for _, c := range newControls { if c == nil { @@ -175,7 +175,7 @@ func (cs *ControlSetStatus) AddControl(newControls ...*Control) { } // Gets the control with the corresponding name, returns nil if not found. -func (cs *ControlSetStatus) GetControl(name ControlName) *Control { +func (cs *ControlSet) GetControl(name ControlName) *Control { for _, control := range cs.Controls { if control.GetName() == name { return control @@ -186,7 +186,7 @@ func (cs *ControlSetStatus) GetControl(name ControlName) *Control { // This checks if the controls are present in the array. But As we merged the // controls array with the struct we also check if they are all active -func (cs *ControlSetStatus) AreControlsAvailable(names []ControlName) bool { +func (cs *ControlSet) AreControlsAvailable(names []ControlName) bool { for _, name := range names { ctl := cs.GetControl(name) if ctl == nil || ctl.State != StateActive { @@ -197,7 +197,7 @@ func (cs *ControlSetStatus) AreControlsAvailable(names []ControlName) bool { } // Returns the names of the controls. -func (cs *ControlSetStatus) Names() []ControlName { +func (cs *ControlSet) Names() []ControlName { names := make([]ControlName, len(cs.Controls)) for i := range cs.Controls { names[i] = cs.Controls[i].GetName() @@ -205,7 +205,7 @@ func (cs *ControlSetStatus) Names() []ControlName { return names } -func (cs *ControlSetStatus) ToProvenanceControls() []*provenance.Control { +func (cs *ControlSet) ToProvenanceControls() []*provenance.Control { var ret = []*provenance.Control{} for _, ctl := range cs.Controls { if ctl.State != StateActive { diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index 8886034..71431ba 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -106,7 +106,7 @@ func (b *Backend) getGitHubConnection(repository *models.Repository, ref string) return ghcontrol.NewGhConnectionWithClient(owner, name, ref, client), nil } -func (b *Backend) GetBranchControls(ctx context.Context, branch *models.Branch) (*slsa.ControlSetStatus, error) { +func (b *Backend) GetBranchControls(ctx context.Context, branch *models.Branch) (*slsa.ControlSet, error) { if branch.Repository == nil { return nil, fmt.Errorf("branch has no repository") } @@ -126,7 +126,7 @@ func (b *Backend) GetBranchControls(ctx context.Context, branch *models.Branch) } // GetBranchControlsAtCommit -func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*slsa.ControlSetStatus, error) { +func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*slsa.ControlSet, error) { if branch.Repository == nil { return nil, fmt.Errorf("branch has no repository") } @@ -170,9 +170,9 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. log.Printf("No provenance attestation found on %s", commit.SHA) } - // NewControlSetStatus returns all the controls for the framework in + // NewControlSet returns all the controls for the framework in // StateNotEnabled. - status := slsa.NewControlSetStatus() + status := slsa.NewControlSet() sinceForever := time.Unix(1, 0) for i, ctrl := range status.Controls { // Check if it's an inherent control, turn it on and don't look back @@ -262,7 +262,7 @@ func (b *Backend) controlImplementationMessage(ctrlName slsa.ControlName) string } } -func (b *Backend) GetTagControls(context.Context, *models.Tag) (*slsa.ControlSetStatus, error) { +func (b *Backend) GetTagControls(context.Context, *models.Tag) (*slsa.ControlSet, error) { return nil, fmt.Errorf("not yet implemented") } diff --git a/pkg/sourcetool/implementation.go b/pkg/sourcetool/implementation.go index d493b50..d92bd52 100644 --- a/pkg/sourcetool/implementation.go +++ b/pkg/sourcetool/implementation.go @@ -37,8 +37,8 @@ type toolImplementation interface { SearchPullRequest(context.Context, *auth.Authenticator, *models.Repository, string) (*models.PullRequest, error) GetVcsBackend(*models.Repository) (models.VcsBackend, error) GetAttestationReader(*models.Repository) (models.AttestationStorageReader, error) - GetBranchControls(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSetStatus, error) - GetBranchControlsAtCommit(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error) + GetBranchControls(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSet, error) + GetBranchControlsAtCommit(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSet, error) ConfigureControls(models.VcsBackend, *models.Repository, []*models.Branch, []models.ControlConfiguration) error GetPolicyStatus(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.Control, error) CreateRepositoryFork(context.Context, *auth.Authenticator, *models.Repository, string) error @@ -55,13 +55,13 @@ func (impl *defaultToolImplementation) ConfigureControls( func (impl *defaultToolImplementation) GetBranchControls( ctx context.Context, backend models.VcsBackend, branch *models.Branch, -) (*slsa.ControlSetStatus, error) { +) (*slsa.ControlSet, error) { return backend.GetBranchControls(ctx, branch) } func (impl *defaultToolImplementation) GetBranchControlsAtCommit( ctx context.Context, backend models.VcsBackend, branch *models.Branch, commit *models.Commit, -) (*slsa.ControlSetStatus, error) { +) (*slsa.ControlSet, error) { return backend.GetBranchControlsAtCommit(ctx, branch, commit) } diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index f7df48e..f08c511 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -39,9 +39,9 @@ type AttestationStorageReader interface { // //counterfeiter:generate . VcsBackend type VcsBackend interface { - GetBranchControls(context.Context, *Branch) (*slsa.ControlSetStatus, error) - GetBranchControlsAtCommit(context.Context, *Branch, *Commit) (*slsa.ControlSetStatus, error) - GetTagControls(context.Context, *Tag) (*slsa.ControlSetStatus, error) + GetBranchControls(context.Context, *Branch) (*slsa.ControlSet, error) + GetBranchControlsAtCommit(context.Context, *Branch, *Commit) (*slsa.ControlSet, error) + GetTagControls(context.Context, *Tag) (*slsa.ControlSet, error) ControlConfigurationDescr(*Branch, ControlConfiguration) string ConfigureControls(*Repository, []*Branch, []ControlConfiguration) error GetLatestCommit(context.Context, *Repository, *Branch) (*Commit, error) diff --git a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go index 88a30ad..0daf19b 100644 --- a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go +++ b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go @@ -54,21 +54,21 @@ type FakeVcsBackend struct { result3 models.ControlPreRemediationFn result4 error } - GetBranchControlsStub func(context.Context, *models.Branch) (*slsa.ControlSetStatus, error) + GetBranchControlsStub func(context.Context, *models.Branch) (*slsa.ControlSet, error) getBranchControlsMutex sync.RWMutex getBranchControlsArgsForCall []struct { arg1 context.Context arg2 *models.Branch } getBranchControlsReturns struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } getBranchControlsReturnsOnCall map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } - GetBranchControlsAtCommitStub func(context.Context, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error) + GetBranchControlsAtCommitStub func(context.Context, *models.Branch, *models.Commit) (*slsa.ControlSet, error) getBranchControlsAtCommitMutex sync.RWMutex getBranchControlsAtCommitArgsForCall []struct { arg1 context.Context @@ -76,11 +76,11 @@ type FakeVcsBackend struct { arg3 *models.Commit } getBranchControlsAtCommitReturns struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } getBranchControlsAtCommitReturnsOnCall map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } GetLatestCommitStub func(context.Context, *models.Repository, *models.Branch) (*models.Commit, error) @@ -98,18 +98,18 @@ type FakeVcsBackend struct { result1 *models.Commit result2 error } - GetTagControlsStub func(context.Context, *models.Tag) (*slsa.ControlSetStatus, error) + GetTagControlsStub func(context.Context, *models.Tag) (*slsa.ControlSet, error) getTagControlsMutex sync.RWMutex getTagControlsArgsForCall []struct { arg1 context.Context arg2 *models.Tag } getTagControlsReturns struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } getTagControlsReturnsOnCall map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } invocations map[string][][]interface{} @@ -328,7 +328,7 @@ func (fake *FakeVcsBackend) ControlPrecheckReturnsOnCall(i int, result1 bool, re }{result1, result2, result3, result4} } -func (fake *FakeVcsBackend) GetBranchControls(arg1 context.Context, arg2 *models.Branch) (*slsa.ControlSetStatus, error) { +func (fake *FakeVcsBackend) GetBranchControls(arg1 context.Context, arg2 *models.Branch) (*slsa.ControlSet, error) { fake.getBranchControlsMutex.Lock() ret, specificReturn := fake.getBranchControlsReturnsOnCall[len(fake.getBranchControlsArgsForCall)] fake.getBranchControlsArgsForCall = append(fake.getBranchControlsArgsForCall, struct { @@ -354,7 +354,7 @@ func (fake *FakeVcsBackend) GetBranchControlsCallCount() int { return len(fake.getBranchControlsArgsForCall) } -func (fake *FakeVcsBackend) GetBranchControlsCalls(stub func(context.Context, *models.Branch) (*slsa.ControlSetStatus, error)) { +func (fake *FakeVcsBackend) GetBranchControlsCalls(stub func(context.Context, *models.Branch) (*slsa.ControlSet, error)) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = stub @@ -367,33 +367,33 @@ func (fake *FakeVcsBackend) GetBranchControlsArgsForCall(i int) (context.Context return argsForCall.arg1, argsForCall.arg2 } -func (fake *FakeVcsBackend) GetBranchControlsReturns(result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeVcsBackend) GetBranchControlsReturns(result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = nil fake.getBranchControlsReturns = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeVcsBackend) GetBranchControlsReturnsOnCall(i int, result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeVcsBackend) GetBranchControlsReturnsOnCall(i int, result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = nil if fake.getBranchControlsReturnsOnCall == nil { fake.getBranchControlsReturnsOnCall = make(map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }) } fake.getBranchControlsReturnsOnCall[i] = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeVcsBackend) GetBranchControlsAtCommit(arg1 context.Context, arg2 *models.Branch, arg3 *models.Commit) (*slsa.ControlSetStatus, error) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommit(arg1 context.Context, arg2 *models.Branch, arg3 *models.Commit) (*slsa.ControlSet, error) { fake.getBranchControlsAtCommitMutex.Lock() ret, specificReturn := fake.getBranchControlsAtCommitReturnsOnCall[len(fake.getBranchControlsAtCommitArgsForCall)] fake.getBranchControlsAtCommitArgsForCall = append(fake.getBranchControlsAtCommitArgsForCall, struct { @@ -420,7 +420,7 @@ func (fake *FakeVcsBackend) GetBranchControlsAtCommitCallCount() int { return len(fake.getBranchControlsAtCommitArgsForCall) } -func (fake *FakeVcsBackend) GetBranchControlsAtCommitCalls(stub func(context.Context, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error)) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommitCalls(stub func(context.Context, *models.Branch, *models.Commit) (*slsa.ControlSet, error)) { fake.getBranchControlsAtCommitMutex.Lock() defer fake.getBranchControlsAtCommitMutex.Unlock() fake.GetBranchControlsAtCommitStub = stub @@ -433,28 +433,28 @@ func (fake *FakeVcsBackend) GetBranchControlsAtCommitArgsForCall(i int) (context return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } -func (fake *FakeVcsBackend) GetBranchControlsAtCommitReturns(result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommitReturns(result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsAtCommitMutex.Lock() defer fake.getBranchControlsAtCommitMutex.Unlock() fake.GetBranchControlsAtCommitStub = nil fake.getBranchControlsAtCommitReturns = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeVcsBackend) GetBranchControlsAtCommitReturnsOnCall(i int, result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommitReturnsOnCall(i int, result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsAtCommitMutex.Lock() defer fake.getBranchControlsAtCommitMutex.Unlock() fake.GetBranchControlsAtCommitStub = nil if fake.getBranchControlsAtCommitReturnsOnCall == nil { fake.getBranchControlsAtCommitReturnsOnCall = make(map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }) } fake.getBranchControlsAtCommitReturnsOnCall[i] = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } @@ -525,7 +525,7 @@ func (fake *FakeVcsBackend) GetLatestCommitReturnsOnCall(i int, result1 *models. }{result1, result2} } -func (fake *FakeVcsBackend) GetTagControls(arg1 context.Context, arg2 *models.Tag) (*slsa.ControlSetStatus, error) { +func (fake *FakeVcsBackend) GetTagControls(arg1 context.Context, arg2 *models.Tag) (*slsa.ControlSet, error) { fake.getTagControlsMutex.Lock() ret, specificReturn := fake.getTagControlsReturnsOnCall[len(fake.getTagControlsArgsForCall)] fake.getTagControlsArgsForCall = append(fake.getTagControlsArgsForCall, struct { @@ -551,7 +551,7 @@ func (fake *FakeVcsBackend) GetTagControlsCallCount() int { return len(fake.getTagControlsArgsForCall) } -func (fake *FakeVcsBackend) GetTagControlsCalls(stub func(context.Context, *models.Tag) (*slsa.ControlSetStatus, error)) { +func (fake *FakeVcsBackend) GetTagControlsCalls(stub func(context.Context, *models.Tag) (*slsa.ControlSet, error)) { fake.getTagControlsMutex.Lock() defer fake.getTagControlsMutex.Unlock() fake.GetTagControlsStub = stub @@ -564,28 +564,28 @@ func (fake *FakeVcsBackend) GetTagControlsArgsForCall(i int) (context.Context, * return argsForCall.arg1, argsForCall.arg2 } -func (fake *FakeVcsBackend) GetTagControlsReturns(result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeVcsBackend) GetTagControlsReturns(result1 *slsa.ControlSet, result2 error) { fake.getTagControlsMutex.Lock() defer fake.getTagControlsMutex.Unlock() fake.GetTagControlsStub = nil fake.getTagControlsReturns = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeVcsBackend) GetTagControlsReturnsOnCall(i int, result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeVcsBackend) GetTagControlsReturnsOnCall(i int, result1 *slsa.ControlSet, result2 error) { fake.getTagControlsMutex.Lock() defer fake.getTagControlsMutex.Unlock() fake.GetTagControlsStub = nil if fake.getTagControlsReturnsOnCall == nil { fake.getTagControlsReturnsOnCall = make(map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }) } fake.getTagControlsReturnsOnCall[i] = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } diff --git a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go index 8f503f2..1b62c99 100644 --- a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go +++ b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go @@ -92,7 +92,7 @@ type FakeToolImplementation struct { result1 models.AttestationStorageReader result2 error } - GetBranchControlsStub func(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSetStatus, error) + GetBranchControlsStub func(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSet, error) getBranchControlsMutex sync.RWMutex getBranchControlsArgsForCall []struct { arg1 context.Context @@ -100,14 +100,14 @@ type FakeToolImplementation struct { arg3 *models.Branch } getBranchControlsReturns struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } getBranchControlsReturnsOnCall map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } - GetBranchControlsAtCommitStub func(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error) + GetBranchControlsAtCommitStub func(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSet, error) getBranchControlsAtCommitMutex sync.RWMutex getBranchControlsAtCommitArgsForCall []struct { arg1 context.Context @@ -116,11 +116,11 @@ type FakeToolImplementation struct { arg4 *models.Commit } getBranchControlsAtCommitReturns struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } getBranchControlsAtCommitReturnsOnCall map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } GetPolicyStatusStub func(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.Control, error) @@ -575,7 +575,7 @@ func (fake *FakeToolImplementation) GetAttestationReaderReturnsOnCall(i int, res }{result1, result2} } -func (fake *FakeToolImplementation) GetBranchControls(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Branch) (*slsa.ControlSetStatus, error) { +func (fake *FakeToolImplementation) GetBranchControls(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Branch) (*slsa.ControlSet, error) { fake.getBranchControlsMutex.Lock() ret, specificReturn := fake.getBranchControlsReturnsOnCall[len(fake.getBranchControlsArgsForCall)] fake.getBranchControlsArgsForCall = append(fake.getBranchControlsArgsForCall, struct { @@ -602,7 +602,7 @@ func (fake *FakeToolImplementation) GetBranchControlsCallCount() int { return len(fake.getBranchControlsArgsForCall) } -func (fake *FakeToolImplementation) GetBranchControlsCalls(stub func(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSetStatus, error)) { +func (fake *FakeToolImplementation) GetBranchControlsCalls(stub func(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSet, error)) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = stub @@ -615,33 +615,33 @@ func (fake *FakeToolImplementation) GetBranchControlsArgsForCall(i int) (context return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } -func (fake *FakeToolImplementation) GetBranchControlsReturns(result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeToolImplementation) GetBranchControlsReturns(result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = nil fake.getBranchControlsReturns = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeToolImplementation) GetBranchControlsReturnsOnCall(i int, result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeToolImplementation) GetBranchControlsReturnsOnCall(i int, result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = nil if fake.getBranchControlsReturnsOnCall == nil { fake.getBranchControlsReturnsOnCall = make(map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }) } fake.getBranchControlsReturnsOnCall[i] = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeToolImplementation) GetBranchControlsAtCommit(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Branch, arg4 *models.Commit) (*slsa.ControlSetStatus, error) { +func (fake *FakeToolImplementation) GetBranchControlsAtCommit(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Branch, arg4 *models.Commit) (*slsa.ControlSet, error) { fake.getBranchControlsAtCommitMutex.Lock() ret, specificReturn := fake.getBranchControlsAtCommitReturnsOnCall[len(fake.getBranchControlsAtCommitArgsForCall)] fake.getBranchControlsAtCommitArgsForCall = append(fake.getBranchControlsAtCommitArgsForCall, struct { @@ -669,7 +669,7 @@ func (fake *FakeToolImplementation) GetBranchControlsAtCommitCallCount() int { return len(fake.getBranchControlsAtCommitArgsForCall) } -func (fake *FakeToolImplementation) GetBranchControlsAtCommitCalls(stub func(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error)) { +func (fake *FakeToolImplementation) GetBranchControlsAtCommitCalls(stub func(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSet, error)) { fake.getBranchControlsAtCommitMutex.Lock() defer fake.getBranchControlsAtCommitMutex.Unlock() fake.GetBranchControlsAtCommitStub = stub @@ -682,28 +682,28 @@ func (fake *FakeToolImplementation) GetBranchControlsAtCommitArgsForCall(i int) return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 } -func (fake *FakeToolImplementation) GetBranchControlsAtCommitReturns(result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeToolImplementation) GetBranchControlsAtCommitReturns(result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsAtCommitMutex.Lock() defer fake.getBranchControlsAtCommitMutex.Unlock() fake.GetBranchControlsAtCommitStub = nil fake.getBranchControlsAtCommitReturns = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeToolImplementation) GetBranchControlsAtCommitReturnsOnCall(i int, result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeToolImplementation) GetBranchControlsAtCommitReturnsOnCall(i int, result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsAtCommitMutex.Lock() defer fake.getBranchControlsAtCommitMutex.Unlock() fake.GetBranchControlsAtCommitStub = nil if fake.getBranchControlsAtCommitReturnsOnCall == nil { fake.getBranchControlsAtCommitReturnsOnCall = make(map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }) } fake.getBranchControlsAtCommitReturnsOnCall[i] = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index 71b7471..8464bb1 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -52,7 +52,7 @@ type Tool struct { } // GetRepoControls returns the controls that are enabled in a repository branch. -func (t *Tool) GetBranchControls(ctx context.Context, branch *models.Branch) (*slsa.ControlSetStatus, error) { +func (t *Tool) GetBranchControls(ctx context.Context, branch *models.Branch) (*slsa.ControlSet, error) { if branch.Repository == nil { return nil, fmt.Errorf("repositoryu not specified in branch") } @@ -81,7 +81,7 @@ func (t *Tool) GetBranchControls(ctx context.Context, branch *models.Branch) (*s } // GetRepoControls returns the controls that are enabled in a repository branch. -func (t *Tool) GetBranchControlsAtCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*slsa.ControlSetStatus, error) { +func (t *Tool) GetBranchControlsAtCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*slsa.ControlSet, error) { if branch.Repository == nil { return nil, fmt.Errorf("repositoryu not specified in branch") } @@ -228,7 +228,7 @@ func (t *Tool) CreateBranchPolicy(ctx context.Context, r *models.Repository, bra // This function will be moved to the policy package once we start integrating // it with the global data models (if we do). -func (t *Tool) createPolicy(r *models.Repository, branch *models.Branch, controls *slsa.ControlSetStatus) (*policy.RepoPolicy, error) { +func (t *Tool) createPolicy(r *models.Repository, branch *models.Branch, controls *slsa.ControlSet) (*policy.RepoPolicy, error) { // Default to SLSA1 since unset date eligibleSince := &time.Time{} eligibleLevel := slsa.SlsaSourceLevel1 diff --git a/pkg/sourcetool/tool_test.go b/pkg/sourcetool/tool_test.go index a69e85b..2e23ec5 100644 --- a/pkg/sourcetool/tool_test.go +++ b/pkg/sourcetool/tool_test.go @@ -21,7 +21,7 @@ func TestGetBranchControls(t *testing.T) { t.Parallel() i := &sourcetoolfakes.FakeToolImplementation{} i.GetPolicyStatusReturns(&slsa.Control{}, nil) - i.GetBranchControlsReturns(&slsa.ControlSetStatus{ + i.GetBranchControlsReturns(&slsa.ControlSet{ RepoUri: "github.com/ok/repo", Branch: "main", Controls: []*slsa.Control{ From 3bc6a6c6fbd840bb1ac8167d606b8a135e903d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Thu, 5 Mar 2026 22:14:52 -0600 Subject: [PATCH 10/41] Fix linter nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/checklevel.go | 11 +- pkg/ghcontrol/checklevel_test.go | 2 +- pkg/policy/policy_test.go | 138 +++++++++---------- pkg/slsa/slsa_types.go | 9 +- pkg/sourcetool/backends/vcs/github/github.go | 2 +- 5 files changed, 76 insertions(+), 86 deletions(-) diff --git a/internal/cmd/checklevel.go b/internal/cmd/checklevel.go index f96749f..7cd2c5c 100644 --- a/internal/cmd/checklevel.go +++ b/internal/cmd/checklevel.go @@ -90,8 +90,6 @@ This is meant to be run within the corresponding GitHub Actions workflow.`, return err } - // fmt.Printf("Controles: %+v\n", controlStatus) - pe := policy.NewPolicyEvaluator() pe.UseLocalPolicy = opts.useLocalPolicy verifiedLevels, policyPath, err := pe.EvaluateControl(cmd.Context(), opts.GetRepository(), opts.GetBranch(), controlStatus) @@ -105,8 +103,13 @@ This is meant to be run within the corresponding GitHub Actions workflow.`, } } - // unsignedVsa, err := attest.CreateUnsignedSourceVsa(ghconnection.GetRepoUri(), ghconnection.GetFullRef(), opts.commit, verifiedLevels, policyPath) - unsignedVsa, err := attest.CreateUnsignedSourceVsa(opts.GetRepository().GetHttpURL(), opts.GetBranch().FullRef(), opts.commit, verifiedLevels, policyPath) + unsignedVsa, err := attest.CreateUnsignedSourceVsa( + opts.GetRepository().GetHttpURL(), + opts.GetBranch().FullRef(), + opts.commit, + verifiedLevels, + policyPath, + ) if err != nil { return err } diff --git a/pkg/ghcontrol/checklevel_test.go b/pkg/ghcontrol/checklevel_test.go index 09399c5..44c7fbe 100644 --- a/pkg/ghcontrol/checklevel_test.go +++ b/pkg/ghcontrol/checklevel_test.go @@ -352,7 +352,7 @@ func TestGetBranchControlsRequiredChecks(t *testing.T) { controlNames := make([]slsa.ControlName, 0, len(controlStatus.Controls.Controls)) for _, control := range controlStatus.Controls.Controls { - controlNames = append(controlNames, slsa.ControlName(control.GetName())) + controlNames = append(controlNames, control.GetName()) if !control.GetSince().Equal(priorTime) { t.Errorf("Expected control.Since %v, got %v", priorTime, control.GetSince()) } diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index 7c773e0..cd526d2 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -742,7 +742,7 @@ func TestComputeEligibleSlsaLevel(t *testing.T) { }, { name: "SLSA Level 1 - partial controls only", - controls: &slsa.ControlSet{Controls: []*slsa.Control{&slsa.Control{Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, Since: &fixedTime, State: slsa.StateActive}}}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{{Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, Since: &fixedTime, State: slsa.StateActive}}}, expectedLevel: slsa.SlsaSourceLevel1, }, { @@ -910,78 +910,82 @@ func TestEvaluateBranchControls(t *testing.T) { } } +// assertControlResult is a shared helper for testing computePolicyResult functions. +func assertControlResult(t *testing.T, fnName string, gotControls []slsa.ControlName, err error, expectedControls []slsa.ControlName, expectError bool, expectedErrorContains string) { + t.Helper() + if expectError { + if err == nil { + t.Errorf("%s() error = nil, want non-nil error containing %q", fnName, expectedErrorContains) + } else if !strings.Contains(err.Error(), expectedErrorContains) { + t.Errorf("%s() error = %q, want error containing %q", fnName, err.Error(), expectedErrorContains) + } + } else { + if err != nil { + t.Errorf("%s() error = %v, want nil", fnName, err) + } + } + if !slices.Equal(gotControls, expectedControls) { + t.Errorf("%s() gotControls = %v, want %v", fnName, gotControls, expectedControls) + } +} + func TestComputeTagHygiene(t *testing.T) { now := timestamppb.Now() tnow := now.AsTime() earlier := timestamppb.New(time.Now().Add(-time.Hour)) - // Branch Policies policyRequiresTagHygieneNow := ProtectedTag{TagHygiene: true, Since: now} policyRequiresTagHygieneEarlier := ProtectedTag{TagHygiene: true, Since: earlier} policyNotRequiresTagHygiene := ProtectedTag{TagHygiene: false, Since: now} - - // Controls tagHygieneControlEnabledNow := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, Since: &tnow, State: slsa.StateActive} tests := []struct { name string tagPolicy *ProtectedTag - controls *slsa.ControlSet + computeFn func() ([]slsa.ControlName, error) expectedControls []slsa.ControlName expectError bool expectedErrorContains string }{ { - name: "Policy requires tag hygiene, control compliant (Policy.Since >= Control.Since)", - tagPolicy: &policyRequiresTagHygieneNow, - controls: &slsa.ControlSet{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}, // Policy.Since == Control.Since + name: "Policy requires tag hygiene, control compliant (Policy.Since >= Control.Since)", + tagPolicy: &policyRequiresTagHygieneNow, + computeFn: func() ([]slsa.ControlName, error) { + return computeTagHygiene(nil, &policyRequiresTagHygieneNow, &slsa.ControlSet{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}) + }, expectedControls: []slsa.ControlName{slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, - expectError: false, }, { - name: "Policy does not require tag hygiene - control state irrelevant", - tagPolicy: &policyNotRequiresTagHygiene, - controls: &slsa.ControlSet{}, // Control state explicitly shown as irrelevant + name: "Policy does not require tag hygiene - control state irrelevant", + computeFn: func() ([]slsa.ControlName, error) { + return computeTagHygiene(nil, &policyNotRequiresTagHygiene, &slsa.ControlSet{}) + }, expectedControls: []slsa.ControlName{}, - expectError: false, }, { - name: "Policy requires tag hygiene, control not present: fail", - tagPolicy: &policyRequiresTagHygieneNow, - controls: &slsa.ControlSet{}, // Tag Hygiene control missing + name: "Policy requires tag hygiene, control not present: fail", + computeFn: func() ([]slsa.ControlName, error) { + return computeTagHygiene(nil, &policyRequiresTagHygieneNow, &slsa.ControlSet{}) + }, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires tag hygiene, but that control is not enabled", }, { - name: "Policy requires tag hygiene, control enabled, Policy.Since < Control.Since: fail", - tagPolicy: &policyRequiresTagHygieneEarlier, // Policy.Since is 'earlier' - controls: &slsa.ControlSet{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}, // Control.Since is 'now' + name: "Policy requires tag hygiene, control enabled, Policy.Since < Control.Since: fail", + computeFn: func() ([]slsa.ControlName, error) { + return computeTagHygiene(nil, &policyRequiresTagHygieneEarlier, &slsa.ControlSet{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}) + }, expectedControls: []slsa.ControlName{}, expectError: true, - expectedErrorContains: "policy requires tag hygiene since", // ...but that control has only been enabled since... + expectedErrorContains: "policy requires tag hygiene since", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotControls, err := computeTagHygiene(nil, tt.tagPolicy, tt.controls) - - if tt.expectError { - if err == nil { - t.Errorf("computeTagHygiene() error = nil, want non-nil error containing %q", tt.expectedErrorContains) - } else if !strings.Contains(err.Error(), tt.expectedErrorContains) { - t.Errorf("computeTagHygiene() error = %q, want error containing %q", err.Error(), tt.expectedErrorContains) - } - } else { - if err != nil { - t.Errorf("computeTagHygiene() error = %v, want nil", err) - } - } - - if !slices.Equal(gotControls, tt.expectedControls) { - t.Errorf("computeTagHygiene() gotControls = %v, want %v", gotControls, tt.expectedControls) - } + gotControls, err := tt.computeFn() + assertControlResult(t, "computeTagHygiene", gotControls, err, tt.expectedControls, tt.expectError, tt.expectedErrorContains) }) } } @@ -991,73 +995,56 @@ func TestComputeReviewEnforced(t *testing.T) { tnow := now.AsTime() earlier := timestamppb.New(time.Now().Add(-time.Hour)) - // Branch Policies policyRequiresReviewNow := ProtectedBranch{RequireReview: true, Since: now} policyRequiresReviewEarlier := ProtectedBranch{RequireReview: true, Since: earlier} policyNotRequiresReview := ProtectedBranch{RequireReview: false, Since: now} - - // Controls reviewControlEnabledNow := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, Since: &tnow, State: slsa.StateActive} tests := []struct { name string - branchPolicy *ProtectedBranch - controls *slsa.ControlSet + computeFn func() ([]slsa.ControlName, error) expectedControls []slsa.ControlName expectError bool expectedErrorContains string }{ { - name: "Policy requires review, control compliant (Policy.Since >= Control.Since)", - branchPolicy: &policyRequiresReviewNow, - controls: &slsa.ControlSet{Controls: []*slsa.Control{reviewControlEnabledNow}}, // Policy.Since == Control.Since + name: "Policy requires review, control compliant (Policy.Since >= Control.Since)", + computeFn: func() ([]slsa.ControlName, error) { + return computeReviewEnforced(&policyRequiresReviewNow, nil, &slsa.ControlSet{Controls: []*slsa.Control{reviewControlEnabledNow}}) + }, expectedControls: []slsa.ControlName{slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, - expectError: false, }, { - name: "Policy does not require review - control state irrelevant", - branchPolicy: &policyNotRequiresReview, - controls: &slsa.ControlSet{}, // Control state explicitly shown as irrelevant + name: "Policy does not require review - control state irrelevant", + computeFn: func() ([]slsa.ControlName, error) { + return computeReviewEnforced(&policyNotRequiresReview, nil, &slsa.ControlSet{}) + }, expectedControls: []slsa.ControlName{}, - expectError: false, }, { - name: "Policy requires review, control not present: fail", - branchPolicy: &policyRequiresReviewNow, - controls: &slsa.ControlSet{}, // Review control missing + name: "Policy requires review, control not present: fail", + computeFn: func() ([]slsa.ControlName, error) { + return computeReviewEnforced(&policyRequiresReviewNow, nil, &slsa.ControlSet{}) + }, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires review, but that control is not enabled", }, { - name: "Policy requires review, control enabled, Policy.Since < Control.Since: fail", - branchPolicy: &policyRequiresReviewEarlier, // Policy.Since is 'earlier' - controls: &slsa.ControlSet{Controls: []*slsa.Control{reviewControlEnabledNow}}, // Control.Since is 'now' + name: "Policy requires review, control enabled, Policy.Since < Control.Since: fail", + computeFn: func() ([]slsa.ControlName, error) { + return computeReviewEnforced(&policyRequiresReviewEarlier, nil, &slsa.ControlSet{Controls: []*slsa.Control{reviewControlEnabledNow}}) + }, expectedControls: []slsa.ControlName{}, expectError: true, - expectedErrorContains: "policy requires review since", // ...but that control has only been enabled since... + expectedErrorContains: "policy requires review since", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotControls, err := computeReviewEnforced(tt.branchPolicy, nil, tt.controls) - - if tt.expectError { - if err == nil { - t.Errorf("computeReviewEnforced() error = nil, want non-nil error containing %q", tt.expectedErrorContains) - } else if !strings.Contains(err.Error(), tt.expectedErrorContains) { - t.Errorf("computeReviewEnforced() error = %q, want error containing %q", err.Error(), tt.expectedErrorContains) - } - } else { - if err != nil { - t.Errorf("computeReviewEnforced() error = %v, want nil", err) - } - } - - if !slices.Equal(gotControls, tt.expectedControls) { - t.Errorf("computeReviewEnforced() gotEnforced = %v, want %v", gotControls, tt.expectedControls) - } + gotControls, err := tt.computeFn() + assertControlResult(t, "computeReviewEnforced", gotControls, err, tt.expectedControls, tt.expectError, tt.expectedErrorContains) }) } } @@ -1158,7 +1145,6 @@ func TestComputeOrgControls(t *testing.T) { } func TestComputeSlsaLevel(t *testing.T) { - //now := timestamppb.Now() rnow := time.Now() now := &rnow rearlier := time.Now().Add(-time.Hour) @@ -1277,7 +1263,7 @@ func TestComputeSlsaLevel(t *testing.T) { func controlsForLevelWithOverride(level slsa.SlsaSourceLevel, baseSince *time.Time, overrides map[slsa.ControlName]time.Time) *slsa.ControlSet { controls := controlsForLevel(level, baseSince) for i, c := range controls.Controls { - if since, ok := overrides[slsa.ControlName(c.GetName())]; ok { + if since, ok := overrides[c.GetName()]; ok { controls.Controls[i] = &slsa.Control{Name: c.GetName(), Since: &since, State: slsa.StateActive} } } diff --git a/pkg/slsa/slsa_types.go b/pkg/slsa/slsa_types.go index 6b31c4a..4f255b6 100644 --- a/pkg/slsa/slsa_types.go +++ b/pkg/slsa/slsa_types.go @@ -7,8 +7,9 @@ import ( "slices" "time" - "github.com/slsa-framework/source-tool/pkg/provenance" "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/slsa-framework/source-tool/pkg/provenance" ) const ( @@ -74,9 +75,9 @@ func NewControlSetFromProvanenaceControls(provControls []*provenance.Control) *C } for _, ctl := range provControls { - t := ctl.Since.AsTime() + t := ctl.GetSince().AsTime() set.Controls = append(set.Controls, &Control{ - Name: ControlName(ctl.Name), + Name: ControlName(ctl.GetName()), State: StateActive, Since: &t, }) @@ -206,7 +207,7 @@ func (cs *ControlSet) Names() []ControlName { } func (cs *ControlSet) ToProvenanceControls() []*provenance.Control { - var ret = []*provenance.Control{} + ret := []*provenance.Control{} for _, ctl := range cs.Controls { if ctl.State != StateActive { continue diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index 71431ba..f2eaa8e 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -187,7 +187,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. if c := activeControls.GetControl(ctrl.Name); c != nil { status.Controls[i].Since = ctrl.Since status.Controls[i].State = slsa.StateActive - status.Controls[i].Message = b.controlImplementationMessage(slsa.ControlName(c.GetName())) + status.Controls[i].Message = b.controlImplementationMessage(c.GetName()) } // Enable ORG_SAFE_EXPUNGE when branch protection (protected refs) is active. From 34553639cd22e1aef6ec970b10c5b9584ac4db8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Sun, 8 Mar 2026 23:44:33 -0600 Subject: [PATCH 11/41] Redesign of the attester API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- go.mod | 154 +++--- go.sum | 483 +++++++++--------- pkg/attest/attester.go | 201 ++++++++ pkg/attest/provenance.go | 315 ++++++------ pkg/attest/statement.go | 210 ++++---- pkg/attest/vsa.go | 121 +---- .../backends/attestation/notes/backend.go | 79 --- 7 files changed, 791 insertions(+), 772 deletions(-) create mode 100644 pkg/attest/attester.go delete mode 100644 pkg/sourcetool/backends/attestation/notes/backend.go diff --git a/go.mod b/go.mod index 6c66a00..f9817c0 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module github.com/slsa-framework/source-tool -go 1.25.7 +go 1.25.8 require ( - github.com/carabiner-dev/attestation v0.2.0 - github.com/carabiner-dev/collector v0.2.8 + github.com/carabiner-dev/attestation v0.2.1 + github.com/carabiner-dev/collector v0.2.10-0.20260309053530-b55bbe428700 github.com/carabiner-dev/signer v0.3.7 github.com/carabiner-dev/vcslocator v0.4.0 github.com/fatih/color v1.18.0 - github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 - github.com/go-git/go-git/v6 v6.0.0-20250711134917-1f24ae85fe16 + github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f + github.com/go-git/go-git/v6 v6.0.0-20260305211659-2083cf940afa github.com/google/go-github/v69 v69.2.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-retryablehttp v0.7.8 @@ -25,9 +25,9 @@ require ( require ( dario.cat/mergo v1.0.2 // indirect - github.com/CycloneDX/cyclonedx-go v0.9.2 // indirect + github.com/CycloneDX/cyclonedx-go v0.10.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/avast/retry-go/v4 v4.7.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect @@ -36,87 +36,88 @@ require ( github.com/carabiner-dev/github v0.2.2 // indirect github.com/carabiner-dev/hasher v0.2.3 // indirect github.com/carabiner-dev/jsonl v0.2.1 // indirect - github.com/carabiner-dev/openeox v0.0.0-20251126193927-142e907140f5 // indirect + github.com/carabiner-dev/openeox v0.0.0-20260302211234-88fe8a305401 // indirect github.com/carabiner-dev/osv v0.0.0-20250124012120-b8ce4531cd92 // indirect - github.com/carabiner-dev/policy v0.4.1-0.20251211203139-302be2dfaf0d // indirect + github.com/carabiner-dev/policy v0.4.2 // indirect github.com/carabiner-dev/predicates v0.1.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/clipperhouse/displaywidth v0.6.0 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.6.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect - github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect - github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect - github.com/docker/cli v29.0.3+incompatible // indirect + github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c // indirect + github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea // indirect + github.com/docker/cli v29.3.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect - github.com/go-git/go-billy/v5 v5.7.0 // indirect - github.com/go-git/go-git/v5 v5.16.5 // indirect + github.com/go-git/go-billy/v5 v5.8.0 // indirect + github.com/go-git/go-git/v5 v5.17.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.24.1 // indirect - github.com/go-openapi/errors v0.22.4 // indirect - github.com/go-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.3 // indirect - github.com/go-openapi/loads v0.23.2 // indirect - github.com/go-openapi/runtime v0.29.2 // indirect - github.com/go-openapi/spec v0.22.1 // indirect - github.com/go-openapi/strfmt v0.25.0 // indirect - github.com/go-openapi/swag v0.25.4 // indirect - github.com/go-openapi/swag/cmdutils v0.25.4 // indirect - github.com/go-openapi/swag/conv v0.25.4 // indirect - github.com/go-openapi/swag/fileutils v0.25.4 // indirect - github.com/go-openapi/swag/jsonname v0.25.4 // indirect - github.com/go-openapi/swag/jsonutils v0.25.4 // indirect - github.com/go-openapi/swag/loading v0.25.4 // indirect - github.com/go-openapi/swag/mangling v0.25.4 // indirect - github.com/go-openapi/swag/netutils v0.25.4 // indirect - github.com/go-openapi/swag/stringutils v0.25.4 // indirect - github.com/go-openapi/swag/typeutils v0.25.4 // indirect - github.com/go-openapi/swag/yamlutils v0.25.4 // indirect - github.com/go-openapi/validate v0.25.1 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-openapi/analysis v0.24.3 // indirect + github.com/go-openapi/errors v0.22.7 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/loads v0.23.3 // indirect + github.com/go-openapi/runtime v0.29.3 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/strfmt v0.26.0 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/swag/cmdutils v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/fileutils v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/mangling v0.25.5 // indirect + github.com/go-openapi/swag/netutils v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + github.com/go-openapi/validate v0.25.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/certificate-transparency-go v1.3.2 // indirect + github.com/google/certificate-transparency-go v1.3.3 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-containerregistry v0.20.7 // indirect + github.com/google/go-containerregistry v0.21.2 // indirect github.com/google/go-github/v73 v73.0.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/in-toto/in-toto-golang v0.9.0 // indirect + github.com/in-toto/in-toto-golang v0.10.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 // indirect - github.com/kevinburke/ssh_config v1.4.0 // indirect - github.com/klauspost/compress v1.18.1 // indirect + github.com/kevinburke/ssh_config v1.6.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect - github.com/oklog/ulid v1.3.1 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect - github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.1.3 // indirect - github.com/olekukonko/tablewriter v1.1.2 // indirect + github.com/olekukonko/errors v1.2.0 // indirect + github.com/olekukonko/ll v0.1.7 // indirect + github.com/olekukonko/tablewriter v1.1.3 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/openvex/go-vex v0.2.7 // indirect - github.com/package-url/packageurl-go v0.1.4 // indirect + github.com/package-url/packageurl-go v0.1.5 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect @@ -127,40 +128,39 @@ require ( github.com/sergi/go-diff v1.4.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/protobuf-specs v0.5.0 // indirect - github.com/sigstore/rekor v1.4.3 // indirect - github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect + github.com/sigstore/rekor v1.5.0 // indirect + github.com/sigstore/rekor-tiles/v2 v2.2.1 // indirect github.com/sigstore/sigstore v1.10.4 // indirect - github.com/sigstore/timestamp-authority/v2 v2.0.3 // indirect + github.com/sigstore/timestamp-authority/v2 v2.0.5 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/skeema/knownhosts v1.3.2 // indirect - github.com/spdx/tools-golang v0.5.5 // indirect + github.com/spdx/tools-golang v0.5.7 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect github.com/theupdateframework/go-tuf/v2 v2.4.1 // indirect - github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect + github.com/transparency-dev/formats v0.1.0 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - go.mongodb.org/mongo-driver v1.17.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.32.0 // indirect - golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect - google.golang.org/grpc v1.76.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.79.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.140.0 // indirect ) diff --git a/go.sum b/go.sum index fcf4ce7..9306205 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,25 @@ -cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= -cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= +cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k= -cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= -cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= -cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ= +cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= @@ -30,16 +30,15 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfg github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo= -github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= +github.com/CycloneDX/cyclonedx-go v0.10.0 h1:7xyklU7YD+CUyGzSFIARG18NYLsKVn4QFg04qSsu+7Y= +github.com/CycloneDX/cyclonedx-go v0.10.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= +github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -50,44 +49,46 @@ github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Z github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= -github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= -github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc= -github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0= -github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg= -github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= +github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= -github.com/aws/aws-sdk-go-v2/service/kms v1.48.2 h1:aL8Y/AbB6I+uw0MjLbdo68NQ8t5lNs3CY3S848HpETk= -github.com/aws/aws-sdk-go-v2/service/kms v1.48.2/go.mod h1:VJcNH6BLr+3VJwinRKdotLOMglHO8mIKlD3ea5c7hbw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= -github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= -github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= -github.com/carabiner-dev/attestation v0.2.0 h1:vEqAIapcHjIoEQad9GrKtEx2czeu7t4cun+1bCEtN1o= -github.com/carabiner-dev/attestation v0.2.0/go.mod h1:dLPe3DPL/0YpKJpNDCQJZdtkZIuWTAH1G0N8V5LJ41c= -github.com/carabiner-dev/collector v0.2.8 h1:TQCWbpdM8xIuJuj0seu6z0zNZVNzczqvCKuaist+lbk= -github.com/carabiner-dev/collector v0.2.8/go.mod h1:TDQ75MeBpoIOSGCtTWQrTxwtYcj/9rIJjonCix4F4HQ= +github.com/carabiner-dev/attestation v0.2.1 h1:VhjV5YlO9TsW50Sr/Zd54bdbZhhDAqgxC3kB9z1I+3Q= +github.com/carabiner-dev/attestation v0.2.1/go.mod h1:O84vF84RZG3pJO/6BYrPs718bZviHF5DKajP1HsrDpw= +github.com/carabiner-dev/collector v0.2.10-0.20260309053530-b55bbe428700 h1:S4NFLl3/UJvA4aUuPVY3MECUQS0jhHdT//+excPvWUc= +github.com/carabiner-dev/collector v0.2.10-0.20260309053530-b55bbe428700/go.mod h1:ltqIryrtd+Ev+zAW9Q2h/UHNVMhT2nR1Fmchk+8lbs8= github.com/carabiner-dev/ghrfs v0.3.4 h1:XJoDXkuw+8KQPTC4oI0da8vLpnx7cfQBGgyjzo+Eqrc= github.com/carabiner-dev/ghrfs v0.3.4/go.mod h1:u9We7molIUX6sCe4ox70juKOnbNAUpDv+B5Cerbqhio= github.com/carabiner-dev/github v0.2.2 h1:Ykrlcct71fRQm4j37LhAz9FyzG4n1nlm2e+V62MIoJM= @@ -96,12 +97,12 @@ github.com/carabiner-dev/hasher v0.2.3 h1:678/FKyUJexzDznoIomnW/Z71hoL6Ehz99W2ZP github.com/carabiner-dev/hasher v0.2.3/go.mod h1:TRannDvJs5eL61xA/K/whtYbZLQPb/CGFXtDnx4h37M= github.com/carabiner-dev/jsonl v0.2.1 h1:2cBv4mGoqWfVkeQO1krIGds9WUm9Jet89QdqWLodlfo= github.com/carabiner-dev/jsonl v0.2.1/go.mod h1:fTypVHMYsAnn6LlW0SqER6XUaI9Wlc/Kv6uyMF+NmKo= -github.com/carabiner-dev/openeox v0.0.0-20251126193927-142e907140f5 h1:yBdk08BZwlHbSZVKHkvWJbu+ugNruYnCWGzCQ/GNQzg= -github.com/carabiner-dev/openeox v0.0.0-20251126193927-142e907140f5/go.mod h1:grnE4r9+/9Cr+Wvh2Lx1Lj7gQ+Mdl2qf0LZA85m8PLM= +github.com/carabiner-dev/openeox v0.0.0-20260302211234-88fe8a305401 h1:fexoMxtBCeYSxNJcd0wTAO3rYyq77wv7AqnNrEyo0JI= +github.com/carabiner-dev/openeox v0.0.0-20260302211234-88fe8a305401/go.mod h1:YDuJw950hcWHl/WWJTpmyAMfRRY3rNXgy4NxnTtRTIY= github.com/carabiner-dev/osv v0.0.0-20250124012120-b8ce4531cd92 h1:BJ9+OCNezZGkU8SrGC3oB7Tj+J0JsonwfZztcgUav6c= github.com/carabiner-dev/osv v0.0.0-20250124012120-b8ce4531cd92/go.mod h1:o7jXwi/fFZ9mQlvVlog0kcvyEkwQT3eWmVQmrorBGpE= -github.com/carabiner-dev/policy v0.4.1-0.20251211203139-302be2dfaf0d h1:nfzLqV09Ray2NvjUxH//o5NE/jP/AnCKOmj/xEixvZk= -github.com/carabiner-dev/policy v0.4.1-0.20251211203139-302be2dfaf0d/go.mod h1:ufVsPpVkUZPxsdI8yxokSSd5dqqDhp1nebbVpjLvvqg= +github.com/carabiner-dev/policy v0.4.2 h1:5yMIqui603UgpUft6wwub7+sErgsCqUJuElZF/McA3U= +github.com/carabiner-dev/policy v0.4.2/go.mod h1:T7ruDEM6F7xWm6OzYRY9s+QiSMsh/VPDufZakCrlYOg= github.com/carabiner-dev/predicates v0.1.0 h1:t6tQF9gFdr6TIccWtuNk3kFasx8eu88INFVGkCUnjL4= github.com/carabiner-dev/predicates v0.1.0/go.mod h1:jL6EAD+LiI6GW/rOdRYAJF4HaA88/V2Q4n7yUGNQ7XM= github.com/carabiner-dev/signer v0.3.7 h1:oEmOg17Szs5+x0oVVGXluPNsVaQyROlXrBPxgwxfOHg= @@ -112,20 +113,20 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s= -github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= -github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= -github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= -github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -133,23 +134,23 @@ github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= -github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= -github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= -github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= -github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= -github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= -github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= -github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= -github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c h1:g349iS+CtAvba7i0Ee9EP1TlTZ9w+UncBY6HSmsFZa0= +github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c/go.mod h1:mCGGmWkOQvEuLdIRfPIpXViBfpWto4AhwtJlAvo62SQ= +github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea h1:ALRwvjsSP53QmnN3Bcj0NpR8SsFLnskny/EIMebAk1c= +github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= +github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk= +github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= -github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= +github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -160,25 +161,24 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= -github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= -github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= -github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= -github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= -github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= -github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 h1:4KqVJTL5eanN8Sgg3BV6f2/QzfZEFbCd+rTak1fGRRA= -github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30/go.mod h1:snwvGrbywVFy2d6KJdQ132zapq4aLyzLMgpo79XdEfM= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= +github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f h1:Uvbx7nITO3Sd1GdXarX0TbyYmOaSNIJP0mm4LocEyyA= +github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git-fixtures/v5 v5.1.0 h1:b8cWxDLTk0s09Ihm9x1HvNGUzxUVlRwIH7EAM0gGDKg= -github.com/go-git/go-git-fixtures/v5 v5.1.0/go.mod h1:CdmU0oQeDuy4Xh8V0i9Ym+vsTkgDDPKEiofBFEVT+aE= -github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= -github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= -github.com/go-git/go-git/v6 v6.0.0-20250711134917-1f24ae85fe16 h1:LGHFWd3pmIuMugWNIQfBd1CY6k6Gy+XuJ/VnNQZCXWg= -github.com/go-git/go-git/v6 v6.0.0-20250711134917-1f24ae85fe16/go.mod h1:gI6xSrrkXH4EKP38iovrsY2EYf2XDU3DrIZRshlNDm0= +github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII= +github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw= +github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= +github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= +github.com/go-git/go-git/v6 v6.0.0-20260305211659-2083cf940afa h1:fIbZ264qSeJ+GRz+5nq6SFonkCanp/6CRXhYutq8GlE= +github.com/go-git/go-git/v6 v6.0.0-20260305211659-2083cf940afa/go.mod h1:V/qoTD4qCYizR+fKFA9++d2APoE8Yheci7dXALaSeuI= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -186,60 +186,62 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM= -github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84= -github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM= -github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= -github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= -github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= -github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= -github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= -github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= -github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0= -github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0= -github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= -github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= -github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= -github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= -github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= -github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= -github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= -github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= -github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= -github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= -github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= -github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= -github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= -github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= -github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= -github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= -github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= -github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= -github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= -github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= -github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= -github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= -github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= -github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= -github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= -github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= -github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= +github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= +github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= +github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU= +github.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= +github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= +github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= +github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= +github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= +github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= +github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -250,21 +252,19 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= -github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo= +github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= -github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= +github.com/google/go-containerregistry v0.21.2 h1:vYaMU4nU55JJGFC9JR/s8NZcTjbE9DBBbvusTW9NeS0= +github.com/google/go-containerregistry v0.21.2/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= github.com/google/go-github/v73 v73.0.0 h1:aR+Utnh+Y4mMkS+2qLQwcQ/cF9mOTpdwnzlaw//rG24= github.com/google/go-github/v73 v73.0.0/go.mod h1:fa6w8+/V+edSU0muqdhCVY7Beh1M8F1IlQPZIANKIYw= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -273,16 +273,16 @@ github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81zxNQw05s= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= +github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -311,16 +311,16 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= -github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= -github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= +github.com/in-toto/in-toto-golang v0.10.0 h1:+s2eZQSK3WmWfYV85qXVSBfqgawi/5L02MaqA4o/tpM= +github.com/in-toto/in-toto-golang v0.10.0/go.mod h1:wjT4RiyFlLWCmLUJjwB8oZcjaq7HA390aMJcD3xXgmg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= -github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -331,12 +331,12 @@ github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= -github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= +github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -354,8 +354,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/maxbrunsfeld/counterfeiter/v6 v6.12.1 h1:D4O2wLxB384TS3ohBJMfolnxb4qGmoZ1PnWNtit8LYo= github.com/maxbrunsfeld/counterfeiter/v6 v6.12.1/go.mod h1:RuJdxo0oI6dClIaMzdl3hewq3a065RH65dofJP03h8I= github.com/migueleliasweb/go-github-mock v1.5.0 h1:dIr6vgVz8QY9sDiDopWxk6pDw4d7K/xIcCk/NQe4ajM= @@ -368,16 +368,16 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0 github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= -github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg= -github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= -github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc= -github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg= +github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= +github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.7 h1:WyK1YZwOTUKHEXZz3VydBDT5t3zDqa9yI8iJg5PHon4= +github.com/olekukonko/ll v0.1.7/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= +github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= +github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -386,8 +386,9 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/openvex/go-vex v0.2.7 h1:/pN3bqvS4QOc6WkkL0hbKzJuAtsUD9vmvk9IZkzD3Zc= github.com/openvex/go-vex v0.2.7/go.mod h1:ZyQC3NXl9jjS53JOpBG3LAUXySkW8IlJ/GIhsnf5D54= -github.com/package-url/packageurl-go v0.1.4 h1:RHfiiN1SSY+Kic537DXch6fy593rxGJW6WDzAiOwNdk= -github.com/package-url/packageurl-go v0.1.4/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= +github.com/package-url/packageurl-go v0.1.5 h1:O4efRXja2XQ5CtiiYiCZ22k/m7i5ugLiAghgcC+eDgk= +github.com/package-url/packageurl-go v0.1.5/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -418,47 +419,39 @@ github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= -github.com/sigstore/rekor v1.4.3 h1:2+aw4Gbgumv8vYM/QVg6b+hvr4x4Cukur8stJrVPKU0= -github.com/sigstore/rekor v1.4.3/go.mod h1:o0zgY087Q21YwohVvGwV9vK1/tliat5mfnPiVI3i75o= -github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo= -github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU= +github.com/sigstore/rekor v1.5.0 h1:rL7SghHd5HLCtsCrxw0yQg+NczGvM75EjSPPWuGjaiQ= +github.com/sigstore/rekor v1.5.0/go.mod h1:D7JoVCUkxwQOpPDNYeu+CE8zeBC18Y5uDo6tF8s2rcQ= +github.com/sigstore/rekor-tiles/v2 v2.2.1 h1:UmV1CBQ3SjxxPGpFmwDoOhoIwiKpM2Qm1pU5tPGmvNk= +github.com/sigstore/rekor-tiles/v2 v2.2.1/go.mod h1:z8n6l6oidpaLjjE6rJERuQqY9X38ulnHZCXyL+DEL7U= github.com/sigstore/sigstore v1.10.4 h1:ytOmxMgLdcUed3w1SbbZOgcxqwMG61lh1TmZLN+WeZE= github.com/sigstore/sigstore v1.10.4/go.mod h1:tDiyrdOref3q6qJxm2G+JHghqfmvifB7hw+EReAfnbI= github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.0 h1:UOHpiyezCj5RuixgIvCV3QyuxIGQT+N6nGZEXA7OTTY= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.0/go.mod h1:U0CZmA2psabDa8DdiV7yXab0AHODzfKqvD2isH7Hrvw= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.0 h1:fq4+8Y4YadxeF8mzhoMRPZ1mVvDYXmI3BfS0vlkPT7M= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.0/go.mod h1:u05nqPWY05lmcdHhv2lPaWTH3FGUhJzO7iW2hbboK3Q= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.0 h1:iUEf5MZYOuXGnXxdF/WrarJrk0DTVHqeIOjYdtpVXtc= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.0/go.mod h1:i6vg5JfEQix46R1rhQlrKmUtJoeH91drltyYOJEk1T4= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.0 h1:dUvPv/MP23ZPIXZUW45kvCIgC0ZRfYxEof57AB6bAtU= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.0/go.mod h1:fR/gDdPvJWGWL70/NgBBIL1O0/3Wma6JHs3tSSYg3s4= -github.com/sigstore/timestamp-authority/v2 v2.0.3 h1:sRyYNtdED/ttLCMdaYnwpf0zre1A9chvjTnCmWWxN8Y= -github.com/sigstore/timestamp-authority/v2 v2.0.3/go.mod h1:mDaHxkt3HmZYoIlwYj4QWo0RUr7VjYU52aVO5f5Qb3I= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4 h1:VZ+L6SKVWbLPHznIF0tBuO7qKMFdJiJMVwFKu9DlY5o= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4/go.mod h1:Rstj47WpJym25il8j4jTL0BfikzP/9AhVD+DsBcYzZc= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.4 h1:G7yOv8bxk3zIEEZyVCixPxtePIAm+t3ZWSaKRPzVw+o= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.4/go.mod h1:hxJelB/bRItMYOzi6qD9xEKjse2QZcikh4TbysfdDHc= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.4 h1:Qxt6dE4IwhJ6gIXmg2q4S/SeqEDSZ29nmfsv7Zb6LL4= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.4/go.mod h1:hJVeNOwarqfyALjOwsf0OR8YA/A96NABucEaQumPr30= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.4 h1:KVavYMPfSf5NryOl6VrZ9nRG3fXOOJOPp7Czk/YCPkM= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.4/go.mod h1:J7CA1AaBkyK8dYq6EdQANhj+8oEcsA7PrIp088qgPiY= +github.com/sigstore/timestamp-authority/v2 v2.0.5 h1:WT17MU4bNRvjRLlTvTO5gmrSIWJVbzwrNXgwsjB+53U= +github.com/sigstore/timestamp-authority/v2 v2.0.5/go.mod h1:oV+Yy0GsfgNAeDZcv/WJjQE42wFtMTtuD85bPLAQk5M= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= -github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= -github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= -github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= +github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg= +github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= @@ -471,14 +464,14 @@ github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuX github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= -github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0 h1:6nAX1aRGnkg2SEUMwO5toB2tQkP0Jd6cbmZ/K5Le1V0= -github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0/go.mod h1:HOC5NWW1wBI2Vke1FGcRBvDATkEYE7AUDiYbXqi2sBw= -github.com/tink-crypto/tink-go/v2 v2.5.0 h1:B8KLF6AofxdBIE4UJIaFbmoj5/1ehEtt7/MmzfI4Zpw= -github.com/tink-crypto/tink-go/v2 v2.5.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= +github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0 h1:j+S+WKBQ5ya26A5EM/uXoVe+a2IaPQN8KgBJZ22cJ+4= +github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0/go.mod h1:OCKJIujnTzDq7f+73NhVs99oA2c1TR6nsOpuasYM6Yo= +github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4= +github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= -github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= -github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= +github.com/transparency-dev/formats v0.1.0 h1:oL0zUFuYUjg8AbtjPMnIRDmjbaHo5jCjEWU5yaNuz0g= +github.com/transparency-dev/formats v0.1.0/go.mod h1:d2FibUOHfCMdCe/+/rbKt1IPLBbPTDfwj46kt541/mU= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= @@ -505,62 +498,58 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= -go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= -go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.step.sm/crypto v0.74.0 h1:/APBEv45yYR4qQFg47HA8w1nesIGcxh44pGyQNw6JRA= -go.step.sm/crypto v0.74.0/go.mod h1:UoXqCAJjjRgzPte0Llaqen7O9P7XjPmgjgTHQGkKCDk= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4= +go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -572,40 +561,40 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= -google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= -google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= -google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= -google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= -google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU= +google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -616,16 +605,14 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= sigs.k8s.io/release-utils v0.12.3 h1:iNVJY81QfmMCmXxMg8IvvkkeQNk6ZWlLj+iPKSlKyVQ= sigs.k8s.io/release-utils v0.12.3/go.mod h1:BvbNmm1BmM3cnEpBmNHWL3wOSziOdGlsYR8vCFq/Q0o= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= diff --git a/pkg/attest/attester.go b/pkg/attest/attester.go new file mode 100644 index 0000000..b2d8edf --- /dev/null +++ b/pkg/attest/attester.go @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors +// SPDX-License-Identifier: Apache-2.0 + +package attest + +import ( + "context" + "errors" + "fmt" + "slices" + "time" + + "github.com/carabiner-dev/attestation" + "github.com/carabiner-dev/collector" + intoto "github.com/in-toto/attestation/go/v1" + "github.com/slsa-framework/source-tool/pkg/provenance" + "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type AttesterOptions struct { + // Initialize dynamic notes collector + InitNotesCollector bool + // Initialize attestations store collector + InitGHCollector bool + // Additional read repositories + Repos []string + // Times to retry fetching attestations + Retries uint8 +} + +var defaultAttesterOptions = AttesterOptions{ + InitNotesCollector: true, + Retries: 3, +} + +type Attester struct { + verifier Verifier + backend models.VcsBackend + Options AttesterOptions + collector *collector.Agent + storer attestation.Storer +} + +type optFn func(*Attester) error + +func WithRepository(repos ...string) optFn { + return func(a *Attester) error { + // TODO(puerco): Validate the repository strings + for _, s := range repos { + if !slices.Contains(a.Options.Repos, s) { + a.Options.Repos = append(a.Options.Repos, s) + } + } + return nil + } +} + +func WithRetries(r uint8) optFn { + return func(a *Attester) error { + a.Options.Retries = r + return nil + } +} + +func WithBackend(b models.VcsBackend) optFn { + return func(a *Attester) error { + a.backend = b + return nil + } +} + +func WithVerifier(vf Verifier) optFn { + return func(a *Attester) error { + a.verifier = vf + return nil + } +} + +func WithGithubCollector(yesno bool) optFn { + return func(a *Attester) error { + a.Options.InitGHCollector = yesno + return nil + } +} + +func WithNotesCollector(yesno bool) optFn { + return func(a *Attester) error { + a.Options.InitNotesCollector = yesno + return nil + } +} + +// Validate checks that the attester configuration is complete +func (a *Attester) Validate() error { + var errs = []error{} + if a.backend == nil { + errs = append(errs, errors.New("attester has no backend defined")) + } + + if len(a.Options.Repos) == 0 && !a.Options.InitGHCollector && !a.Options.InitNotesCollector { + errs = append(errs, errors.New("no attestation repository configured")) + } + + if a.verifier == nil { + errs = append(errs, errors.New("attester has no verifier")) + } + + if a.storer == nil { + // errs = append(errs, errors.New("attester has no attestation storer defined")) + } + + return errors.Join(errs...) +} + +// NewAttester creates a new attester +func NewAttester(fn ...optFn) (*Attester, error) { + attester := &Attester{ + Options: defaultAttesterOptions, + } + + for _, f := range fn { + if err := f(attester); err != nil { + return nil, err + } + } + + // Check attester configuration + if err := attester.Validate(); err != nil { + return nil, err + } + + return attester, nil +} + +// createCurrentProvenance creates the provenance statement for the specified commit +// without any context from the previous provenance (if any). +func (a *Attester) createCurrentProvenance(ctx context.Context, branch *models.Branch, commit, prevCommit *models.Commit) (*intoto.Statement, error) { + // Get the active controls + controlStatus, err := a.backend.GetBranchControlsAtCommit(ctx, branch, commit) + if err != nil { + return nil, err + } + + // Build the provenance predicate + var curProvPred = provenance.SourceProvenancePred{ + PrevCommit: prevCommit.SHA, + RepoUri: branch.Repository.GetHttpURL(), + ActivityType: controlStatus.ActivityType, + Actor: controlStatus.ActorLogin, + Branch: branch.FullRef(), + CreatedOn: timestamppb.New(time.Now()), + Controls: controlStatus.ToProvenanceControls(), + } + + // At the very least provenance is available starting now. :) + // ... indeed, but don't set the `since`` date because doing so breaks + // checking against policies. + // See https://github.com/slsa-framework/source-tool/issues/272 + curProvPred.AddControl( + &provenance.Control{ + Name: slsa.SLSA_SOURCE_SCS_PROVENANCE.String(), + }, + ) + + return addPredToStatement(&curProvPred, provenance.SourceProvPredicateType, commit.SHA) +} + +// addPredToStatement generates a new statement and adds the provenance predicate +func addPredToStatement(provPred proto.Message, predicateType, commit string) (*intoto.Statement, error) { + predJson, err := protojson.MarshalOptions{ + Multiline: true, + Indent: " ", + }.Marshal(provPred) + if err != nil { + return nil, fmt.Errorf("marshaling predicate proto: %w", err) + } + + sub := []*intoto.ResourceDescriptor{{ + Digest: map[string]string{"gitCommit": commit}, + }} + + var predPb structpb.Struct + err = protojson.Unmarshal(predJson, &predPb) + if err != nil { + return nil, err + } + + statementPb := &intoto.Statement{ + Type: intoto.StatementTypeUri, + Subject: sub, + PredicateType: predicateType, + Predicate: &predPb, + } + + return statementPb, nil +} diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index 74608a3..11e48e3 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -1,44 +1,29 @@ -// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors +// SPDX-FileCopyrightText: Copyright 2026 The SLSA Authors // SPDX-License-Identifier: Apache-2.0 package attest import ( - "bufio" "context" "errors" "fmt" - "os" "strings" "time" - v1 "github.com/in-toto/attestation/go/predicates/vsa/v1" - spb "github.com/in-toto/attestation/go/v1" + "github.com/carabiner-dev/attestation" + "github.com/carabiner-dev/collector" + "github.com/carabiner-dev/collector/filters" + vsa "github.com/in-toto/attestation/go/predicates/vsa/v1" + intoto "github.com/in-toto/attestation/go/v1" "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) -type ProvenanceAttestorOptions struct { - VsaRetries uint8 -} - -type ProvenanceAttestor struct { - verifier Verifier - gh_connection *ghcontrol.GitHubConnection - Options ProvenanceAttestorOptions -} - -func NewProvenanceAttestor(gh_connection *ghcontrol.GitHubConnection, verifier Verifier) *ProvenanceAttestor { - return &ProvenanceAttestor{verifier: verifier, gh_connection: gh_connection} -} - -func GetSourceProvPred(statement *spb.Statement) (*provenance.SourceProvenancePred, error) { +func GetSourceProvPred(statement *intoto.Statement) (*provenance.SourceProvenancePred, error) { if statement == nil { return nil, errors.New("nil statement") } @@ -65,7 +50,7 @@ func GetSourceProvPred(statement *spb.Statement) (*provenance.SourceProvenancePr return &predStruct, nil } -func GetTagProvPred(statement *spb.Statement) (*provenance.TagProvenancePred, error) { +func GetTagProvPred(statement *intoto.Statement) (*provenance.TagProvenancePred, error) { if statement == nil { return nil, errors.New("nil statement") } @@ -92,164 +77,185 @@ func GetTagProvPred(statement *spb.Statement) (*provenance.TagProvenancePred, er return &predStruct, nil } -func addPredToStatement(provPred any, predicateType, commit string) (*spb.Statement, error) { - msg, ok := provPred.(proto.Message) - if !ok { - return nil, fmt.Errorf("unable to serialize predicate as proto message") +func (a *Attester) getCollector(branch *models.Branch) (*collector.Agent, error) { + if err := collector.LoadDefaultRepositoryTypes(); err != nil { + return nil, err } - predJson, err := protojson.MarshalOptions{ - Multiline: true, - Indent: " ", - }.Marshal(msg) + agent, err := collector.New() if err != nil { - return nil, fmt.Errorf("marshaling predicate proto: %w", err) + return nil, err } - sub := []*spb.ResourceDescriptor{{ - Digest: map[string]string{"gitCommit": commit}, - }} + if a.Options.InitNotesCollector { + if err := agent.AddRepositoryFromString(fmt.Sprintf("dnote:%s", branch.Repository.GetHttpURL())); err != nil { + return nil, err + } + } - var predPb structpb.Struct - err = protojson.Unmarshal(predJson, &predPb) - if err != nil { - return nil, err + if a.Options.InitGHCollector { + if err := agent.AddRepositoryFromString(fmt.Sprintf("github:%s", branch.Repository.Path)); err != nil { + return nil, err + } } - statementPb := spb.Statement{ - Type: spb.StatementTypeUri, - Subject: sub, - PredicateType: predicateType, - Predicate: &predPb, + for _, initString := range a.Options.Repos { + if err := agent.AddRepositoryFromString(initString); err != nil { + return nil, err + } } - return &statementPb, nil + return agent, nil } -// Create provenance for the current commit without any context from the previous provenance (if any). -func (pa ProvenanceAttestor) createCurrentProvenance(ctx context.Context, commit, prevCommit, ref string) (*spb.Statement, error) { - controlStatus, err := pa.gh_connection.GetBranchControlsAtCommit(ctx, commit, ref) +// GetVSA returns a revision's VSA attestation +func (a *Attester) GetRevisionVSA(ctx context.Context, branch *models.Branch, commit *models.Commit) (attestation.Envelope, *vsa.VerificationSummary, error) { + if commit == nil { + return nil, nil, errors.New("commit is nil") + } + c, err := a.getCollector(branch) if err != nil { - return nil, err + return nil, nil, fmt.Errorf("unable to get collector: %w", err) } - curTime := time.Now() - - var curProvPred provenance.SourceProvenancePred - curProvPred.PrevCommit = prevCommit - curProvPred.RepoUri = pa.gh_connection.GetRepoUri() - curProvPred.Actor = controlStatus.ActorLogin - curProvPred.ActivityType = controlStatus.ActivityType - curProvPred.Branch = ref - curProvPred.CreatedOn = timestamppb.New(curTime) - curProvPred.Controls = controlStatus.Controls.ToProvenanceControls() - - // At the very least provenance is available starting now. :) - // ... indeed, but don't set the `since`` date because doing so breaks - // checking against policies. - // See https://github.com/slsa-framework/source-tool/issues/272 - curProvPred.AddControl( - &provenance.Control{ - Name: slsa.SLSA_SOURCE_SCS_PROVENANCE.String(), + // Configure the matcher to filter all the predicate types of the provenance + matcher := &filters.PredicateTypeMatcher{ + PredicateTypes: map[attestation.PredicateType]struct{}{ + attestation.PredicateType("https://slsa.dev/verification_summary/v1"): {}, }, - ) - - return addPredToStatement(&curProvPred, provenance.SourceProvPredicateType, commit) -} - -// GetVSA returns the revision VSA -func (pa ProvenanceAttestor) GetVSA(ctx context.Context, commit, ref string) (*spb.Statement, *v1.VerificationSummary, error) { - notes, err := pa.gh_connection.GetNotesForCommit(ctx, commit) - if notes == "" { - Debugf("didn't find notes for commit %s", commit) - return nil, nil, nil } - if err != nil { - return nil, nil, fmt.Errorf("fetching notes from commit: %w", err) + var attErr error + var atts []attestation.Envelope + for i := 0; i <= int(a.Options.Retries); i++ { + // Fetch the attestations from the configured repos + atts, attErr = c.FetchAttestationsBySubject( + ctx, []attestation.Subject{commit.ToResourceDescriptor()}, + collector.WithQuery(attestation.NewQuery().WithFilter(matcher)), + ) + if attErr == nil { + break + } + time.Sleep(time.Duration(i*5) * time.Second) } - - bundleReader := NewBundleReader(bufio.NewReader(strings.NewReader(notes)), pa.verifier) - return getVsaFromReader(bundleReader, commit, ref, "") -} - -// Gets provenance for the commit from git notes. -func (pa ProvenanceAttestor) GetProvenance(ctx context.Context, commit, ref string) (*spb.Statement, *provenance.SourceProvenancePred, error) { - notes, err := pa.gh_connection.GetNotesForCommit(ctx, commit) - if notes == "" { - Debugf("didn't find notes for commit %s", commit) - return nil, nil, nil + if attErr != nil { + return nil, nil, fmt.Errorf("fetching attestations: %w", err) } - if err != nil { - return nil, nil, fmt.Errorf("fetching notes from commit: %w", err) + if len(atts) == 0 { + Debugf("no attestations returned from collector") + return nil, nil, nil } - bundleReader := NewBundleReader(bufio.NewReader(strings.NewReader(notes)), pa.verifier) - - return pa.getProvFromReader(bundleReader, commit, ref) -} - -func (pa ProvenanceAttestor) getProvFromReader(reader *BundleReader, commit, ref string) (*spb.Statement, *provenance.SourceProvenancePred, error) { - for { - stmt, err := reader.ReadStatement(MatchesTypeAndCommit(provenance.SourceProvPredicateType, commit)) - if err != nil { - // Ignore errors, we want to check all the lines. - Debugf("error while processing line: %v", err) + // Range the attestations and find and validate that it matches our repo + // and expected resources + for _, att := range atts { + predicate := att.GetPredicate().GetParsed() + vsaPred, ok := predicate.(*vsa.VerificationSummary) + if !ok { + // This should not happen continue } - if stmt == nil { - // No statements left. - break + // Check the verifier ID matches + if vsaPred.GetVerifier().GetId() != VsaVerifierId { + Debugf("VSA verfier ID does not match %s", VsaVerifierId) + continue } - // We know the statement is good, what about the predicate? - provPred, err := GetSourceProvPred(stmt) - if err != nil { - return nil, nil, err + // Check the VSA resource to ensure it is our repo + cleanResourceUri := strings.TrimPrefix(vsaPred.GetResourceUri(), "git+") + if branch.Repository.GetHttpURL() != "" && cleanResourceUri != branch.Repository.GetHttpURL() { + Debugf("ResourceUri is %s but we want %s", cleanResourceUri, branch.Repository.GetHttpURL()) + continue } - if pa.gh_connection.GetRepoUri() == provPred.GetRepoUri() && (ref == ghcontrol.AnyReference || provPred.GetBranch() == ref) { - // Should be good! - return stmt, provPred, nil - } else { - Debugf("prov '%v' does not reference commit '%s' for branch '%s', skipping", stmt, commit, ref) + + // And the verification is passed + // // Is the result PASSED? + if vsaPred.GetVerificationResult() != "PASSED" { + Debugf("verificationResult is %s but must be 'PASSED'", vsaPred.GetVerificationResult()) + continue } + + return att, vsaPred, nil } - Debugf("didn't find commit %s for ref %s", commit, ref) + // None of the collected attestations are valid return nil, nil, nil + } -func (pa ProvenanceAttestor) getPrevProvenance(ctx context.Context, prevAttPath, prevCommit, ref string) (*spb.Statement, *provenance.SourceProvenancePred, error) { - if prevAttPath != "" { - f, err := os.Open(prevAttPath) - if err != nil { - return nil, nil, err +// GetRevisionProvenance returns the provenance attestation for a commit by querying +// the configured collectors. +func (a *Attester) GetRevisionProvenance(ctx context.Context, branch *models.Branch, commit *models.Commit) (*provenance.SourceProvenancePred, error) { + c, err := a.getCollector(branch) + if err != nil { + return nil, fmt.Errorf("unable to get collector: %w", err) + } + + // Configure the matcher to filter all the predicate types of the provenance + matcher := &filters.PredicateTypeMatcher{ + PredicateTypes: map[attestation.PredicateType]struct{}{ + attestation.PredicateType(provenance.SourceProvPredicateType): {}, + }, + } + + var attErr error + var atts []attestation.Envelope + for i := 0; i <= int(a.Options.Retries); i++ { + // Fetch the attestations from the configured repos + atts, attErr = c.FetchAttestationsBySubject( + ctx, []attestation.Subject{commit.ToResourceDescriptor()}, + collector.WithQuery(attestation.NewQuery().WithFilter(matcher)), + ) + if attErr == nil { + break } - return pa.getProvFromReader(NewBundleReader(bufio.NewReader(f), pa.verifier), prevCommit, ref) + time.Sleep(time.Duration(i*5) * time.Second) + } + if attErr != nil { + return nil, fmt.Errorf("fetching attestations: %w", attErr) + } + + if len(atts) == 0 { + Debugf("no attestations returned from collector") + return nil, nil } - // Try to get the previous bundle ourselves... - return pa.GetProvenance(ctx, prevCommit, ref) + // Now extract the provenance + for _, att := range atts { + pred := &provenance.SourceProvenancePred{} + if err = protojson.Unmarshal(att.GetPredicate().GetData(), pred); err == nil { + return pred, nil + } + } + return nil, fmt.Errorf("unable to parse predicate: %w", err) } -func (pa ProvenanceAttestor) CreateSourceProvenance(ctx context.Context, prevAttPath, commit, prevCommit, ref string) (*spb.Statement, error) { +// prevAttPath string +func (a *Attester) CreateSourceProvenance(ctx context.Context, branch *models.Branch, commit *models.Commit) (*intoto.Statement, error) { + // Get the previous commit + prevCommit, err := a.backend.GetPreviousCommit(ctx, branch, commit) + if err != nil { + return nil, fmt.Errorf("getting previous commit: %w", err) + } + // Source provenance is based on // 1. The current control situation (we assume 'commit' has _just_ occurred). // 2. How long the properties have been enforced according to the previous provenance. - - curProv, err := pa.createCurrentProvenance(ctx, commit, prevCommit, ref) + curProv, err := a.createCurrentProvenance(ctx, branch, commit, prevCommit) if err != nil { - return nil, err + return nil, fmt.Errorf("creating provenance predicate: %w", err) } - prevProvStmt, prevProvPred, err := pa.getPrevProvenance(ctx, prevAttPath, prevCommit, ref) + //prevProvStmt, prevProvPred, err := a.GetRevisionProvenance(ctx, branch, prevCommit) + prevProvPred, err := a.GetRevisionProvenance(ctx, branch, prevCommit) if err != nil { return nil, err } // No prior provenance found, so we just go with current. - if prevProvStmt == nil || prevProvPred == nil { + //if prevProvStmt == nil || prevProvPred == nil { + if prevProvPred == nil { Debugf("No previous provenance found, have to bootstrap\n") return curProv, nil } @@ -272,54 +278,47 @@ func (pa ProvenanceAttestor) CreateSourceProvenance(ctx context.Context, prevAtt curProvPred.Controls[i] = curControl } - return addPredToStatement(curProvPred, provenance.SourceProvPredicateType, commit) + return addPredToStatement(curProvPred, provenance.SourceProvPredicateType, commit.SHA) } -func (pa ProvenanceAttestor) CreateTagProvenance(ctx context.Context, commit, ref, actor string) (*spb.Statement, error) { +// CreateTagProvenance creates a provenance statement for a tag. +func (a Attester) CreateTagProvenance(ctx context.Context, branch *models.Branch, tag *models.Tag, actor string) (*intoto.Statement, error) { + if tag.Commit == nil { + return nil, fmt.Errorf("tag does not have its commit set") + } + // 1. Check that the tag hygiene control is still enabled and how long it's been enabled, store it in the prov. // 2. Get a VSA associated with this commit, if any. // 3. Record the levels and branches covered by that VSA in the provenance. - - controlStatus, err := pa.gh_connection.GetTagControls(ctx, commit, ref) + controls, err := a.backend.GetTagControls(ctx, tag) if err != nil { - return nil, err + return nil, fmt.Errorf("getting tag controls: %w", err) } // Find the most recent VSA for this commit. Any reference is OK. // TODO: in the future get all of them. // TODO: we should actually verify this vsa: https://github.com/slsa-framework/source-tool/issues/148 - var tries uint8 - var vsaStatement *spb.Statement - var vsaPred *v1.VerificationSummary - for { - vsaStatement, vsaPred, err = GetVsa(ctx, pa.gh_connection, pa.verifier, commit, ghcontrol.AnyReference) - if err != nil { - return nil, fmt.Errorf("error fetching VSA when creating tag provenance %w", err) - } - - tries++ - if tries >= pa.Options.VsaRetries || vsaPred != nil { - break - } - time.Sleep(time.Duration(tries*5) * time.Second) + vsaAtt, vsaPred, err := a.GetRevisionVSA(ctx, branch, tag.Commit) // Aqui antes era: ghcontrol.AnyReference + if err != nil { + return nil, fmt.Errorf("error fetching VSA when creating tag provenance %w", err) } - if vsaPred == nil { - // TODO: If there's not a VSA should we still issue provenance? + if vsaAtt == nil { + // TODO: If there's not a VSA, should we still issue provenance? return nil, nil } - vsaRefs, err := GetSourceRefsForCommit(vsaStatement, commit) + vsaRefs, err := GetSourceRefsForCommit(vsaAtt, tag.Commit) if err != nil { return nil, fmt.Errorf("error getting source refs from vsa %w", err) } curProvPred := provenance.TagProvenancePred{ - RepoUri: pa.gh_connection.GetRepoUri(), + RepoUri: branch.Repository.GetHttpURL(), Actor: actor, - Tag: ref, + Tag: tag.Name, CreatedOn: timestamppb.Now(), - Controls: controlStatus.Controls.ToProvenanceControls(), + Controls: controls.ToProvenanceControls(), VsaSummaries: []*provenance.VsaSummary{ { SourceRefs: vsaRefs, @@ -328,5 +327,5 @@ func (pa ProvenanceAttestor) CreateTagProvenance(ctx context.Context, commit, re }, } - return addPredToStatement(&curProvPred, provenance.TagProvPredicateType, commit) + return addPredToStatement(&curProvPred, provenance.TagProvPredicateType, tag.Commit.SHA) } diff --git a/pkg/attest/statement.go b/pkg/attest/statement.go index 8ab57b8..f279776 100644 --- a/pkg/attest/statement.go +++ b/pkg/attest/statement.go @@ -5,15 +5,14 @@ package attest import ( "bufio" - "errors" "fmt" - "io" - "strings" - spb "github.com/in-toto/attestation/go/v1" + "github.com/carabiner-dev/attestation" + intoto "github.com/in-toto/attestation/go/v1" "google.golang.org/protobuf/encoding/protojson" "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type BundleReader struct { @@ -25,43 +24,44 @@ func NewBundleReader(reader *bufio.Reader, verifier Verifier) *BundleReader { return &BundleReader{reader: reader, verifier: verifier} } -func (br *BundleReader) convertLineToStatement(line string) (*spb.Statement, error) { - // Is this a sigstore bundle with a statement? - // Verify will check the signature, but nothing else. - vr, err := br.verifier.Verify(line) - if err == nil { - // This is it. - return vr.Statement, nil - } - - // Compatibility hack bridging identities for repository migration - // See here for more info and when to drop: - // - // https://github.com/slsa-framework/source-tool/issues/255 - if strings.Contains(err.Error(), "no matching CertificateIdentity") && strings.Contains(err.Error(), OldExpectedSan) { - ver, err := (&BndVerifier{ - Options: VerificationOptions{ - ExpectedIssuer: ExpectedIssuer, - ExpectedSan: OldExpectedSan, - }, - }).Verify(line) - if err == nil { - Debugf("found statement signed with old identity") - return ver.Statement, nil - } - } - - Debugf("Line '%s' failed verification: %v", line, err) - - // TODO: add support for 'regular' DSSEs. - - return nil, fmt.Errorf("could not convert line to statement: '%s': %w", line, err) -} - -func GetSourceRefsForCommit(vsaStatement *spb.Statement, commit string) ([]string, error) { - subject := GetSubjectForCommit(vsaStatement, commit) +// func (br *BundleReader) convertLineToStatement(line string) (*intoto.Statement, error) { +// // Is this a sigstore bundle with a statement? +// // Verify will check the signature, but nothing else. +// vr, err := br.verifier.Verify(line) +// if err == nil { +// // This is it. +// return vr.Statement, nil +// } + +// // Compatibility hack bridging identities for repository migration +// // See here for more info and when to drop: +// // +// // https://github.com/slsa-framework/source-tool/issues/255 +// if strings.Contains(err.Error(), "no matching CertificateIdentity") && strings.Contains(err.Error(), OldExpectedSan) { +// ver, err := (&BndVerifier{ +// Options: VerificationOptions{ +// ExpectedIssuer: ExpectedIssuer, +// ExpectedSan: OldExpectedSan, +// }, +// }).Verify(line) +// if err == nil { +// Debugf("found statement signed with old identity") +// return ver.Statement, nil +// } +// } + +// Debugf("Line '%s' failed verification: %v", line, err) + +// // TODO: add support for 'regular' DSSEs. + +// return nil, fmt.Errorf("could not convert line to statement: '%s': %w", line, err) +// } + +// GetSourceRefsForCommit returns the source branch annotations from the subject +func GetSourceRefsForCommit(att attestation.Envelope, commit *models.Commit) ([]string, error) { + subject := GetSubjectForCommit(att, commit) if subject == nil { - return []string{}, fmt.Errorf("statement \n%v\n does not match commit %s", StatementToString(vsaStatement), commit) + return []string{}, fmt.Errorf("statement \n%v\n does not match commit %s", string(att.GetPredicate().GetData()), commit) } annotations := subject.GetAnnotations() sourceRefs, ok := annotations.GetFields()[slsa.SourceRefsAnnotation] @@ -82,78 +82,92 @@ func GetSourceRefsForCommit(vsaStatement *spb.Statement, commit string) ([]strin return stringRefs, nil } -type StatementMatcher func(*spb.Statement) bool - -func MatchesTypeAndCommit(predicateType, commit string) StatementMatcher { - return func(statement *spb.Statement) bool { - if statement.GetPredicateType() != predicateType { - Debugf("statement predicate type (%s) doesn't match %s", statement.GetPredicateType(), predicateType) - return false - } - if !DoesSubjectIncludeCommit(statement, commit) { - Debugf("statement \n%v\n does not match commit %s", StatementToString(statement), commit) - return false - } - return true - } -} +// type StatementMatcher func(*intoto.Statement) bool + +// func MatchesTypeAndCommit(predicateType, commit string) StatementMatcher { +// return func(statement *intoto.Statement) bool { +// if statement.GetPredicateType() != predicateType { +// Debugf("statement predicate type (%s) doesn't match %s", statement.GetPredicateType(), predicateType) +// return false +// } +// if !DoesSubjectIncludeCommit(statement, commit) { +// Debugf("statement \n%v\n does not match commit %s", StatementToString(att.GetStatement()), commit) +// return false +// } +// return true +// } +// } // Reads all the statements that: // 1. Have a valid signature // 2. Have the specified predicate type. // 3. Have a subject that matches the specified git commit. -func (br *BundleReader) ReadStatement(matcher StatementMatcher) (*spb.Statement, error) { - // Read until we get a statement or end of file. - for { - line, err := br.reader.ReadString('\n') - if err != nil { - // Handle end of file gracefully - if !errors.Is(err, io.EOF) { - return nil, err - } - if line == "" { - // Nothing to see here. - break - } - } - if line == "\n" { - // skip empties - continue - } - statement, err := br.convertLineToStatement(line) - if err != nil { - // Ignore errors, the next line could be fine. - Debugf("problem converting line to statement line: '%s', error: %v", line, err) - } - if statement == nil { - // Not sure what this is, just continue +// func (br *BundleReader) ReadStatement(matcher StatementMatcher) (*intoto.Statement, error) { +// // Read until we get a statement or end of file. +// for { +// line, err := br.reader.ReadString('\n') +// if err != nil { +// // Handle end of file gracefully +// if !errors.Is(err, io.EOF) { +// return nil, err +// } +// if line == "" { +// // Nothing to see here. +// break +// } +// } +// if line == "\n" { +// // skip empties +// continue +// } +// statement, err := br.convertLineToStatement(line) +// if err != nil { +// // Ignore errors, the next line could be fine. +// Debugf("problem converting line to statement line: '%s', error: %v", line, err) +// } +// if statement == nil { +// // Not sure what this is, just continue +// continue +// } +// if matcher(statement) { +// return statement, nil +// } +// // If we loop again it's because that line didn't have a matching statement +// } +// return nil, nil +// } + +// func DoesSubjectIncludeCommit(statement *intoto.Statement, commit string) bool { +// return GetSubjectForCommit(statement, commit) != nil +// } + +// Returns the _first_ subject that includes the commit. +// TODO: add support for multiple subjects... +func GetSubjectForCommit(att attestation.Envelope, commit *models.Commit) *intoto.ResourceDescriptor { + var fromSha *intoto.ResourceDescriptor + for _, subject := range att.GetStatement().GetSubjects() { + rd, ok := subject.(*intoto.ResourceDescriptor) + if !ok { continue } - if matcher(statement) { - return statement, nil + val, ok := rd.GetDigest()["gitCommit"] + if ok && val == commit.SHA { + return rd } - // If we loop again it's because that line didn't have a matching statement - } - return nil, nil -} - -func DoesSubjectIncludeCommit(statement *spb.Statement, commit string) bool { - return GetSubjectForCommit(statement, commit) != nil -} -// Returns the _first_ subject that includes the commit. -// TODO: add support for multiple subjects... -func GetSubjectForCommit(statement *spb.Statement, commit string) *spb.ResourceDescriptor { - for _, subject := range statement.GetSubject() { - if subject.GetDigest()["gitCommit"] == commit { - return subject + val, ok = rd.GetDigest()["sha1"] + if rd.GetDigest()["sha1"] == commit.SHA { + return fromSha } } - return nil + + // Prefer the gitCommit version, but if we saw a sha1 of the commit + // sha return the backup resource descriptor. + return fromSha } // Just make this easy for logging... -func StatementToString(stmt *spb.Statement) string { +func StatementToString(stmt *intoto.Statement) string { if stmt == nil { return "" } diff --git a/pkg/attest/vsa.go b/pkg/attest/vsa.go index c366f89..bdff404 100644 --- a/pkg/attest/vsa.go +++ b/pkg/attest/vsa.go @@ -4,10 +4,7 @@ package attest import ( - "bufio" - "context" "fmt" - "strings" vpb "github.com/in-toto/attestation/go/predicates/vsa/v1" spb "github.com/in-toto/attestation/go/v1" @@ -15,8 +12,8 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) const ( @@ -24,12 +21,14 @@ const ( VsaVerifierId = "https://github.com/slsa-framework/source-actions" ) -func CreateUnsignedSourceVsa(repoUri, ref, commit string, verifiedLevels slsa.SourceVerifiedLevels, policy string) (string, error) { - return createUnsignedSourceVsaAllParams(repoUri, ref, commit, verifiedLevels, policy, VsaVerifierId, "PASSED") +func CreateUnsignedSourceVsa(branch *models.Branch, commit *models.Commit, verifiedLevels slsa.SourceVerifiedLevels, policy string) (string, error) { + return createUnsignedSourceVsaAllParams(branch, commit, verifiedLevels, policy, VsaVerifierId, "PASSED") } -func createUnsignedSourceVsaAllParams(repoUri, ref, commit string, verifiedLevels slsa.SourceVerifiedLevels, policy, verifiedId, result string) (string, error) { - resourceUri := fmt.Sprintf("git+%s", repoUri) +// createUnsignedSourceVsaAllParams generates a VSA +func createUnsignedSourceVsaAllParams(branch *models.Branch, commit *models.Commit, verifiedLevels slsa.SourceVerifiedLevels, policy, verifiedId, result string) (string, error) { + // The attestation records a VCS locator + resourceUri := fmt.Sprintf("git+%s", branch.Repository.GetHttpURL()) vsaPred := &vpb.VerificationSummary{ Verifier: &vpb.VerificationSummary_Verifier{ Id: verifiedId, @@ -46,13 +45,13 @@ func createUnsignedSourceVsaAllParams(repoUri, ref, commit string, verifiedLevel return "", err } - branchAnnotation := map[string]any{slsa.SourceRefsAnnotation: []any{ref}} + branchAnnotation := map[string]any{slsa.SourceRefsAnnotation: []any{branch.FullRef()}} annotationStruct, err := structpb.NewStruct(branchAnnotation) if err != nil { return "", fmt.Errorf("creating struct from map: %w", err) } sub := []*spb.ResourceDescriptor{{ - Digest: map[string]string{"gitCommit": commit}, + Digest: map[string]string{"gitCommit": commit.SHA}, Annotations: annotationStruct, }} @@ -75,105 +74,3 @@ func createUnsignedSourceVsaAllParams(repoUri, ref, commit string, verifiedLevel } return string(statement), nil } - -// Gets a VSA for the commit from git notes. -func GetVsa(ctx context.Context, ghc *ghcontrol.GitHubConnection, verifier Verifier, commit, ref string) (*spb.Statement, *vpb.VerificationSummary, error) { - notes, err := ghc.GetNotesForCommit(ctx, commit) - if err != nil { - return nil, nil, fmt.Errorf("fetching commit note: %w", err) - } - - if notes == "" { - Debugf("didn't find notes for commit %s", commit) - return nil, nil, nil - } - - return getVsaFromReader(NewBundleReader(bufio.NewReader(strings.NewReader(notes)), verifier), commit, ref, ghc.GetRepoUri()) -} - -func getVsaPred(statement *spb.Statement) (*vpb.VerificationSummary, error) { - predJson, err := protojson.Marshal(statement.GetPredicate()) - if err != nil { - return nil, err - } - - var predStruct vpb.VerificationSummary - // Using regular json.Unmarshal because this is just a regular struct. - err = protojson.Unmarshal(predJson, &predStruct) - if err != nil { - return nil, err - } - return &predStruct, nil -} - -func MatchesTypeCommitAndRef(predicateType, commit, targetRef string) StatementMatcher { - return func(statement *spb.Statement) bool { - if statement.GetPredicateType() != predicateType { - Debugf("statement predicate type (%s) doesn't match %s", statement.GetPredicateType(), predicateType) - return false - } - refs, err := GetSourceRefsForCommit(statement, commit) - if err != nil { - Debugf("statement \n%v\n does not match commit %s: %v", StatementToString(statement), commit, err) - return false - } - for _, ref := range refs { - if targetRef == ghcontrol.AnyReference || ref == targetRef { - Debugf("statement \n%v\n matches commit '%s' on ref '%s'", StatementToString(statement), commit, targetRef) - return true - } - } - Debugf("source_refs (%v) in VSA does not contain %s", refs, targetRef) - return false - } -} - -func getVsaFromReader(reader *BundleReader, commit, ref, repoUri string) (*spb.Statement, *vpb.VerificationSummary, error) { - // We want to return the first valid VSA. - // We should follow instructions from - // https://slsa.dev/spec/draft/verifying-source#how-to-verify-slsa-a-source-revision - - for { - stmt, err := reader.ReadStatement(MatchesTypeCommitAndRef(VsaPredicateType, commit, ref)) - if err != nil { - // Ignore errors, we want to check all the lines. - Debugf("error while processing line: %v", err) - continue - } - - if stmt == nil { - // No statements left. - break - } - - vsaPred, err := getVsaPred(stmt) - if err != nil { - return nil, nil, err - } - - // Is it the verifier ID we expect? - if vsaPred.GetVerifier().GetId() != VsaVerifierId { - Debugf("we do not accept Verifier.Id %s", vsaPred.GetVerifier().GetId()) - continue - } - - // Does repo match? - // remove git+http:// from resourceUri - cleanResourceUri := strings.TrimPrefix(vsaPred.GetResourceUri(), "git+") - if repoUri != "" && cleanResourceUri != repoUri { - Debugf("ResourceUri is %s but we want %s", cleanResourceUri, repoUri) - continue - } - - // Is the result PASSED? - if vsaPred.GetVerificationResult() != "PASSED" { - Debugf("verificationResult is %s but must be PASSED", vsaPred.GetVerificationResult()) - continue - } - - return stmt, vsaPred, nil - } - - Debugf("didn't find commit %s for ref %s", commit, ref) - return nil, nil, nil -} diff --git a/pkg/sourcetool/backends/attestation/notes/backend.go b/pkg/sourcetool/backends/attestation/notes/backend.go deleted file mode 100644 index 5caf0ec..0000000 --- a/pkg/sourcetool/backends/attestation/notes/backend.go +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors -// SPDX-License-Identifier: Apache-2.0 - -// Package notes implements an attestation storage backend that reads from -// git commit notes -package notes - -import ( - "context" - "fmt" - - vpb "github.com/in-toto/attestation/go/predicates/vsa/v1" - attestation "github.com/in-toto/attestation/go/v1" - - "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/auth" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" - "github.com/slsa-framework/source-tool/pkg/provenance" - "github.com/slsa-framework/source-tool/pkg/sourcetool/models" -) - -type Backend struct { - authenticator *auth.Authenticator -} - -func New() *Backend { - return &Backend{ - authenticator: auth.New(), - } -} - -// getGitHubConnection gets a github connector to the specified branch -func (b *Backend) getGitHubConnection(branch *models.Branch) (*ghcontrol.GitHubConnection, error) { - if branch.Repository == nil { - return nil, fmt.Errorf("branch does not have its repository set") - } - - client, err := b.authenticator.GetGitHubClient() - if err != nil { - return nil, fmt.Errorf("creating GitHub client: %w", err) - } - - repoOwner, repoName, err := branch.Repository.PathAsGitHubOwnerName() - if err != nil { - return nil, err - } - - return ghcontrol.NewGhConnectionWithClient(repoOwner, repoName, branch.FullRef(), client), nil -} - -// GetCommitVsa retrieves a VSA by looking into the specified commit notes -func (b *Backend) GetCommitVsa(ctx context.Context, branch *models.Branch, commit *models.Commit) (*attestation.Statement, *vpb.VerificationSummary, error) { - gcx, err := b.getGitHubConnection(branch) - if err != nil { - return nil, nil, err - } - statement, predicate, err := attest.GetVsa(ctx, gcx, attest.GetDefaultVerifier(), commit.SHA, branch.FullRef()) - if err != nil { - return nil, nil, fmt.Errorf("reading VSA: %w", err) - } - - return statement, predicate, nil -} - -// GetCommitProvenance gets the provenance attestation of a commit in a branch -func (b *Backend) GetCommitProvenance(ctx context.Context, branch *models.Branch, commit *models.Commit) (*attestation.Statement, *provenance.SourceProvenancePred, error) { - gcx, err := b.getGitHubConnection(branch) - if err != nil { - return nil, nil, err - } - - pa := attest.NewProvenanceAttestor(gcx, attest.GetDefaultVerifier()) - statement, predicate, err := pa.GetProvenance(ctx, commit.SHA, branch.FullRef()) - if err != nil { - return nil, nil, fmt.Errorf("reading provenance attestation: %w", err) - } - - return statement, predicate, nil -} From e2f7b324c8189db10809f001e99aa54b6feb768c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Sun, 8 Mar 2026 23:46:32 -0600 Subject: [PATCH 12/41] Update github backend to latest interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/sourcetool/backends/vcs/github/github.go | 25 +++++++++++++++++--- pkg/sourcetool/backends/vcs/github/manage.go | 8 +++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index f2eaa8e..5bd4a74 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -148,12 +148,15 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. // We need to manually check for PROVENANCE_AVAILABLE which is not // handled by ghcontrol - attestor := attest.NewProvenanceAttestor( - ghc, attest.GetDefaultVerifier(), + attestor, err := attest.NewAttester( + attest.WithBackend(b), attest.WithVerifier(attest.GetDefaultVerifier()), ) + if err != nil { + return nil, err + } // Fetch the attestation. If found, then add the control: - attestation, _, err := attestor.GetProvenance(ctx, commit.SHA, branch.FullRef()) + attestation, err := attestor.GetRevisionProvenance(ctx, branch, commit) if err != nil { return nil, fmt.Errorf("attempting to read provenance from commit %q: %w", commit.SHA, err) } @@ -356,3 +359,19 @@ func (b *Backend) getRecommendedAction(r *models.Repository, _ *models.Branch, c return nil } } + +// GetPreviousCommit takes a commit in +func (b *Backend) GetPreviousCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*models.Commit, error) { + ghx, err := b.getGitHubConnection(branch.Repository, branch.FullRef()) + if err != nil { + return nil, err + } + // TODO:IMPLEMENT + rawCommit, err := ghx.GetPriorCommit(ctx, commit.SHA) + if err != nil { + return nil, fmt.Errorf("fetching previous commit: %w", err) + } + return &models.Commit{ + SHA: rawCommit, + }, nil +} diff --git a/pkg/sourcetool/backends/vcs/github/manage.go b/pkg/sourcetool/backends/vcs/github/manage.go index 8f9e408..b91a34a 100644 --- a/pkg/sourcetool/backends/vcs/github/manage.go +++ b/pkg/sourcetool/backends/vcs/github/manage.go @@ -18,10 +18,10 @@ import ( ) const ( - ActionsOrg = "slsa-framework" - ActionsRepo = "source-actions" - workflowPath = ".github/workflows/compute_slsa_source.yaml" - workflowSource = "git+https://github.com/slsa-" + ActionsOrg = "slsa-framework" + ActionsRepo = "source-actions" + workflowPath = ".github/workflows/compute_slsa_source.yaml" + //workflowSource = "git+https://github.com/slsa-" // workflowCommitMessage will be used as the commit message and the PR title workflowCommitMessage = "Add SLSA Source Provenance Workflow" From 2107e70b7a8e124878ab378bf553de7676f3f73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Sun, 8 Mar 2026 23:47:53 -0600 Subject: [PATCH 13/41] Remove deprecated notes in tree collector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/policy/policy.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 549ecfe..4530bf1 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -27,7 +27,6 @@ import ( "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" - "github.com/slsa-framework/source-tool/pkg/sourcetool/backends/attestation/notes" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) @@ -537,10 +536,6 @@ type PolicyEvaluator struct { func NewPolicyEvaluator() *PolicyEvaluator { eval := &PolicyEvaluator{} - // TODO(puerco): Implement functional opts to reuse clients - if eval.reader == nil { - eval.reader = notes.New() - } if eval.authenticator == nil { eval.authenticator = auth.New() From 07dbe7670c5552be8cb780bb48815383acad1592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Sun, 8 Mar 2026 23:48:48 -0600 Subject: [PATCH 14/41] Merge controlset with git fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/slsa/slsa_types.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/slsa/slsa_types.go b/pkg/slsa/slsa_types.go index 4f255b6..d3369f0 100644 --- a/pkg/slsa/slsa_types.go +++ b/pkg/slsa/slsa_types.go @@ -106,9 +106,17 @@ func NewControlSet() *ControlSet { // ControlSet is a snapshot of the status of SLSA controls in a branch at // a point in time. type ControlSet struct { - RepoUri string - Branch string - Time time.Time + RepoUri string + Branch string + // The time we are observing the controls + Time time.Time + // The time the commit we're evaluating was pushed. + CommitPushTime time.Time + // The actor that pushed the commit. + ActorLogin string + // The type of activity that created the commit. + ActivityType string + // List of controls Controls []*Control } From f6fe256e4964307dfad6e727b0bb0b6f0f6c6d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Sun, 8 Mar 2026 23:49:16 -0600 Subject: [PATCH 15/41] Unify sourcetool API w/components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/sourcetool/implementation.go | 16 --- pkg/sourcetool/models/models.go | 9 ++ .../models/modelsfakes/fake_vcs_backend.go | 81 +++++++++++++++ pkg/sourcetool/options.go | 14 +++ pkg/sourcetool/options/options.go | 13 ++- .../fake_tool_implementation.go | 98 ++----------------- pkg/sourcetool/tool.go | 74 +++++++------- pkg/sourcetool/tool_test.go | 1 - 8 files changed, 154 insertions(+), 152 deletions(-) diff --git a/pkg/sourcetool/implementation.go b/pkg/sourcetool/implementation.go index d92bd52..9c7f763 100644 --- a/pkg/sourcetool/implementation.go +++ b/pkg/sourcetool/implementation.go @@ -20,8 +20,6 @@ import ( "github.com/slsa-framework/source-tool/pkg/repo" roptions "github.com/slsa-framework/source-tool/pkg/repo/options" "github.com/slsa-framework/source-tool/pkg/slsa" - "github.com/slsa-framework/source-tool/pkg/sourcetool/backends/attestation/notes" - ghbackend "github.com/slsa-framework/source-tool/pkg/sourcetool/backends/vcs/github" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" "github.com/slsa-framework/source-tool/pkg/sourcetool/options" ) @@ -35,8 +33,6 @@ type toolImplementation interface { CreatePolicyPR(*auth.Authenticator, *options.Options, *models.Repository, *policy.RepoPolicy) (*models.PullRequest, error) CheckForks(*options.Options) error SearchPullRequest(context.Context, *auth.Authenticator, *models.Repository, string) (*models.PullRequest, error) - GetVcsBackend(*models.Repository) (models.VcsBackend, error) - GetAttestationReader(*models.Repository) (models.AttestationStorageReader, error) GetBranchControls(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSet, error) GetBranchControlsAtCommit(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSet, error) ConfigureControls(models.VcsBackend, *models.Repository, []*models.Branch, []models.ControlConfiguration) error @@ -65,18 +61,6 @@ func (impl *defaultToolImplementation) GetBranchControlsAtCommit( return backend.GetBranchControlsAtCommit(ctx, branch, commit) } -// GetAttestationReader returns the att reader object -func (impl *defaultToolImplementation) GetAttestationReader(_ *models.Repository) (models.AttestationStorageReader, error) { - // We only have the notes backend for now - return notes.New(), nil -} - -// GetVcsBackend returns the VCS backend to handle the repository defined in the options -func (impl *defaultToolImplementation) GetVcsBackend(*models.Repository) (models.VcsBackend, error) { - // for now we only support github, so there - return ghbackend.New(), nil -} - // VerifyOptions checks options are in good shape to run // TODO(puerco): To be completed func (impl *defaultToolImplementation) VerifyOptionsForFullOnboard(a *auth.Authenticator, opts *options.Options) error { diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index f08c511..3a821b5 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -46,6 +46,7 @@ type VcsBackend interface { ConfigureControls(*Repository, []*Branch, []ControlConfiguration) error GetLatestCommit(context.Context, *Repository, *Branch) (*Commit, error) ControlPrecheck(*Repository, []*Branch, ControlConfiguration) (bool, string, ControlPreRemediationFn, error) + GetPreviousCommit(context.Context, *Branch, *Commit) (*Commit, error) } // ControlPreRemediation is a function returned by the VCS backends @@ -68,6 +69,14 @@ type Commit struct { Message string } +func (c *Commit) ToResourceDescriptor() *attestation.ResourceDescriptor { + return &attestation.ResourceDescriptor{ + Digest: map[string]string{ + "sha1": c.SHA, "gitCommit": c.SHA, + }, + } +} + type Branch struct { Name string Repository *Repository diff --git a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go index 0daf19b..ed678ac 100644 --- a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go +++ b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go @@ -98,6 +98,21 @@ type FakeVcsBackend struct { result1 *models.Commit result2 error } + GetPreviousCommitStub func(context.Context, *models.Branch, *models.Commit) (*models.Commit, error) + getPreviousCommitMutex sync.RWMutex + getPreviousCommitArgsForCall []struct { + arg1 context.Context + arg2 *models.Branch + arg3 *models.Commit + } + getPreviousCommitReturns struct { + result1 *models.Commit + result2 error + } + getPreviousCommitReturnsOnCall map[int]struct { + result1 *models.Commit + result2 error + } GetTagControlsStub func(context.Context, *models.Tag) (*slsa.ControlSet, error) getTagControlsMutex sync.RWMutex getTagControlsArgsForCall []struct { @@ -525,6 +540,72 @@ func (fake *FakeVcsBackend) GetLatestCommitReturnsOnCall(i int, result1 *models. }{result1, result2} } +func (fake *FakeVcsBackend) GetPreviousCommit(arg1 context.Context, arg2 *models.Branch, arg3 *models.Commit) (*models.Commit, error) { + fake.getPreviousCommitMutex.Lock() + ret, specificReturn := fake.getPreviousCommitReturnsOnCall[len(fake.getPreviousCommitArgsForCall)] + fake.getPreviousCommitArgsForCall = append(fake.getPreviousCommitArgsForCall, struct { + arg1 context.Context + arg2 *models.Branch + arg3 *models.Commit + }{arg1, arg2, arg3}) + stub := fake.GetPreviousCommitStub + fakeReturns := fake.getPreviousCommitReturns + fake.recordInvocation("GetPreviousCommit", []interface{}{arg1, arg2, arg3}) + fake.getPreviousCommitMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVcsBackend) GetPreviousCommitCallCount() int { + fake.getPreviousCommitMutex.RLock() + defer fake.getPreviousCommitMutex.RUnlock() + return len(fake.getPreviousCommitArgsForCall) +} + +func (fake *FakeVcsBackend) GetPreviousCommitCalls(stub func(context.Context, *models.Branch, *models.Commit) (*models.Commit, error)) { + fake.getPreviousCommitMutex.Lock() + defer fake.getPreviousCommitMutex.Unlock() + fake.GetPreviousCommitStub = stub +} + +func (fake *FakeVcsBackend) GetPreviousCommitArgsForCall(i int) (context.Context, *models.Branch, *models.Commit) { + fake.getPreviousCommitMutex.RLock() + defer fake.getPreviousCommitMutex.RUnlock() + argsForCall := fake.getPreviousCommitArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVcsBackend) GetPreviousCommitReturns(result1 *models.Commit, result2 error) { + fake.getPreviousCommitMutex.Lock() + defer fake.getPreviousCommitMutex.Unlock() + fake.GetPreviousCommitStub = nil + fake.getPreviousCommitReturns = struct { + result1 *models.Commit + result2 error + }{result1, result2} +} + +func (fake *FakeVcsBackend) GetPreviousCommitReturnsOnCall(i int, result1 *models.Commit, result2 error) { + fake.getPreviousCommitMutex.Lock() + defer fake.getPreviousCommitMutex.Unlock() + fake.GetPreviousCommitStub = nil + if fake.getPreviousCommitReturnsOnCall == nil { + fake.getPreviousCommitReturnsOnCall = make(map[int]struct { + result1 *models.Commit + result2 error + }) + } + fake.getPreviousCommitReturnsOnCall[i] = struct { + result1 *models.Commit + result2 error + }{result1, result2} +} + func (fake *FakeVcsBackend) GetTagControls(arg1 context.Context, arg2 *models.Tag) (*slsa.ControlSet, error) { fake.getTagControlsMutex.Lock() ret, specificReturn := fake.getTagControlsReturnsOnCall[len(fake.getTagControlsArgsForCall)] diff --git a/pkg/sourcetool/options.go b/pkg/sourcetool/options.go index ff5a09a..7a9a136 100644 --- a/pkg/sourcetool/options.go +++ b/pkg/sourcetool/options.go @@ -11,6 +11,20 @@ import ( type ConfigFn func(*Tool) error +func WithGithubCollector(yesno bool) ConfigFn { + return func(t *Tool) error { + t.Options.InitGHCollector = yesno + return nil + } +} + +func WithNotesCollector(yesno bool) ConfigFn { + return func(t *Tool) error { + t.Options.InitNotesCollector = yesno + return nil + } +} + func WithAuthenticator(a *auth.Authenticator) ConfigFn { return func(t *Tool) error { if a == nil { diff --git a/pkg/sourcetool/options/options.go b/pkg/sourcetool/options/options.go index 26e1617..b311f54 100644 --- a/pkg/sourcetool/options/options.go +++ b/pkg/sourcetool/options/options.go @@ -20,11 +20,18 @@ type Options struct { // PolicyRepo is the repository where the policies are stored PolicyRepo string + + // Initialize GitHub attestations store collector + InitGHCollector bool + + // Initialize Dynamic notes collector + InitNotesCollector bool } // DefaultOptions holds the default options the tool initializes with var Default = Options{ - PolicyRepo: fmt.Sprintf("%s/%s", policy.SourcePolicyRepoOwner, policy.SourcePolicyRepo), - UseSSH: true, - CreatePolicyPR: true, + PolicyRepo: fmt.Sprintf("%s/%s", policy.SourcePolicyRepoOwner, policy.SourcePolicyRepo), + UseSSH: true, + CreatePolicyPR: true, + InitNotesCollector: true, } diff --git a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go index 1b62c99..dc766c7 100644 --- a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go +++ b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go @@ -79,19 +79,6 @@ type FakeToolImplementation struct { createRepositoryForkReturnsOnCall map[int]struct { result1 error } - GetAttestationReaderStub func(*models.Repository) (models.AttestationStorageReader, error) - getAttestationReaderMutex sync.RWMutex - getAttestationReaderArgsForCall []struct { - arg1 *models.Repository - } - getAttestationReaderReturns struct { - result1 models.AttestationStorageReader - result2 error - } - getAttestationReaderReturnsOnCall map[int]struct { - result1 models.AttestationStorageReader - result2 error - } GetBranchControlsStub func(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSet, error) getBranchControlsMutex sync.RWMutex getBranchControlsArgsForCall []struct { @@ -139,10 +126,9 @@ type FakeToolImplementation struct { result1 *slsa.Control result2 error } - GetVcsBackendStub func(*models.Repository) (models.VcsBackend, error) + GetVcsBackendStub func() (models.VcsBackend, error) getVcsBackendMutex sync.RWMutex getVcsBackendArgsForCall []struct { - arg1 *models.Repository } getVcsBackendReturns struct { result1 models.VcsBackend @@ -511,70 +497,6 @@ func (fake *FakeToolImplementation) CreateRepositoryForkReturnsOnCall(i int, res }{result1} } -func (fake *FakeToolImplementation) GetAttestationReader(arg1 *models.Repository) (models.AttestationStorageReader, error) { - fake.getAttestationReaderMutex.Lock() - ret, specificReturn := fake.getAttestationReaderReturnsOnCall[len(fake.getAttestationReaderArgsForCall)] - fake.getAttestationReaderArgsForCall = append(fake.getAttestationReaderArgsForCall, struct { - arg1 *models.Repository - }{arg1}) - stub := fake.GetAttestationReaderStub - fakeReturns := fake.getAttestationReaderReturns - fake.recordInvocation("GetAttestationReader", []interface{}{arg1}) - fake.getAttestationReaderMutex.Unlock() - if stub != nil { - return stub(arg1) - } - if specificReturn { - return ret.result1, ret.result2 - } - return fakeReturns.result1, fakeReturns.result2 -} - -func (fake *FakeToolImplementation) GetAttestationReaderCallCount() int { - fake.getAttestationReaderMutex.RLock() - defer fake.getAttestationReaderMutex.RUnlock() - return len(fake.getAttestationReaderArgsForCall) -} - -func (fake *FakeToolImplementation) GetAttestationReaderCalls(stub func(*models.Repository) (models.AttestationStorageReader, error)) { - fake.getAttestationReaderMutex.Lock() - defer fake.getAttestationReaderMutex.Unlock() - fake.GetAttestationReaderStub = stub -} - -func (fake *FakeToolImplementation) GetAttestationReaderArgsForCall(i int) *models.Repository { - fake.getAttestationReaderMutex.RLock() - defer fake.getAttestationReaderMutex.RUnlock() - argsForCall := fake.getAttestationReaderArgsForCall[i] - return argsForCall.arg1 -} - -func (fake *FakeToolImplementation) GetAttestationReaderReturns(result1 models.AttestationStorageReader, result2 error) { - fake.getAttestationReaderMutex.Lock() - defer fake.getAttestationReaderMutex.Unlock() - fake.GetAttestationReaderStub = nil - fake.getAttestationReaderReturns = struct { - result1 models.AttestationStorageReader - result2 error - }{result1, result2} -} - -func (fake *FakeToolImplementation) GetAttestationReaderReturnsOnCall(i int, result1 models.AttestationStorageReader, result2 error) { - fake.getAttestationReaderMutex.Lock() - defer fake.getAttestationReaderMutex.Unlock() - fake.GetAttestationReaderStub = nil - if fake.getAttestationReaderReturnsOnCall == nil { - fake.getAttestationReaderReturnsOnCall = make(map[int]struct { - result1 models.AttestationStorageReader - result2 error - }) - } - fake.getAttestationReaderReturnsOnCall[i] = struct { - result1 models.AttestationStorageReader - result2 error - }{result1, result2} -} - func (fake *FakeToolImplementation) GetBranchControls(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Branch) (*slsa.ControlSet, error) { fake.getBranchControlsMutex.Lock() ret, specificReturn := fake.getBranchControlsReturnsOnCall[len(fake.getBranchControlsArgsForCall)] @@ -775,18 +697,17 @@ func (fake *FakeToolImplementation) GetPolicyStatusReturnsOnCall(i int, result1 }{result1, result2} } -func (fake *FakeToolImplementation) GetVcsBackend(arg1 *models.Repository) (models.VcsBackend, error) { +func (fake *FakeToolImplementation) GetVcsBackend() (models.VcsBackend, error) { fake.getVcsBackendMutex.Lock() ret, specificReturn := fake.getVcsBackendReturnsOnCall[len(fake.getVcsBackendArgsForCall)] fake.getVcsBackendArgsForCall = append(fake.getVcsBackendArgsForCall, struct { - arg1 *models.Repository - }{arg1}) + }{}) stub := fake.GetVcsBackendStub fakeReturns := fake.getVcsBackendReturns - fake.recordInvocation("GetVcsBackend", []interface{}{arg1}) + fake.recordInvocation("GetVcsBackend", []interface{}{}) fake.getVcsBackendMutex.Unlock() if stub != nil { - return stub(arg1) + return stub() } if specificReturn { return ret.result1, ret.result2 @@ -800,19 +721,12 @@ func (fake *FakeToolImplementation) GetVcsBackendCallCount() int { return len(fake.getVcsBackendArgsForCall) } -func (fake *FakeToolImplementation) GetVcsBackendCalls(stub func(*models.Repository) (models.VcsBackend, error)) { +func (fake *FakeToolImplementation) GetVcsBackendCalls(stub func() (models.VcsBackend, error)) { fake.getVcsBackendMutex.Lock() defer fake.getVcsBackendMutex.Unlock() fake.GetVcsBackendStub = stub } -func (fake *FakeToolImplementation) GetVcsBackendArgsForCall(i int) *models.Repository { - fake.getVcsBackendMutex.RLock() - defer fake.getVcsBackendMutex.RUnlock() - argsForCall := fake.getVcsBackendArgsForCall[i] - return argsForCall.arg1 -} - func (fake *FakeToolImplementation) GetVcsBackendReturns(result1 models.VcsBackend, result2 error) { fake.getVcsBackendMutex.Lock() defer fake.getVcsBackendMutex.Unlock() diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index 8464bb1..f6c76be 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -15,9 +15,11 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" + "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/auth" "github.com/slsa-framework/source-tool/pkg/policy" "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/backends/vcs/github" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" "github.com/slsa-framework/source-tool/pkg/sourcetool/options" ) @@ -30,6 +32,7 @@ var ControlConfigurations = []models.ControlConfiguration{ func New(funcs ...ConfigFn) (*Tool, error) { t := &Tool{ Options: options.Default, + backend: github.New(), // For now this is fixed to the github backend impl: &defaultToolImplementation{}, } @@ -39,6 +42,19 @@ func New(funcs ...ConfigFn) (*Tool, error) { } } + // Create the tool's attester + attester, err := attest.NewAttester( + attest.WithVerifier(attest.GetDefaultVerifier()), + attest.WithBackend(t.backend), + attest.WithGithubCollector(t.Options.InitGHCollector), + attest.WithNotesCollector(t.Options.InitNotesCollector), + ) + if err != nil { + return nil, fmt.Errorf("creating attester: %w", err) + } + + t.attester = attester + return t, nil } @@ -47,6 +63,8 @@ func New(funcs ...ConfigFn) (*Tool, error) { // we want to slowly move it to public function under this struct. type Tool struct { Authenticator *auth.Authenticator + attester *attest.Attester + backend models.VcsBackend Options options.Options impl toolImplementation } @@ -57,14 +75,9 @@ func (t *Tool) GetBranchControls(ctx context.Context, branch *models.Branch) (*s return nil, fmt.Errorf("repositoryu not specified in branch") } - backend, err := t.impl.GetVcsBackend(branch.Repository) - if err != nil { - return nil, fmt.Errorf("getting VCS backend: %w", err) - } - // Get the control status in the branch. Backends are expected to // return the full SLSA Source control catalog - controls, err := t.impl.GetBranchControls(ctx, backend, branch) + controls, err := t.impl.GetBranchControls(ctx, t.backend, branch) if err != nil { return nil, fmt.Errorf("getting branch controls: %w", err) } @@ -86,14 +99,9 @@ func (t *Tool) GetBranchControlsAtCommit(ctx context.Context, branch *models.Bra return nil, fmt.Errorf("repositoryu not specified in branch") } - backend, err := t.impl.GetVcsBackend(branch.Repository) - if err != nil { - return nil, fmt.Errorf("getting VCS backend: %w", err) - } - // Get the control status in the branch. Backends are expected to // return the full SLSA Source control catalog - controls, err := t.impl.GetBranchControlsAtCommit(ctx, backend, branch, commit) + controls, err := t.impl.GetBranchControlsAtCommit(ctx, t.backend, branch, commit) if err != nil { return nil, fmt.Errorf("getting branch controls: %w", err) } @@ -112,16 +120,11 @@ func (t *Tool) GetBranchControlsAtCommit(ctx context.Context, branch *models.Bra // OnboardRepository configures a repository to set up the required controls // to meet SLSA Source L3. func (t *Tool) OnboardRepository(ctx context.Context, repo *models.Repository, branches []*models.Branch) error { - backend, err := t.impl.GetVcsBackend(repo) - if err != nil { - return fmt.Errorf("getting VCS backend: %w", err) - } - if err := t.impl.VerifyOptionsForFullOnboard(t.Authenticator, &t.Options); err != nil { return fmt.Errorf("verifying options: %w", err) } - if err := backend.ConfigureControls( + if err := t.backend.ConfigureControls( repo, branches, []models.ControlConfiguration{ models.CONFIG_BRANCH_RULES, models.CONFIG_GEN_PROVENANCE, models.CONFIG_TAG_RULES, }, @@ -134,11 +137,6 @@ func (t *Tool) OnboardRepository(ctx context.Context, repo *models.Repository, b // ConfigureControls sets up a control in the repo func (t *Tool) ConfigureControls(ctx context.Context, repo *models.Repository, branches []*models.Branch, configs []models.ControlConfiguration) error { - backend, err := t.impl.GetVcsBackend(repo) - if err != nil { - return fmt.Errorf("getting VCS backend: %w", err) - } - // The policy configuration is not handled by the backend if slices.Contains(configs, models.CONFIG_POLICY) { if err := t.impl.CheckPolicyFork(&t.Options); err != nil { @@ -156,17 +154,12 @@ func (t *Tool) ConfigureControls(ctx context.Context, repo *models.Repository, b } } - return t.impl.ConfigureControls(backend, repo, branches, configs) + return t.impl.ConfigureControls(t.backend, repo, branches, configs) } // ControlConfigurationDescr returns a description of the controls func (t *Tool) ControlConfigurationDescr(branch *models.Branch, config models.ControlConfiguration) string { - backend, err := t.impl.GetVcsBackend(branch.Repository) - if err != nil { - return "" - } - - return backend.ControlConfigurationDescr(branch, config) + return t.backend.ControlConfigurationDescr(branch, config) } func (t *Tool) FindPolicyPR(ctx context.Context, repo *models.Repository) (*models.PullRequest, error) { @@ -213,12 +206,8 @@ func (t *Tool) CreateBranchPolicy(ctx context.Context, r *models.Repository, bra if branches == nil { return nil, errors.New("no branches defined") } - backend, err := t.impl.GetVcsBackend(r) - if err != nil { - return nil, fmt.Errorf("getting backend: %w", err) - } - controls, err := t.impl.GetBranchControls(ctx, backend, branches[0]) + controls, err := t.impl.GetBranchControls(ctx, t.backend, branches[0]) if err != nil { return nil, fmt.Errorf("getting branch controls: %w", err) } @@ -313,10 +302,15 @@ func (t *Tool) CreatePolicyRepoFork(ctx context.Context) error { func (t *Tool) ControlPrecheck( _ context.Context, r *models.Repository, branches []*models.Branch, config models.ControlConfiguration, ) (ok bool, remediationMessage string, remediateFn models.ControlPreRemediationFn, err error) { - backend, err := t.impl.GetVcsBackend(r) - if err != nil { - return false, "", nil, fmt.Errorf("getting VCS backend: %w", err) - } + return t.backend.ControlPrecheck(r, branches, config) +} + +// Attester returns an attester object with the tool configuration +func (t *Tool) Attester() *attest.Attester { + return t.attester +} - return backend.ControlPrecheck(r, branches, config) +// GetPreviousCommit returns the previous commit of the passed commit +func (t *Tool) GetPreviousCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*models.Commit, error) { + return t.backend.GetPreviousCommit(ctx, branch, commit) } diff --git a/pkg/sourcetool/tool_test.go b/pkg/sourcetool/tool_test.go index 2e23ec5..1784f6d 100644 --- a/pkg/sourcetool/tool_test.go +++ b/pkg/sourcetool/tool_test.go @@ -98,7 +98,6 @@ func TestConfigureControls(t *testing.T) { t.Helper() i := &sourcetoolfakes.FakeToolImplementation{} i.GetVcsBackendReturns(&modelsfakes.FakeVcsBackend{}, nil) - i.GetAttestationReaderReturns(&modelsfakes.FakeAttestationStorageReader{}, nil) i.CreatePolicyPRReturns(nil, syntErr) return i }, From 38f700e0e9a627d0948ce0c58e195646184616ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Sun, 8 Mar 2026 23:49:46 -0600 Subject: [PATCH 16/41] Update audit to new backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/audit/audit.go | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/pkg/audit/audit.go b/pkg/audit/audit.go index 28ac99d..230f929 100644 --- a/pkg/audit/audit.go +++ b/pkg/audit/audit.go @@ -7,19 +7,22 @@ import ( "context" "fmt" "iter" + "os" vpb "github.com/in-toto/attestation/go/predicates/vsa/v1" "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/provenance" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type Auditor struct { ghc *ghcontrol.GitHubConnection // TODO: This should probably be turned into a pointer. verifier attest.Verifier - pa *attest.ProvenanceAttestor + attester *attest.Attester + backend models.VcsBackend } type AuditCommitResult struct { @@ -46,30 +49,30 @@ func (ar *AuditCommitResult) IsGood() bool { return good } -func NewAuditor(ghc *ghcontrol.GitHubConnection, pa *attest.ProvenanceAttestor, verifier attest.Verifier) *Auditor { +func NewAuditor(ghc *ghcontrol.GitHubConnection, pa *attest.Attester, verifier attest.Verifier) *Auditor { return &Auditor{ ghc: ghc, verifier: verifier, - pa: pa, + attester: pa, } } -func (a *Auditor) AuditCommit(ctx context.Context, commit string) (ar *AuditCommitResult, err error) { - ar = &AuditCommitResult{Commit: commit} +func (a *Auditor) AuditCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (ar *AuditCommitResult, err error) { + ar = &AuditCommitResult{Commit: commit.SHA} - _, vsa, err := attest.GetVsa(ctx, a.ghc, a.verifier, commit, a.ghc.GetFullRef()) + _, vsa, err := a.attester.GetRevisionVSA(ctx, branch, commit) if err != nil { return nil, fmt.Errorf("getting vsa for revision %s: %w", commit, err) } ar.VsaPred = vsa - _, prov, err := a.pa.GetProvenance(ctx, commit, a.ghc.GetFullRef()) + prov, err := a.attester.GetRevisionProvenance(ctx, branch, commit) if err != nil { return nil, fmt.Errorf("getting prov for revision %s: %w", commit, err) } ar.ProvPred = prov - ghPrior, err := a.ghc.GetPriorCommit(ctx, commit) + ghPrior, err := a.ghc.GetPriorCommit(ctx, commit.SHA) if err != nil { return nil, fmt.Errorf("could not get prior commit for revision %s: %w", commit, err) } @@ -80,7 +83,8 @@ func (a *Auditor) AuditCommit(ctx context.Context, commit string) (ar *AuditComm // If there's no provenance, let's check the controls to see how they're looking. // It could be that provenance generation failed, but the controls were still // in place. - controlStatus, err = a.ghc.GetBranchControlsAtCommit(ctx, commit, a.ghc.GetFullRef()) + // TODO: (use backend method here) + controlStatus, err = a.ghc.GetBranchControlsAtCommit(ctx, commit.SHA, a.ghc.GetFullRef()) if err != nil { // Let's still return ar so they can continue if they want. return ar, fmt.Errorf("could not get controls for %s on %s: %w", commit, a.ghc.GetFullRef(), err) @@ -91,8 +95,12 @@ func (a *Auditor) AuditCommit(ctx context.Context, commit string) (ar *AuditComm return ar, nil } -func (a *Auditor) AuditBranch(ctx context.Context, branch string) iter.Seq2[*AuditCommitResult, error] { - latestCommit, err := a.ghc.GetLatestCommit(ctx, branch) +func (a *Auditor) AuditBranch(ctx context.Context, branch *models.Branch) iter.Seq2[*AuditCommitResult, error] { + latestCommit, err := a.backend.GetLatestCommit(ctx, branch.Repository, branch) + if err != nil { + fmt.Fprintf(os.Stderr, "error fetching latest commit: %v", err) + return nil + } return func(yield func(*AuditCommitResult, error) bool) { if err != nil { @@ -100,12 +108,12 @@ func (a *Auditor) AuditBranch(ctx context.Context, branch string) iter.Seq2[*Aud return } nextCommit := latestCommit - for ok := true; ok; ok = (nextCommit != "") { - ar, err := a.AuditCommit(ctx, nextCommit) + for ok := true; ok; ok = (nextCommit.SHA != "") { + ar, err := a.AuditCommit(ctx, branch, nextCommit) if !yield(ar, err) { return } - nextCommit = ar.GhPriorCommit + nextCommit = &models.Commit{SHA: ar.GhPriorCommit} } } } From 15c2bc7ed96e53ca31ef4356b29bebab739736e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Sun, 8 Mar 2026 23:50:32 -0600 Subject: [PATCH 17/41] Update commands to new source tool API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/audit.go | 185 +++++++++++++------------- internal/cmd/checklevel.go | 6 +- internal/cmd/checklevelprov.go | 233 +++++++++++++++++---------------- internal/cmd/checktag.go | 144 +++++++++++--------- internal/cmd/options.go | 6 + internal/cmd/prov.go | 56 +++++--- internal/cmd/root.go | 21 ++- internal/cmd/verifycommit.go | 99 +++++++------- 8 files changed, 400 insertions(+), 350 deletions(-) diff --git a/internal/cmd/audit.go b/internal/cmd/audit.go index 6456fe1..8613c21 100644 --- a/internal/cmd/audit.go +++ b/internal/cmd/audit.go @@ -4,7 +4,6 @@ package cmd import ( - "context" "errors" "fmt" @@ -13,6 +12,7 @@ import ( "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/audit" "github.com/slsa-framework/source-tool/pkg/ghcontrol" + "github.com/slsa-framework/source-tool/pkg/sourcetool" ) type AuditMode int @@ -154,7 +154,101 @@ Future: if err := opts.Validate(); err != nil { return err } - return doAudit(opts) + + authenticator, err := CheckAuth() + if err != nil { + return err + } + + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + ) + if err != nil { + return err + } + + ghc := ghcontrol.NewGhConnection(opts.owner, opts.repository, ghcontrol.BranchToFullRef(opts.branch)).WithAuthToken(githubToken) + + auditor := audit.NewAuditor(ghc, srctool.Attester(), attest.GetDefaultVerifier()) + + latestCommit, err := ghc.GetLatestCommit(cmd.Context(), opts.branch) + if err != nil { + return fmt.Errorf("could not get latest commit for %s", opts.branch) + } + + // Initialize JSON result structure if needed + var jsonResult *AuditResultJSON + if opts.outputFormatIsJSON() { + jsonResult = &AuditResultJSON{ + Owner: opts.owner, + Repository: opts.repository, + Branch: opts.branch, + LatestCommit: latestCommit, + CommitResults: []AuditCommitResultJSON{}, + } + } else { + // Print header for text output + opts.writeTextf("Auditing branch %s starting from revision %s\n", opts.branch, latestCommit) + } + + // Single loop for both JSON and text output + count := 0 + passed := 0 + failed := 0 + + for ar, err := range auditor.AuditBranch(cmd.Context(), opts.GetBranch()) { + if ar == nil { + return err + } + + // Process result based on output format + if opts.outputFormatIsJSON() { + commitResult := convertAuditResultToJSON(ghc, ar, opts.auditMode) + if err != nil { + commitResult.Error = err.Error() + } + if commitResult.Status == statusPassed { + passed++ + } else { + failed++ + } + jsonResult.CommitResults = append(jsonResult.CommitResults, commitResult) + } else { + // Text output + if err != nil { + opts.writeTextf("\terror: %v\n", err) + } + printResult(ghc, ar, opts.auditMode) + } + + // Check for early termination conditions + if opts.endingCommit != "" && opts.endingCommit == ar.Commit { + if !opts.outputFormatIsJSON() { + opts.writeTextf("Found ending commit %s\n", opts.endingCommit) + } + break + } + if opts.auditDepth > 0 && count >= opts.auditDepth { + if !opts.outputFormatIsJSON() { + opts.writeTextf("Reached depth limit %d\n", opts.auditDepth) + } + break + } + count++ + } + + // Write JSON output if needed + if opts.outputFormatIsJSON() { + jsonResult.Summary = &AuditSummary{ + TotalCommits: len(jsonResult.CommitResults), + PassedCommits: passed, + FailedCommits: failed, + } + return opts.writeJSON(jsonResult) + } + + return nil }, } opts.AddFlags(auditCmd) @@ -230,90 +324,3 @@ func convertAuditResultToJSON(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCo return result } - -func doAudit(auditArgs *auditOpts) error { - ghc := ghcontrol.NewGhConnection(auditArgs.owner, auditArgs.repository, ghcontrol.BranchToFullRef(auditArgs.branch)).WithAuthToken(githubToken) - ctx := context.Background() - verifier := getVerifier(&auditArgs.verifierOptions) - pa := attest.NewProvenanceAttestor(ghc, verifier) - - auditor := audit.NewAuditor(ghc, pa, verifier) - - latestCommit, err := ghc.GetLatestCommit(ctx, auditArgs.branch) - if err != nil { - return fmt.Errorf("could not get latest commit for %s", auditArgs.branch) - } - - // Initialize JSON result structure if needed - var jsonResult *AuditResultJSON - if auditArgs.outputFormatIsJSON() { - jsonResult = &AuditResultJSON{ - Owner: auditArgs.owner, - Repository: auditArgs.repository, - Branch: auditArgs.branch, - LatestCommit: latestCommit, - CommitResults: []AuditCommitResultJSON{}, - } - } else { - // Print header for text output - auditArgs.writeTextf("Auditing branch %s starting from revision %s\n", auditArgs.branch, latestCommit) - } - - // Single loop for both JSON and text output - count := 0 - passed := 0 - failed := 0 - - for ar, err := range auditor.AuditBranch(ctx, auditArgs.branch) { - if ar == nil { - return err - } - - // Process result based on output format - if auditArgs.outputFormatIsJSON() { - commitResult := convertAuditResultToJSON(ghc, ar, auditArgs.auditMode) - if err != nil { - commitResult.Error = err.Error() - } - if commitResult.Status == statusPassed { - passed++ - } else { - failed++ - } - jsonResult.CommitResults = append(jsonResult.CommitResults, commitResult) - } else { - // Text output - if err != nil { - auditArgs.writeTextf("\terror: %v\n", err) - } - printResult(ghc, ar, auditArgs.auditMode) - } - - // Check for early termination conditions - if auditArgs.endingCommit != "" && auditArgs.endingCommit == ar.Commit { - if !auditArgs.outputFormatIsJSON() { - auditArgs.writeTextf("Found ending commit %s\n", auditArgs.endingCommit) - } - break - } - if auditArgs.auditDepth > 0 && count >= auditArgs.auditDepth { - if !auditArgs.outputFormatIsJSON() { - auditArgs.writeTextf("Reached depth limit %d\n", auditArgs.auditDepth) - } - break - } - count++ - } - - // Write JSON output if needed - if auditArgs.outputFormatIsJSON() { - jsonResult.Summary = &AuditSummary{ - TotalCommits: len(jsonResult.CommitResults), - PassedCommits: passed, - FailedCommits: failed, - } - return auditArgs.writeJSON(jsonResult) - } - - return nil -} diff --git a/internal/cmd/checklevel.go b/internal/cmd/checklevel.go index 7cd2c5c..6e5b023 100644 --- a/internal/cmd/checklevel.go +++ b/internal/cmd/checklevel.go @@ -104,11 +104,7 @@ This is meant to be run within the corresponding GitHub Actions workflow.`, } unsignedVsa, err := attest.CreateUnsignedSourceVsa( - opts.GetRepository().GetHttpURL(), - opts.GetBranch().FullRef(), - opts.commit, - verifiedLevels, - policyPath, + opts.GetBranch(), opts.GetCommit(), verifiedLevels, policyPath, ) if err != nil { return err diff --git a/internal/cmd/checklevelprov.go b/internal/cmd/checklevelprov.go index 0408ccc..8bcf4c9 100644 --- a/internal/cmd/checklevelprov.go +++ b/internal/cmd/checklevelprov.go @@ -4,7 +4,6 @@ package cmd import ( - "context" "errors" "fmt" "log" @@ -21,9 +20,8 @@ import ( "google.golang.org/protobuf/encoding/protojson" "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/auth" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/policy" + "github.com/slsa-framework/source-tool/pkg/sourcetool" ) type pushOptions struct { @@ -184,130 +182,141 @@ and pushed to its remote (--push=note). return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return doCheckLevelProv(opts) - }, - } - - opts.AddFlags(checklevelprovCmd) - parentCmd.AddCommand(checklevelprovCmd) -} - -func doCheckLevelProv(checkLevelProvArgs *checkLevelProvOpts) error { - t := githubToken - var err error - if t == "" { - t, err = auth.New().ReadToken() - if err != nil { - return err - } - } - ghconnection := ghcontrol.NewGhConnection(checkLevelProvArgs.owner, checkLevelProvArgs.repository, ghcontrol.BranchToFullRef(checkLevelProvArgs.branch)).WithAuthToken(t) - ghconnection.Options.AllowMergeCommits = checkLevelProvArgs.allowMergeCommits - ctx := context.Background() - - prevCommit := checkLevelProvArgs.prevCommit - if prevCommit == "" { - prevCommit, err = ghconnection.GetPriorCommit(ctx, checkLevelProvArgs.commit) - if err != nil { - return err - } - } - - pa := attest.NewProvenanceAttestor(ghconnection, getVerifier(&checkLevelProvArgs.verifierOptions)) - prov, err := pa.CreateSourceProvenance(ctx, checkLevelProvArgs.prevBundlePath, checkLevelProvArgs.commit, prevCommit, ghconnection.GetFullRef()) - if err != nil { - return err - } + authenticator, err := CheckAuth() + if err != nil { + return err + } - // check p against policy - pe := policy.NewPolicyEvaluator() - pe.UseLocalPolicy = checkLevelProvArgs.useLocalPolicy - verifiedLevels, policyPath, err := pe.EvaluateSourceProv(ctx, checkLevelProvArgs.GetRepository(), checkLevelProvArgs.GetBranch(), prov) - if err != nil { - return err - } + t, err := authenticator.ReadToken() + if err != nil { + return err + } - // create vsa - unsignedVsa, err := attest.CreateUnsignedSourceVsa(ghconnection.GetRepoUri(), ghconnection.GetFullRef(), checkLevelProvArgs.commit, verifiedLevels, policyPath) - if err != nil { - return err - } + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + ) + if err != nil { + return err + } - unsignedProv, err := protojson.Marshal(prov) - if err != nil { - return err - } + // TODO: Esto donde lo metemos + // ghconnection.Options.AllowMergeCommits = opts.allowMergeCommits + + // var prevCommit *models.Commit + // if opts.prevCommit != "" { + // prevCommit = &models.Commit{SHA: opts.prevCommit} + // } else { + // prevCommit, err = srctool.GetPreviousCommit(cmd.Context(), opts.GetBranch(), opts.GetCommit()) + // if err != nil { + // return err + // } + // } + + // Create the provenance attestation + prov, err := srctool.Attester().CreateSourceProvenance( + cmd.Context(), opts.GetBranch(), opts.GetCommit(), + ) + if err != nil { + return err + } - // Store both the unsigned provenance and vsa - switch { - case checkLevelProvArgs.outputUnsignedBundle != "": - f, err := os.OpenFile(checkLevelProvArgs.outputUnsignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec - if err != nil { - return err - } - defer f.Close() //nolint:errcheck + // check p against policy + pe := policy.NewPolicyEvaluator() + pe.UseLocalPolicy = opts.useLocalPolicy + verifiedLevels, policyPath, err := pe.EvaluateSourceProv(cmd.Context(), opts.GetRepository(), opts.GetBranch(), prov) + if err != nil { + return err + } - if _, err := f.WriteString(string(unsignedProv) + "\n" + unsignedVsa + "\n"); err != nil { - return fmt.Errorf("writing signed bundle: %w", err) - } - case checkLevelProvArgs.outputSignedBundle != "" || len(checkLevelProvArgs.pushLocation) > 0: - var f *os.File - if checkLevelProvArgs.outputSignedBundle != "" { - f, err = os.OpenFile(checkLevelProvArgs.outputSignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec + // create vsa + unsignedVsa, err := attest.CreateUnsignedSourceVsa( + opts.GetBranch(), opts.GetCommit(), verifiedLevels, policyPath, + ) if err != nil { return err } - } - defer func() { - if f != nil { - f.Close() //nolint:errcheck,gosec + + unsignedProv, err := protojson.Marshal(prov) + if err != nil { + return err } - }() - signedProv, err := attest.Sign(string(unsignedProv)) - if err != nil { - return err - } + // Store both the unsigned provenance and vsa + switch { + case opts.outputUnsignedBundle != "": + f, err := os.OpenFile(opts.outputUnsignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec + if err != nil { + return err + } + defer f.Close() //nolint:errcheck - signedVsa, err := attest.Sign(unsignedVsa) - if err != nil { - return err - } + if _, err := f.WriteString(string(unsignedProv) + "\n" + unsignedVsa + "\n"); err != nil { + return fmt.Errorf("writing signed bundle: %w", err) + } + case opts.outputSignedBundle != "" || len(opts.pushLocation) > 0: + var f *os.File + if opts.outputSignedBundle != "" { + f, err = os.OpenFile(opts.outputSignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec + if err != nil { + return err + } + } + defer func() { + if f != nil { + f.Close() //nolint:errcheck,gosec + } + }() + + signedProv, err := attest.Sign(string(unsignedProv)) + if err != nil { + return err + } - // If a file was specified, write the attestations - if f != nil { - if _, err := f.WriteString(signedProv + "\n" + signedVsa + "\n"); err != nil { - return fmt.Errorf("writing bundle data: %w", err) - } - } + signedVsa, err := attest.Sign(unsignedVsa) + if err != nil { + return err + } - cl, err := checkLevelProvArgs.GetCollectorAgent(checkLevelProvArgs.commitOptions, t) - if err != nil { - return fmt.Errorf("creating storage repositories: %w", err) - } + // If a file was specified, write the attestations + if f != nil { + if _, err := f.WriteString(signedProv + "\n" + signedVsa + "\n"); err != nil { + return fmt.Errorf("writing bundle data: %w", err) + } + } - // If there are any storage repositories configured, push the attestations - if cl != nil { - // Parse the attestations into envelopes - envProv, err := envelope.Parsers.Parse(strings.NewReader(signedProv)) - if err != nil || len(envProv) == 0 { - return fmt.Errorf("parsing provenance: %w", err) - } - envVsa, err := envelope.Parsers.Parse(strings.NewReader(signedVsa)) - if err != nil || len(envVsa) == 0 { - return fmt.Errorf("parsing VSA: %w", err) - } + cl, err := opts.GetCollectorAgent(opts.commitOptions, t) + if err != nil { + return fmt.Errorf("creating storage repositories: %w", err) + } - // And store them - err = cl.Store(ctx, []attestation.Envelope{envProv[0], envVsa[0]}) - if err != nil { - return fmt.Errorf("storing attestations: %w", err) + // If there are any storage repositories configured, push the attestations + if cl != nil { + // Parse the attestations into envelopes + envProv, err := envelope.Parsers.Parse(strings.NewReader(signedProv)) + if err != nil || len(envProv) == 0 { + return fmt.Errorf("parsing provenance: %w", err) + } + envVsa, err := envelope.Parsers.Parse(strings.NewReader(signedVsa)) + if err != nil || len(envVsa) == 0 { + return fmt.Errorf("parsing VSA: %w", err) + } + + // And store them + err = cl.Store(cmd.Context(), []attestation.Envelope{envProv[0], envVsa[0]}) + if err != nil { + return fmt.Errorf("storing attestations: %w", err) + } + } + default: + log.Printf("unsigned prov: %s\n", unsignedProv) + log.Printf("unsigned vsa: %s\n", unsignedVsa) } - } - default: - log.Printf("unsigned prov: %s\n", unsignedProv) - log.Printf("unsigned vsa: %s\n", unsignedVsa) + fmt.Print(verifiedLevels) + return nil + }, } - fmt.Print(verifiedLevels) - return nil + + opts.AddFlags(checklevelprovCmd) + parentCmd.AddCommand(checklevelprovCmd) } diff --git a/internal/cmd/checktag.go b/internal/cmd/checktag.go index cd6b0b5..116c2a1 100644 --- a/internal/cmd/checktag.go +++ b/internal/cmd/checktag.go @@ -4,7 +4,6 @@ package cmd import ( - "context" "errors" "fmt" "log" @@ -14,14 +13,15 @@ import ( "google.golang.org/protobuf/encoding/protojson" "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/policy" + "github.com/slsa-framework/source-tool/pkg/sourcetool" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type checkTagOptions struct { repoOptions verifierOptions - commit string + commitOptions tagName string actor string outputSignedBundle string @@ -31,6 +31,7 @@ type checkTagOptions struct { func (cto *checkTagOptions) Validate() error { errs := []error{ + cto.commitOptions.Validate(), cto.repoOptions.Validate(), cto.verifierOptions.Validate(), } @@ -48,6 +49,13 @@ func (cto *checkTagOptions) AddFlags(cmd *cobra.Command) { cmd.PersistentFlags().Uint8Var(&cto.vsaRetries, "retries", 3, "Number of times to retry fetching the commit's VSA") } +func (cto *checkTagOptions) GetTag() *models.Tag { + return &models.Tag{ + Name: cto.tagName, + Commit: cto.GetCommit(), + } +} + func addCheckTag(parentCmd *cobra.Command) { opts := &checkTagOptions{} @@ -56,71 +64,77 @@ func addCheckTag(parentCmd *cobra.Command) { GroupID: "assessment", Short: "Checks to see if the tag operation should be allowed and issues a VSA", RunE: func(cmd *cobra.Command, args []string) error { - return doCheckTag(opts) + authenticator, err := CheckAuth() + if err != nil { + return err + } + + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + ) + if err != nil { + return err + } + + // Create tag provenance. + // pa.Options.VsaRetries = opts.vsaRetries // Retry fetching the commit's VSA + + prov, err := srctool.Attester().CreateTagProvenance(cmd.Context(), opts.GetBranch(), opts.GetTag(), opts.actor) + if err != nil { + return fmt.Errorf("creating tag provenance metadata: %w", err) + } + + // check p against policy + pe := policy.NewPolicyEvaluator() + pe.UseLocalPolicy = opts.useLocalPolicy + verifiedLevels, policyPath, err := pe.EvaluateTagProv(cmd.Context(), opts.GetRepository(), prov) + if err != nil { + return fmt.Errorf("evaluating the tag provenance metadata: %w", err) + } + + // create vsa + unsignedVsa, err := attest.CreateUnsignedSourceVsa( + opts.GetBranch(), opts.GetCommit(), verifiedLevels, policyPath, + ) + if err != nil { + return err + } + + unsignedProv, err := protojson.Marshal(prov) + if err != nil { + return err + } + + if opts.outputSignedBundle != "" { + f, err := os.OpenFile(opts.outputSignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec + if err != nil { + return err + } + defer f.Close() //nolint:errcheck + + signedProv, err := attest.Sign(string(unsignedProv)) + if err != nil { + return err + } + + signedVsa, err := attest.Sign(unsignedVsa) + if err != nil { + return err + } + + if _, err := f.WriteString(signedProv + "\n" + signedVsa + "\n"); err != nil { + return fmt.Errorf("writing bundledata: %w", err) + } + } else { + log.Printf("unsigned prov: %s\n", unsignedProv) + log.Printf("unsigned vsa: %s\n", unsignedVsa) + } + fmt.Print(verifiedLevels) + return nil }, } opts.AddFlags(checktagCmd) parentCmd.AddCommand(checktagCmd) } - -func doCheckTag(args *checkTagOptions) error { - ghconnection := ghcontrol.NewGhConnection(args.owner, args.repository, ghcontrol.TagToFullRef(args.tagName)).WithAuthToken(githubToken) - ctx := context.Background() - verifier := getVerifier(&args.verifierOptions) - - // Create tag provenance. - pa := attest.NewProvenanceAttestor(ghconnection, verifier) - pa.Options.VsaRetries = args.vsaRetries // Retry fetching the commit's VSA - - prov, err := pa.CreateTagProvenance(ctx, args.commit, ghcontrol.TagToFullRef(args.tagName), args.actor) - if err != nil { - return fmt.Errorf("creating tag provenance metadata: %w", err) - } - - // check p against policy - pe := policy.NewPolicyEvaluator() - pe.UseLocalPolicy = args.useLocalPolicy - verifiedLevels, policyPath, err := pe.EvaluateTagProv(ctx, args.GetRepository(), prov) - if err != nil { - return fmt.Errorf("evaluating the tag provenance metadata: %w", err) - } - - // create vsa - unsignedVsa, err := attest.CreateUnsignedSourceVsa(ghconnection.GetRepoUri(), ghconnection.GetFullRef(), args.commit, verifiedLevels, policyPath) - if err != nil { - return err - } - - unsignedProv, err := protojson.Marshal(prov) - if err != nil { - return err - } - - if args.outputSignedBundle != "" { - f, err := os.OpenFile(args.outputSignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec - if err != nil { - return err - } - defer f.Close() //nolint:errcheck - - signedProv, err := attest.Sign(string(unsignedProv)) - if err != nil { - return err - } - - signedVsa, err := attest.Sign(unsignedVsa) - if err != nil { - return err - } - - if _, err := f.WriteString(signedProv + "\n" + signedVsa + "\n"); err != nil { - return fmt.Errorf("writing bundledata: %w", err) - } - } else { - log.Printf("unsigned prov: %s\n", unsignedProv) - log.Printf("unsigned vsa: %s\n", unsignedVsa) - } - fmt.Print(verifiedLevels) - return nil -} diff --git a/internal/cmd/options.go b/internal/cmd/options.go index 93f7cf2..8c10237 100644 --- a/internal/cmd/options.go +++ b/internal/cmd/options.go @@ -210,6 +210,12 @@ func (co *commitOptions) EnsureDefaults() error { return nil } +func (co *commitOptions) GetCommit() *models.Commit { + return &models.Commit{ + SHA: co.commit, + } +} + type verifierOptions struct { expectedIssuer string expectedSan string diff --git a/internal/cmd/prov.go b/internal/cmd/prov.go index 79bc534..bbcb2f6 100644 --- a/internal/cmd/prov.go +++ b/internal/cmd/prov.go @@ -4,15 +4,13 @@ package cmd import ( - "context" "errors" "fmt" "github.com/spf13/cobra" "google.golang.org/protobuf/encoding/protojson" - "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" + "github.com/slsa-framework/source-tool/pkg/sourcetool" ) type provOptions struct { @@ -67,25 +65,43 @@ func addProv(parentCmd *cobra.Command) { if err := opts.Validate(); err != nil { return fmt.Errorf("validating options: %w", err) } - return doProv(&opts) + + authenticator, err := CheckAuth() + if err != nil { + return err + } + + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + ) + if err != nil { + return err + } + + // var prevCommit *models.Commit + // if opts.prevCommit != "" { + // prevCommit = &models.Commit{SHA: opts.prevCommit} + // } else { + // prevCommit, err = srctool.GetPreviousCommit(cmd.Context(), opts.GetBranch(), opts.GetCommit()) + // if err != nil { + // return err + // } + // } + + // opts.prevAttPath, + newProv, err := srctool.Attester().CreateSourceProvenance(cmd.Context(), opts.GetBranch(), opts.GetCommit()) + if err != nil { + return err + } + provStr, err := protojson.Marshal(newProv) + if err != nil { + return err + } + fmt.Println(string(provStr)) + return nil }, } opts.AddFlags(provCmd) parentCmd.AddCommand(provCmd) } - -func doProv(opts *provOptions) error { - ghconnection := ghcontrol.NewGhConnection(opts.owner, opts.repository, ghcontrol.BranchToFullRef(opts.branch)).WithAuthToken(githubToken) - ctx := context.Background() - pa := attest.NewProvenanceAttestor(ghconnection, getVerifier(&opts.verifierOptions)) - newProv, err := pa.CreateSourceProvenance(ctx, opts.prevAttPath, opts.commit, opts.prevCommit, ghconnection.GetFullRef()) - if err != nil { - return err - } - provStr, err := protojson.Marshal(newProv) - if err != nil { - return err - } - fmt.Println(string(provStr)) - return nil -} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 6934e19..3e74233 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -10,22 +10,21 @@ import ( "github.com/spf13/cobra" - "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/auth" ) var githubToken string -func getVerifier(vo *verifierOptions) attest.Verifier { - options := attest.DefaultVerifierOptions - if vo.expectedIssuer != "" { - options.ExpectedIssuer = vo.expectedIssuer - } - if vo.expectedSan != "" { - options.ExpectedSan = vo.expectedSan - } - return attest.NewBndVerifier(options) -} +// func getVerifier(vo *verifierOptions) attest.Verifier { +// options := attest.DefaultVerifierOptions +// if vo.expectedIssuer != "" { +// options.ExpectedIssuer = vo.expectedIssuer +// } +// if vo.expectedSan != "" { +// options.ExpectedSan = vo.expectedSan +// } +// return attest.NewBndVerifier(options) +// } func buildRootCommand() *cobra.Command { // rootCmd represents the base command when called without any subcommands diff --git a/internal/cmd/verifycommit.go b/internal/cmd/verifycommit.go index b23d48b..2d3135d 100644 --- a/internal/cmd/verifycommit.go +++ b/internal/cmd/verifycommit.go @@ -4,14 +4,12 @@ package cmd import ( - "context" "errors" "fmt" "github.com/spf13/cobra" - "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" + "github.com/slsa-framework/source-tool/pkg/sourcetool" ) type verifyCommitOptions struct { @@ -89,55 +87,60 @@ func addVerifyCommit(cmd *cobra.Command) { if err := opts.Validate(); err != nil { return fmt.Errorf("validating options: %w", err) } - return doVerifyCommit(&opts) - }, - } - opts.AddFlags(cmd) - cmd.AddCommand(verifyCommitCmd) -} + authenticator, err := CheckAuth() + if err != nil { + return err + } -func doVerifyCommit(opts *verifyCommitOptions) error { - var ref string - var refType string - var refName string - switch { - case opts.branch != "": - ref = ghcontrol.BranchToFullRef(opts.branch) - refType = "branch" - refName = opts.branch - case opts.tag != "": - ref = ghcontrol.TagToFullRef(opts.tag) - refType = "tag" - refName = opts.tag - default: - return fmt.Errorf("must specify either branch or tag") - } + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + ) + if err != nil { + return err + } + + var refType string + var refName string + switch { + case opts.branch != "": + //ref = ghcontrol.BranchToFullRef(opts.branch) + refType = "branch" + refName = opts.branch + case opts.tag != "": + //ref = ghcontrol.TagToFullRef(opts.tag) + refType = "tag" + refName = opts.tag + default: + return fmt.Errorf("must specify either branch or tag") + } - ghconnection := ghcontrol.NewGhConnection(opts.owner, opts.repository, ref).WithAuthToken(githubToken) - ctx := context.Background() + _, vsaPred, err := srctool.Attester().GetRevisionVSA(cmd.Context(), opts.GetBranch(), opts.GetCommit()) + if err != nil { + return err + } - _, vsaPred, err := attest.GetVsa(ctx, ghconnection, getVerifier(&opts.verifierOptions), opts.commit, ghconnection.GetFullRef()) - if err != nil { - return err - } + result := VerifyCommitResult{ + Success: vsaPred != nil, + Commit: opts.commit, + Ref: refName, + RefType: refType, + Owner: opts.owner, + Repository: opts.repository, + } - result := VerifyCommitResult{ - Success: vsaPred != nil, - Commit: opts.commit, - Ref: refName, - RefType: refType, - Owner: opts.owner, - Repository: opts.repository, - } + if vsaPred == nil { + result.Message = fmt.Sprintf( + "no VSA matching commit '%s' on %s '%s' found in github.com/%s/%s", + opts.commit, refType, refName, opts.owner, opts.repository, + ) + return opts.writeResult(result) + } - if vsaPred == nil { - result.Message = fmt.Sprintf( - "no VSA matching commit '%s' on %s '%s' found in github.com/%s/%s", - opts.commit, refType, refName, opts.owner, opts.repository, - ) - return opts.writeResult(result) + result.VerifiedLevels = vsaPred.GetVerifiedLevels() + return opts.writeResult(result) + }, } - - result.VerifiedLevels = vsaPred.GetVerifiedLevels() - return opts.writeResult(result) + opts.AddFlags(cmd) + cmd.AddCommand(verifyCommitCmd) } From 24afe41ae2c44170eded001390709553dea4b84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 00:13:13 -0600 Subject: [PATCH 18/41] Update attest pkg tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/prov.go | 1 - internal/cmd/verifycommit.go | 5 +- pkg/attest/attester.go | 11 +- pkg/attest/log.go | 1 - pkg/attest/provenance.go | 5 +- pkg/attest/provenance_test.go | 231 ------------------- pkg/attest/statement_test.go | 64 +++-- pkg/attest/vsa_test.go | 171 ++++++++------ pkg/auth/authenticator.go | 2 +- pkg/sourcetool/backends/vcs/github/manage.go | 2 +- 10 files changed, 160 insertions(+), 333 deletions(-) delete mode 100644 pkg/attest/provenance_test.go diff --git a/internal/cmd/prov.go b/internal/cmd/prov.go index bbcb2f6..a0ff3c6 100644 --- a/internal/cmd/prov.go +++ b/internal/cmd/prov.go @@ -34,7 +34,6 @@ func (po *provOptions) AddFlags(cmd *cobra.Command) { cmd.PersistentFlags().StringVar(&po.prevCommit, "prev_commit", "", "The commit prior to 'commit'.") } -//nolint:dupl func addProv(parentCmd *cobra.Command) { opts := provOptions{} provCmd := &cobra.Command{ diff --git a/internal/cmd/verifycommit.go b/internal/cmd/verifycommit.go index 2d3135d..6563169 100644 --- a/internal/cmd/verifycommit.go +++ b/internal/cmd/verifycommit.go @@ -57,7 +57,6 @@ func (vco *verifyCommitOptions) AddFlags(cmd *cobra.Command) { ) } -//nolint:dupl func addVerifyCommit(cmd *cobra.Command) { opts := verifyCommitOptions{} verifyCommitCmd := &cobra.Command{ @@ -104,11 +103,11 @@ func addVerifyCommit(cmd *cobra.Command) { var refName string switch { case opts.branch != "": - //ref = ghcontrol.BranchToFullRef(opts.branch) + // ref = ghcontrol.BranchToFullRef(opts.branch) refType = "branch" refName = opts.branch case opts.tag != "": - //ref = ghcontrol.TagToFullRef(opts.tag) + // ref = ghcontrol.TagToFullRef(opts.tag) refType = "tag" refName = opts.tag default: diff --git a/pkg/attest/attester.go b/pkg/attest/attester.go index b2d8edf..ea827cb 100644 --- a/pkg/attest/attester.go +++ b/pkg/attest/attester.go @@ -13,13 +13,14 @@ import ( "github.com/carabiner-dev/attestation" "github.com/carabiner-dev/collector" intoto "github.com/in-toto/attestation/go/v1" - "github.com/slsa-framework/source-tool/pkg/provenance" - "github.com/slsa-framework/source-tool/pkg/slsa" - "github.com/slsa-framework/source-tool/pkg/sourcetool/models" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/slsa-framework/source-tool/pkg/provenance" + "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type AttesterOptions struct { @@ -97,7 +98,7 @@ func WithNotesCollector(yesno bool) optFn { // Validate checks that the attester configuration is complete func (a *Attester) Validate() error { - var errs = []error{} + errs := []error{} if a.backend == nil { errs = append(errs, errors.New("attester has no backend defined")) } @@ -147,7 +148,7 @@ func (a *Attester) createCurrentProvenance(ctx context.Context, branch *models.B } // Build the provenance predicate - var curProvPred = provenance.SourceProvenancePred{ + curProvPred := provenance.SourceProvenancePred{ PrevCommit: prevCommit.SHA, RepoUri: branch.Repository.GetHttpURL(), ActivityType: controlStatus.ActivityType, diff --git a/pkg/attest/log.go b/pkg/attest/log.go index 7d10a16..977fe37 100644 --- a/pkg/attest/log.go +++ b/pkg/attest/log.go @@ -9,6 +9,5 @@ import ( ) func Debugf(format string, args ...any) { - //nolint:gosec // G706 This is feneral purpose logger slog.Debug(fmt.Sprintf(format, args...)) } diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index 11e48e3..e93e368 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -181,7 +181,6 @@ func (a *Attester) GetRevisionVSA(ctx context.Context, branch *models.Branch, co // None of the collected attestations are valid return nil, nil, nil - } // GetRevisionProvenance returns the provenance attestation for a commit by querying @@ -247,14 +246,14 @@ func (a *Attester) CreateSourceProvenance(ctx context.Context, branch *models.Br return nil, fmt.Errorf("creating provenance predicate: %w", err) } - //prevProvStmt, prevProvPred, err := a.GetRevisionProvenance(ctx, branch, prevCommit) + // prevProvStmt, prevProvPred, err := a.GetRevisionProvenance(ctx, branch, prevCommit) prevProvPred, err := a.GetRevisionProvenance(ctx, branch, prevCommit) if err != nil { return nil, err } // No prior provenance found, so we just go with current. - //if prevProvStmt == nil || prevProvPred == nil { + // if prevProvStmt == nil || prevProvPred == nil { if prevProvPred == nil { Debugf("No previous provenance found, have to bootstrap\n") return curProv, nil diff --git a/pkg/attest/provenance_test.go b/pkg/attest/provenance_test.go deleted file mode 100644 index e030a4c..0000000 --- a/pkg/attest/provenance_test.go +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors -// SPDX-License-Identifier: Apache-2.0 - -package attest - -import ( - "encoding/json" - "testing" - "time" - - "github.com/google/go-github/v69/github" - "github.com/migueleliasweb/go-github-mock/src/mock" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/slsa-framework/source-tool/pkg/ghcontrol" - "github.com/slsa-framework/source-tool/pkg/provenance" - "github.com/slsa-framework/source-tool/pkg/slsa" - "github.com/slsa-framework/source-tool/pkg/testsupport" -) - -var rulesetOldTime = time.Now().Add(-time.Hour) - -func rulesForTagImmutability() *github.RepositoryRulesetRules { - return &github.RepositoryRulesetRules{ - Update: &github.UpdateRuleParameters{}, - Deletion: &github.EmptyRuleParameters{}, - NonFastForward: &github.EmptyRuleParameters{}, - } -} - -func conditionsForTagImmutability() *github.RepositoryRulesetConditions { - return &github.RepositoryRulesetConditions{ - RefName: &github.RepositoryRulesetRefConditionParameters{ - Include: []string{"~ALL"}, - }, - } -} - -func createTestProv(t *testing.T, repoUri, ref, commit string) string { - provPred := provenance.SourceProvenancePred{RepoUri: repoUri, Branch: ref, ActivityType: "pr_merge", Actor: "test actor"} - stmt, err := addPredToStatement(&provPred, provenance.SourceProvPredicateType, commit) - if err != nil { - t.Fatalf("failure creating test prov: %v", err) - } - - statementBytes, err := json.Marshal(&stmt) - require.NoError(t, err, "failure marshalling statement: %v", err) - - return string(statementBytes) -} - -func newNotesContent(content string) *github.RepositoryContent { - return &github.RepositoryContent{ - Content: github.Ptr(content), - } -} - -func newTagHygieneRulesetsResponse(id int64, target github.RulesetTarget, enforcement github.RulesetEnforcement, - updatedAt time.Time, -) *github.RepositoryRuleset { - return &github.RepositoryRuleset{ - ID: github.Ptr(id), - Target: github.Ptr(target), - Enforcement: enforcement, - UpdatedAt: github.Ptr(github.Timestamp{Time: updatedAt}), - Rules: rulesForTagImmutability(), - Conditions: conditionsForTagImmutability(), - } -} - -func newMockedGitHubClient(rulesetResponse *github.RepositoryRuleset, notesContent *github.RepositoryContent) *github.Client { - return github.NewClient(mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetReposRulesetsByOwnerByRepo, - []*github.RepositoryRuleset{ - rulesetResponse, - }, - ), - mock.WithRequestMatch( - mock.GetReposRulesetsByOwnerByRepoByRulesetId, - *rulesetResponse, - ), - mock.WithRequestMatch( - mock.GetReposContentsByOwnerByRepoByPath, - *notesContent, - ), - )) -} - -// Helper to create a test GH Branch connection with no client. -func newTestGhConnection(owner, repo, branch string, rulesetResponse *github.RepositoryRuleset, notesContent *github.RepositoryContent) *ghcontrol.GitHubConnection { - return ghcontrol.NewGhConnectionWithClient( - owner, repo, ghcontrol.BranchToFullRef(branch), - newMockedGitHubClient(rulesetResponse, notesContent)) -} - -func timesEqualWithinMargin(t1, t2 time.Time, margin time.Duration) bool { - diff := t1.Sub(t2).Abs() - return diff <= margin -} - -func assertTagProvPredsEqual(t *testing.T, actual, expected *provenance.TagProvenancePred) { - if actual.GetActor() != expected.GetActor() { - t.Errorf("Actor %v does not match expected value %v", actual.GetActor(), expected.GetActor()) - } - - if actual.GetRepoUri() != expected.GetRepoUri() { - t.Errorf("RepoUri %v does not match expected value %v", actual.GetRepoUri(), expected.GetRepoUri()) - } - - if actual.GetTag() != expected.GetTag() { - t.Errorf("Tag %v does not match expected value %v", actual.GetTag(), expected.GetTag()) - } - - if timesEqualWithinMargin(actual.GetCreatedOn().AsTime(), expected.GetCreatedOn().AsTime(), 5*time.Second) { - t.Errorf("CreatedOn %v does not match expected value %v", actual.GetCreatedOn(), expected.GetCreatedOn()) - } - - if len(actual.GetControls()) != len(expected.GetControls()) { - t.Errorf("Control %v does not match expected value %v", actual.GetControls(), expected.GetControls()) - } else { - for ci := range actual.GetControls() { - if !timesEqualWithinMargin(actual.GetControls()[ci].GetSince().AsTime(), expected.GetControls()[ci].GetSince().AsTime(), time.Second) { - t.Errorf("control at [%d]'s time %v does not match expected time %v", ci, - actual.GetControls()[ci].GetSince(), expected.GetControls()[ci].GetSince()) - } - } - } - - require.Len(t, actual.GetVsaSummaries(), len(expected.GetVsaSummaries())) - for i := range actual.GetVsaSummaries() { - if !proto.Equal(actual.GetVsaSummaries()[i], expected.GetVsaSummaries()[i]) { - t.Errorf("VsaSummaries %v does not match expected value %v", actual.GetVsaSummaries(), expected.GetVsaSummaries()) - } - } -} - -func TestReadProvSuccess(t *testing.T) { - testProv := createTestProv(t, "https://github.com/owner/repo", "main", "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") - ghc := newTestGhConnection("owner", "repo", "branch", - // We just need _some_ rulesets response, we don't care what. - newTagHygieneRulesetsResponse(123, github.RulesetTargetTag, - github.RulesetEnforcementActive, rulesetOldTime), - newNotesContent(testProv)) - verifier := testsupport.NewMockVerifier() - - pa := NewProvenanceAttestor(ghc, verifier) - readStmt, readPred, err := pa.GetProvenance(t.Context(), "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa", "main") - if err != nil { - t.Fatalf("error finding prov: %v", err) - } - if readStmt == nil || readPred == nil { - t.Errorf("could not find provenance") - } -} - -func TestReadProvFailure(t *testing.T) { - testProv := createTestProv(t, "foo", "main", "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") - ghc := newTestGhConnection("owner", "repo", "branch", - // We just need _some_ rulesets response, we don't care what. - newTagHygieneRulesetsResponse(456, github.RulesetTargetBranch, - github.RulesetEnforcementEvaluate, rulesetOldTime), - newNotesContent(testProv)) - verifier := testsupport.NewMockVerifier() - - pa := NewProvenanceAttestor(ghc, verifier) - _, readPred, err := pa.GetProvenance(t.Context(), "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa", "main") - if err != nil { - t.Fatalf("error finding prov: %v", err) - } - if readPred != nil { - t.Errorf("should not have gotten provenance: %+v", readPred) - } -} - -func TestCreateTagProvenance(t *testing.T) { - testVsa := createTestVsa(t, "https://github.com/owner/repo", "refs/some/ref", "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa", slsa.SourceVerifiedLevels{"TEST_LEVEL"}) - - ghc := newTestGhConnection("owner", "repo", "branch", - newTagHygieneRulesetsResponse(123, github.RulesetTargetTag, - github.RulesetEnforcementActive, rulesetOldTime), - newNotesContent(testVsa)) - verifier := testsupport.NewMockVerifier() - - pa := NewProvenanceAttestor(ghc, verifier) - - stmt, err := pa.CreateTagProvenance(t.Context(), "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa", "refs/tags/v1", "the-tag-pusher") - if err != nil { - t.Fatalf("error creating tag prov %v", err) - } - - if stmt == nil { - t.Fatalf("returned statement is nil") - } - - if stmt.GetPredicateType() != provenance.TagProvPredicateType { - t.Errorf("statement pred type %v does not match expected %v", stmt.GetPredicateType(), provenance.TagProvPredicateType) - } - - if !DoesSubjectIncludeCommit(stmt, "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") { - t.Errorf("statement subject %v does not match expected %v", stmt.GetSubject(), "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") - } - - tagPred, err := GetTagProvPred(stmt) - if err != nil { - t.Fatalf("error getting tag prov %v", err) - } - - expectedPred := provenance.TagProvenancePred{ - RepoUri: "https://github.com/owner/repo", - Actor: "the-tag-pusher", - Tag: "refs/tags/v1", - CreatedOn: timestamppb.New(rulesetOldTime), - Controls: []*provenance.Control{ - { - Name: "TAG_HYGIENE", - Since: timestamppb.New(rulesetOldTime), - }, - }, - VsaSummaries: []*provenance.VsaSummary{ - { - SourceRefs: []string{"refs/some/ref"}, - VerifiedLevels: []string{"TEST_LEVEL"}, - }, - }, - } - - assertTagProvPredsEqual(t, tagPred, &expectedPred) -} diff --git a/pkg/attest/statement_test.go b/pkg/attest/statement_test.go index 4a77d68..d525fa0 100644 --- a/pkg/attest/statement_test.go +++ b/pkg/attest/statement_test.go @@ -8,30 +8,66 @@ import ( "slices" "testing" + "github.com/carabiner-dev/attestation" spb "github.com/in-toto/attestation/go/v1" "google.golang.org/protobuf/types/known/structpb" + + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) -func newStatement(commit string, annotation map[string]any) (*spb.Statement, error) { - var sub []*spb.ResourceDescriptor +// mockEnvelope implements attestation.Envelope for testing. +type mockEnvelope struct { + statement *mockStatement + predicate *mockPredicate +} + +func (m *mockEnvelope) GetStatement() attestation.Statement { return m.statement } +func (m *mockEnvelope) GetPredicate() attestation.Predicate { return m.predicate } +func (m *mockEnvelope) GetSignatures() []attestation.Signature { return nil } +func (m *mockEnvelope) GetCertificate() attestation.Certificate { return nil } +func (m *mockEnvelope) GetVerification() attestation.Verification { return nil } +func (m *mockEnvelope) Verify(...any) error { return nil } + +type mockStatement struct { + subjects []attestation.Subject +} + +func (m *mockStatement) GetSubjects() []attestation.Subject { return m.subjects } +func (m *mockStatement) GetPredicate() attestation.Predicate { return nil } +func (m *mockStatement) GetPredicateType() attestation.PredicateType { return "" } +func (m *mockStatement) GetType() string { return "" } +func (m *mockStatement) GetVerification() attestation.Verification { return nil } + +type mockPredicate struct { + data []byte +} + +func (m *mockPredicate) GetType() attestation.PredicateType { return "" } +func (m *mockPredicate) SetType(attestation.PredicateType) error { return nil } +func (m *mockPredicate) GetParsed() any { return nil } +func (m *mockPredicate) GetData() []byte { return m.data } +func (m *mockPredicate) GetVerification() attestation.Verification { return nil } +func (m *mockPredicate) GetOrigin() attestation.Subject { return nil } +func (m *mockPredicate) SetOrigin(attestation.Subject) {} +func (m *mockPredicate) SetVerification(attestation.Verification) {} + +func newMockEnvelope(commit string, annotation map[string]any) (attestation.Envelope, error) { + var subjects []attestation.Subject if annotation != nil { annotationStruct, err := structpb.NewStruct(annotation) if err != nil { return nil, fmt.Errorf("creating struct from map: %w", err) } - sub = []*spb.ResourceDescriptor{{ + subjects = []attestation.Subject{&spb.ResourceDescriptor{ Digest: map[string]string{"gitCommit": commit}, Annotations: annotationStruct, }} } - statementPb := spb.Statement{ - Type: spb.StatementTypeUri, - Subject: sub, - PredicateType: "test", - Predicate: &structpb.Struct{}, - } - return &statementPb, nil + return &mockEnvelope{ + statement: &mockStatement{subjects: subjects}, + predicate: &mockPredicate{data: []byte("{}")}, + }, nil } func stringToAnyArray(valArray []string) []any { @@ -46,7 +82,6 @@ func TestGetSourceRefsForCommit(t *testing.T) { tests := []struct { name string annotationName string - stmt *spb.Statement refs []string expectedRefs []string expectErr bool @@ -89,12 +124,13 @@ func TestGetSourceRefsForCommit(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - stmt, err := newStatement("abc123", map[string]any{tt.annotationName: stringToAnyArray(tt.refs)}) + env, err := newMockEnvelope("abc123", map[string]any{tt.annotationName: stringToAnyArray(tt.refs)}) if err != nil { - t.Fatalf("error creating statement: %v", err) + t.Fatalf("error creating envelope: %v", err) } - gotRefs, err := GetSourceRefsForCommit(stmt, "abc123") + commit := &models.Commit{SHA: "abc123"} + gotRefs, err := GetSourceRefsForCommit(env, commit) if err != nil && !tt.expectErr { t.Errorf("did not expect error, got %v", err) diff --git a/pkg/attest/vsa_test.go b/pkg/attest/vsa_test.go index be110f3..d77fcd4 100644 --- a/pkg/attest/vsa_test.go +++ b/pkg/attest/vsa_test.go @@ -3,104 +3,129 @@ package attest -// TODO: This test package uses some functions that live in provenance_test.go -// that seems ugly and maybe we should fix it. - import ( - "slices" "testing" - "github.com/google/go-github/v69/github" + spb "github.com/in-toto/attestation/go/v1" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" "github.com/slsa-framework/source-tool/pkg/slsa" - "github.com/slsa-framework/source-tool/pkg/testsupport" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) -func createTestVsa(t *testing.T, repoUri, ref, commit string, verifiedLevels slsa.SourceVerifiedLevels) string { - vsa, err := CreateUnsignedSourceVsa(repoUri, ref, commit, verifiedLevels, "test-policy") - if err != nil { - t.Fatalf("failure creating test vsa: %v", err) +func newTestBranch(hostname, path, branchName string) *models.Branch { + return &models.Branch{ + Name: branchName, + Repository: &models.Repository{ + Hostname: hostname, + Path: path, + }, } - return vsa } -func createTestVsaWithIdAndResult(t *testing.T, repoUri, ref, commit, id, result string, verifiedLevels slsa.SourceVerifiedLevels) string { - vsa, err := createUnsignedSourceVsaAllParams(repoUri, ref, commit, verifiedLevels, "test-policy", id, result) - if err != nil { - t.Fatalf("failure creating test vsa: %v", err) - } - return vsa +func newTestCommit(sha string) *models.Commit { + return &models.Commit{SHA: sha} } -func TestReadVsaSuccess(t *testing.T) { - testVsa := createTestVsa(t, "https://github.com/owner/repo", "refs/some/ref", "de9395302d14b24c0a42685cf27315d93c88ff79", slsa.SourceVerifiedLevels{"TEST_LEVEL"}) - ghc := newTestGhConnection("owner", "repo", "branch", - // We just need _some_ rulesets response, we don't care what. - newTagHygieneRulesetsResponse(123, github.RulesetTargetTag, - github.RulesetEnforcementActive, rulesetOldTime), - newNotesContent(testVsa)) - verifier := testsupport.NewMockVerifier() - - readStmt, readPred, err := GetVsa(t.Context(), ghc, verifier, "de9395302d14b24c0a42685cf27315d93c88ff79", "refs/some/ref") - if err != nil { - t.Fatalf("error finding vsa: %v", err) - } - if readStmt == nil || readPred == nil { - t.Errorf("could not find vsa") - } +func TestCreateUnsignedSourceVsa(t *testing.T) { + branch := newTestBranch("github.com", "owner/repo", "main") + commit := newTestCommit("de9395302d14b24c0a42685cf27315d93c88ff79") - if !slices.Contains(readPred.GetVerifiedLevels(), "TEST_LEVEL") { - t.Errorf("expected VSA to contain TEST_LEVEL, but it just contains %v", readPred.GetVerifiedLevels()) - } + vsaJSON, err := CreateUnsignedSourceVsa(branch, commit, slsa.SourceVerifiedLevels{"TEST_LEVEL"}, "test-policy") + require.NoError(t, err) + require.NotEmpty(t, vsaJSON) + + // Parse the statement back + var stmt spb.Statement + err = protojson.Unmarshal([]byte(vsaJSON), &stmt) + require.NoError(t, err) + + require.Equal(t, VsaPredicateType, stmt.GetPredicateType()) + require.Len(t, stmt.GetSubject(), 1) + require.Equal(t, commit.SHA, stmt.GetSubject()[0].GetDigest()["gitCommit"]) + + // Verify the predicate contains the verifier ID and result + predFields := stmt.GetPredicate().GetFields() + require.Equal(t, VsaVerifierId, predFields["verifier"].GetStructValue().GetFields()["id"].GetStringValue()) + require.Equal(t, "PASSED", predFields["verificationResult"].GetStringValue()) + require.Equal(t, "test-policy", predFields["policy"].GetStructValue().GetFields()["uri"].GetStringValue()) + + // Check verified levels + levels := predFields["verifiedLevels"].GetListValue().GetValues() + require.Len(t, levels, 1) + require.Equal(t, "TEST_LEVEL", levels[0].GetStringValue()) + + // Check resource URI includes the repo URL + require.Contains(t, predFields["resourceUri"].GetStringValue(), branch.Repository.GetHttpURL()) } -func TestReadVsaInvalidVsas(t *testing.T) { - goodRepo := "https://github.com/org/foo" - goodRef := "refs/heads/main" - goodCommit := "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa" +func TestCreateUnsignedSourceVsaMultipleLevels(t *testing.T) { + branch := newTestBranch("github.com", "owner/repo", "main") + commit := newTestCommit("73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") + + vsaJSON, err := CreateUnsignedSourceVsa(branch, commit, slsa.SourceVerifiedLevels{"LEVEL_1", "LEVEL_2", "LEVEL_3"}, "test-policy") + require.NoError(t, err) + + var stmt spb.Statement + err = protojson.Unmarshal([]byte(vsaJSON), &stmt) + require.NoError(t, err) + + levels := stmt.GetPredicate().GetFields()["verifiedLevels"].GetListValue().GetValues() + require.Len(t, levels, 3) +} + +func TestCreateUnsignedSourceVsaAllParams(t *testing.T) { + branch := newTestBranch("github.com", "owner/repo", "main") + commit := newTestCommit("73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") - // We want to make sure invalid VSAs aren't returned. tests := []struct { - name string - vsa string + name string + verifierID string + result string }{ { - name: "wrong commit", - vsa: createTestVsa(t, goodRepo, goodRef, "def456", slsa.SourceVerifiedLevels{}), - }, - { - name: "wrong repo uri", - vsa: createTestVsa(t, "https://github.com/foo/bar", goodRef, goodCommit, slsa.SourceVerifiedLevels{}), - }, - { - name: "wrong ref", - vsa: createTestVsa(t, goodRepo, "refs/heads/bad", goodCommit, slsa.SourceVerifiedLevels{}), - }, - { - name: "wrong verifier ID", - vsa: createTestVsaWithIdAndResult(t, goodRepo, goodRef, goodCommit, "bad id", "PASSED", slsa.SourceVerifiedLevels{}), + name: "custom verifier and PASSED", + verifierID: "custom-verifier", + result: "PASSED", }, { - name: "bad result", - vsa: createTestVsaWithIdAndResult(t, goodRepo, goodRef, goodCommit, VsaVerifierId, "FAILED", slsa.SourceVerifiedLevels{}), + name: "default verifier and FAILED", + verifierID: VsaVerifierId, + result: "FAILED", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ghc := newTestGhConnection("org", "foo", "main", - // We just need _some_ rulesets response, we don't care what. - newTagHygieneRulesetsResponse(123, github.RulesetTargetTag, - github.RulesetEnforcementActive, rulesetOldTime), - newNotesContent(tt.vsa)) - verifier := testsupport.NewMockVerifier() - - _, readPred, err := GetVsa(t.Context(), ghc, verifier, "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa", "refs/heads/main") - if err != nil { - t.Fatalf("error finding vsa: %v", err) - } - if readPred != nil { - t.Errorf("should not have gotten vsa: %+v", readPred) - } + vsaJSON, err := createUnsignedSourceVsaAllParams(branch, commit, slsa.SourceVerifiedLevels{}, "test-policy", tt.verifierID, tt.result) + require.NoError(t, err) + + var stmt spb.Statement + err = protojson.Unmarshal([]byte(vsaJSON), &stmt) + require.NoError(t, err) + + predFields := stmt.GetPredicate().GetFields() + require.Equal(t, tt.verifierID, predFields["verifier"].GetStructValue().GetFields()["id"].GetStringValue()) + require.Equal(t, tt.result, predFields["verificationResult"].GetStringValue()) }) } } + +func TestCreateUnsignedSourceVsaSubjectAnnotation(t *testing.T) { + branch := newTestBranch("github.com", "owner/repo", "main") + commit := newTestCommit("abc123") + + vsaJSON, err := CreateUnsignedSourceVsa(branch, commit, slsa.SourceVerifiedLevels{}, "test-policy") + require.NoError(t, err) + + var stmt spb.Statement + err = protojson.Unmarshal([]byte(vsaJSON), &stmt) + require.NoError(t, err) + + // The subject should have a source_refs annotation with the branch ref + annotations := stmt.GetSubject()[0].GetAnnotations() + require.NotNil(t, annotations) + refs := annotations.GetFields()[slsa.SourceRefsAnnotation].GetListValue().GetValues() + require.Len(t, refs, 1) + require.Equal(t, branch.FullRef(), refs[0].GetStringValue()) +} diff --git a/pkg/auth/authenticator.go b/pkg/auth/authenticator.go index 3d50684..f2888e2 100644 --- a/pkg/auth/authenticator.go +++ b/pkg/auth/authenticator.go @@ -54,7 +54,7 @@ type DeviceCodeResponse struct { // TokenResponse is the data structure returned when exchanging tokens type TokenResponse struct { - AccessToken string `json:"access_token"` //nolint:gosec // G117 This is the github struct + AccessToken string `json:"access_token"` TokenType string `json:"token_type"` Scope string `json:"scope"` Error string `json:"error"` diff --git a/pkg/sourcetool/backends/vcs/github/manage.go b/pkg/sourcetool/backends/vcs/github/manage.go index b91a34a..362e7a9 100644 --- a/pkg/sourcetool/backends/vcs/github/manage.go +++ b/pkg/sourcetool/backends/vcs/github/manage.go @@ -21,7 +21,7 @@ const ( ActionsOrg = "slsa-framework" ActionsRepo = "source-actions" workflowPath = ".github/workflows/compute_slsa_source.yaml" - //workflowSource = "git+https://github.com/slsa-" + // workflowSource = "git+https://github.com/slsa-" // workflowCommitMessage will be used as the commit message and the PR title workflowCommitMessage = "Add SLSA Source Provenance Workflow" From db77af8c60b548ef1067470f03b2799551eee4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 00:16:48 -0600 Subject: [PATCH 19/41] Fix bug in subject extrction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/attest/statement.go | 5 +-- pkg/attest/statement_test.go | 85 ++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/pkg/attest/statement.go b/pkg/attest/statement.go index f279776..bad0315 100644 --- a/pkg/attest/statement.go +++ b/pkg/attest/statement.go @@ -155,9 +155,8 @@ func GetSubjectForCommit(att attestation.Envelope, commit *models.Commit) *intot return rd } - val, ok = rd.GetDigest()["sha1"] - if rd.GetDigest()["sha1"] == commit.SHA { - return fromSha + if val, ok = rd.GetDigest()["sha1"]; ok && val == commit.SHA { + fromSha = rd } } diff --git a/pkg/attest/statement_test.go b/pkg/attest/statement_test.go index d525fa0..b119739 100644 --- a/pkg/attest/statement_test.go +++ b/pkg/attest/statement_test.go @@ -70,6 +70,91 @@ func newMockEnvelope(commit string, annotation map[string]any) (attestation.Enve }, nil } +func newMockEnvelopeWithSubjects(subjects []attestation.Subject) attestation.Envelope { + return &mockEnvelope{ + statement: &mockStatement{subjects: subjects}, + predicate: &mockPredicate{data: []byte("{}")}, + } +} + +func TestGetSubjectForCommit(t *testing.T) { + commitSHA := "abc123" + commit := &models.Commit{SHA: commitSHA} + + tests := []struct { + name string + subjects []attestation.Subject + wantNil bool + }{ + { + name: "match by gitCommit", + subjects: []attestation.Subject{ + &spb.ResourceDescriptor{ + Digest: map[string]string{"gitCommit": commitSHA}, + }, + }, + }, + { + name: "match by sha1 fallback", + subjects: []attestation.Subject{ + &spb.ResourceDescriptor{ + Digest: map[string]string{"sha1": commitSHA}, + }, + }, + }, + { + name: "gitCommit preferred over sha1", + subjects: []attestation.Subject{ + &spb.ResourceDescriptor{ + Digest: map[string]string{"sha1": commitSHA}, + Name: "sha1-subject", + }, + &spb.ResourceDescriptor{ + Digest: map[string]string{"gitCommit": commitSHA}, + Name: "gitCommit-subject", + }, + }, + }, + { + name: "no match", + subjects: []attestation.Subject{ + &spb.ResourceDescriptor{ + Digest: map[string]string{"gitCommit": "other"}, + }, + }, + wantNil: true, + }, + { + name: "empty subjects", + subjects: nil, + wantNil: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := newMockEnvelopeWithSubjects(tt.subjects) + got := GetSubjectForCommit(env, commit) + + if tt.wantNil { + if got != nil { + t.Errorf("expected nil, got %v", got) + } + return + } + if got == nil { + t.Fatal("expected non-nil subject") + } + + // For the "preferred" test, verify we got the gitCommit subject + if tt.name == "gitCommit preferred over sha1" { + if got.GetName() != "gitCommit-subject" { + t.Errorf("expected gitCommit-subject, got %s", got.GetName()) + } + } + }) + } +} + func stringToAnyArray(valArray []string) []any { aa := make([]any, len(valArray)) for i := range valArray { From 6c4123c72c560037d68f35ef5ccd76fc4efdac6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 15:02:29 -0600 Subject: [PATCH 20/41] Wire back allow merge commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/checklevel.go | 1 + internal/cmd/checklevelprov.go | 4 +--- pkg/sourcetool/backends/vcs/github/github.go | 10 ++++++---- pkg/sourcetool/models/models.go | 5 +++++ pkg/sourcetool/options.go | 7 +++++++ pkg/sourcetool/options/options.go | 3 +++ pkg/sourcetool/tool.go | 7 ++++++- 7 files changed, 29 insertions(+), 8 deletions(-) diff --git a/internal/cmd/checklevel.go b/internal/cmd/checklevel.go index 6e5b023..6bef0bb 100644 --- a/internal/cmd/checklevel.go +++ b/internal/cmd/checklevel.go @@ -80,6 +80,7 @@ This is meant to be run within the corresponding GitHub Actions workflow.`, // Create a new sourcetool object srctool, err := sourcetool.New( sourcetool.WithAuthenticator(authenticator), + sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), ) if err != nil { return err diff --git a/internal/cmd/checklevelprov.go b/internal/cmd/checklevelprov.go index 8bcf4c9..418c6d9 100644 --- a/internal/cmd/checklevelprov.go +++ b/internal/cmd/checklevelprov.go @@ -195,14 +195,12 @@ and pushed to its remote (--push=note). // Create a new sourcetool object srctool, err := sourcetool.New( sourcetool.WithAuthenticator(authenticator), + sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), ) if err != nil { return err } - // TODO: Esto donde lo metemos - // ghconnection.Options.AllowMergeCommits = opts.allowMergeCommits - // var prevCommit *models.Commit // if opts.prevCommit != "" { // prevCommit = &models.Commit{SHA: opts.prevCommit} diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index 5bd4a74..245fa50 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -66,10 +66,10 @@ var InherentControls = slsa.ControlNameSet{ // slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, } -func New() *Backend { +func New(options *models.BackendOptions) *Backend { return &Backend{ authenticator: auth.New(), - Options: Options{UseFork: true}, + Options: options, } } @@ -80,7 +80,7 @@ type Options struct { // Backend implemets the GitHub sourcetool backend type Backend struct { authenticator *auth.Authenticator - Options Options + Options *models.BackendOptions } // getGitHubConnection builds a github connector to a repository @@ -103,7 +103,9 @@ func (b *Backend) getGitHubConnection(repository *models.Repository, ref string) return nil, err } - return ghcontrol.NewGhConnectionWithClient(owner, name, ref, client), nil + ghc := ghcontrol.NewGhConnectionWithClient(owner, name, ref, client) + ghc.Options.AllowMergeCommits = b.Options.AllowMergeCommits + return ghc, nil } func (b *Backend) GetBranchControls(ctx context.Context, branch *models.Branch) (*slsa.ControlSet, error) { diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index 3a821b5..f9dea81 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -49,6 +49,11 @@ type VcsBackend interface { GetPreviousCommit(context.Context, *Branch, *Commit) (*Commit, error) } +type BackendOptions struct { + AllowMergeCommits bool + DriverOptions any +} + // ControlPreRemediation is a function returned by the VCS backends // when checking for prerequisites that the user may optionally run type ControlPreRemediationFn func() (string, error) diff --git a/pkg/sourcetool/options.go b/pkg/sourcetool/options.go index 7a9a136..e9978df 100644 --- a/pkg/sourcetool/options.go +++ b/pkg/sourcetool/options.go @@ -62,3 +62,10 @@ func WithPolicyRepo(slug string) ConfigFn { return nil } } + +func WithAllowMergeCommits(allow bool) ConfigFn { + return func(t *Tool) error { + t.Options.AllowMergeCommits = allow + return nil + } +} diff --git a/pkg/sourcetool/options/options.go b/pkg/sourcetool/options/options.go index b311f54..7566496 100644 --- a/pkg/sourcetool/options/options.go +++ b/pkg/sourcetool/options/options.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/slsa-framework/source-tool/pkg/policy" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type Options struct { @@ -26,6 +27,8 @@ type Options struct { // Initialize Dynamic notes collector InitNotesCollector bool + + models.BackendOptions } // DefaultOptions holds the default options the tool initializes with diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index f6c76be..80821f4 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -32,7 +32,7 @@ var ControlConfigurations = []models.ControlConfiguration{ func New(funcs ...ConfigFn) (*Tool, error) { t := &Tool{ Options: options.Default, - backend: github.New(), // For now this is fixed to the github backend + backend: github.New(&options.Default.BackendOptions), // For now this is fixed to the github backend impl: &defaultToolImplementation{}, } @@ -42,6 +42,11 @@ func New(funcs ...ConfigFn) (*Tool, error) { } } + // Propagate options to the backend + if ghBackend, ok := t.backend.(*github.Backend); ok { + ghBackend.Options.AllowMergeCommits = t.Options.AllowMergeCommits + } + // Create the tool's attester attester, err := attest.NewAttester( attest.WithVerifier(attest.GetDefaultVerifier()), From 557286e9125feac65ce616d74e6b9b72530a8b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 18:33:16 -0600 Subject: [PATCH 21/41] Correct githb control gen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/attest/attester.go | 16 +++-- pkg/attest/provenance.go | 4 +- pkg/sourcetool/backends/vcs/github/github.go | 61 +++++++++++++++----- 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/pkg/attest/attester.go b/pkg/attest/attester.go index ea827cb..f01a95b 100644 --- a/pkg/attest/attester.go +++ b/pkg/attest/attester.go @@ -147,6 +147,10 @@ func (a *Attester) createCurrentProvenance(ctx context.Context, branch *models.B return nil, err } + if controlStatus == nil { + return nil, errors.New("VCS backend returned a nil controlset") + } + // Build the provenance predicate curProvPred := provenance.SourceProvenancePred{ PrevCommit: prevCommit.SHA, @@ -162,11 +166,13 @@ func (a *Attester) createCurrentProvenance(ctx context.Context, branch *models.B // ... indeed, but don't set the `since`` date because doing so breaks // checking against policies. // See https://github.com/slsa-framework/source-tool/issues/272 - curProvPred.AddControl( - &provenance.Control{ - Name: slsa.SLSA_SOURCE_SCS_PROVENANCE.String(), - }, - ) + if curProvPred.GetControl(slsa.SLSA_SOURCE_SCS_PROVENANCE.String()) == nil { + curProvPred.AddControl( + &provenance.Control{ + Name: slsa.SLSA_SOURCE_SCS_PROVENANCE.String(), + }, + ) + } return addPredToStatement(&curProvPred, provenance.SourceProvPredicateType, commit.SHA) } diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index e93e368..dbe1a79 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -107,7 +107,7 @@ func (a *Attester) getCollector(branch *models.Branch) (*collector.Agent, error) return agent, nil } -// GetVSA returns a revision's VSA attestation +// GetRevisionVSA returns a revision's VSA attestation func (a *Attester) GetRevisionVSA(ctx context.Context, branch *models.Branch, commit *models.Commit) (attestation.Envelope, *vsa.VerificationSummary, error) { if commit == nil { return nil, nil, errors.New("commit is nil") @@ -281,7 +281,7 @@ func (a *Attester) CreateSourceProvenance(ctx context.Context, branch *models.Br } // CreateTagProvenance creates a provenance statement for a tag. -func (a Attester) CreateTagProvenance(ctx context.Context, branch *models.Branch, tag *models.Tag, actor string) (*intoto.Statement, error) { +func (a *Attester) CreateTagProvenance(ctx context.Context, branch *models.Branch, tag *models.Tag, actor string) (*intoto.Statement, error) { if tag.Commit == nil { return nil, fmt.Errorf("tag does not have its commit set") } diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index 245fa50..fd9016b 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -136,6 +136,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. if commit == nil { return nil, errors.New("commit is not set") } + ghc, err := b.getGitHubConnection(branch.Repository, branch.FullRef()) if err != nil { return nil, fmt.Errorf("getting github connection: %w", err) @@ -150,7 +151,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. // We need to manually check for PROVENANCE_AVAILABLE which is not // handled by ghcontrol - attestor, err := attest.NewAttester( + attester, err := attest.NewAttester( attest.WithBackend(b), attest.WithVerifier(attest.GetDefaultVerifier()), ) if err != nil { @@ -158,23 +159,55 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. } // Fetch the attestation. If found, then add the control: - attestation, err := attestor.GetRevisionProvenance(ctx, branch, commit) + attestation, err := attester.GetRevisionProvenance(ctx, branch, commit) if err != nil { return nil, fmt.Errorf("attempting to read provenance from commit %q: %w", commit.SHA, err) } if attestation != nil { - activeControls.AddControl(&slsa.Control{ - Name: slsa.SLSA_SOURCE_SCS_PROVENANCE, - }) - - // If we got the provenance attestaion, we assume we also have a VSA - activeControls.AddControl(&slsa.Control{ - Name: slsa.SLSA_SOURCE_SCS_VSA, - }) + // We carry over the since date from the previous attestation, only if + // it > 0 (unix origin) + var t *time.Time + if ctrl := attestation.GetControl(slsa.SLSA_SOURCE_SCS_PROVENANCE.String()); ctrl != nil { + if ctrl.Since != nil && ctrl.Since.AsTime().Unix() != 0 { + rt := ctrl.Since.AsTime() + t = &rt + } + } else if ctrl := attestation.GetControl(slsa.DEPRECATED_ProvenanceAvailable.String()); ctrl != nil { + if ctrl.Since != nil && ctrl.Since.AsTime().Unix() != 0 { + rt := ctrl.Since.AsTime() + t = &rt + } + } + + if tc := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_PROVENANCE); tc != nil { + if t != nil { + tc.Since = t + } + } else { + activeControls.AddControl(&slsa.Control{ + Name: slsa.SLSA_SOURCE_SCS_PROVENANCE, + Since: t, + }) + } } else { log.Printf("No provenance attestation found on %s", commit.SHA) } + _, vsaPred, err := attester.GetRevisionVSA(ctx, branch, commit) + if err != nil { + return nil, fmt.Errorf("reading VSA: %w", err) + } + + if vsaPred != nil { + if tc := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_VSA); tc == nil { + activeControls.AddControl(&slsa.Control{ + Name: slsa.SLSA_SOURCE_SCS_VSA, + }) + } + } else { + log.Printf("No VSA found for commit") + } + // NewControlSet returns all the controls for the framework in // StateNotEnabled. status := slsa.NewControlSet() @@ -188,9 +221,9 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. continue } - // Check if it's one of the active controls + // Check if it's one of the active controls and enable it and copy the since date if c := activeControls.GetControl(ctrl.Name); c != nil { - status.Controls[i].Since = ctrl.Since + status.Controls[i].Since = c.Since status.Controls[i].State = slsa.StateActive status.Controls[i].Message = b.controlImplementationMessage(c.GetName()) } @@ -199,7 +232,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. // Without force push, content cannot be expunged. if ctrl.Name == slsa.SLSA_SOURCE_ORG_SAFE_EXPUNGE { if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS); c != nil { - status.Controls[i].Since = ctrl.Since + status.Controls[i].Since = c.Since status.Controls[i].State = slsa.StateActive status.Controls[i].Message = b.controlImplementationMessage(ctrl.Name) } @@ -208,7 +241,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. // Enable ORG_CONTINUITY when SCS branch continuity is active. if ctrl.Name == slsa.SLSA_SOURCE_ORG_CONTINUITY { if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_CONTINUITY); c != nil { - status.Controls[i].Since = ctrl.Since + status.Controls[i].Since = c.Since status.Controls[i].State = slsa.StateActive status.Controls[i].Message = b.controlImplementationMessage(ctrl.Name) } From 406b942791dd5cbae741431589d58f1842523138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 18:33:57 -0600 Subject: [PATCH 22/41] ControlSet: Fix panic when date is nil MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/slsa/slsa_types.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/slsa/slsa_types.go b/pkg/slsa/slsa_types.go index d3369f0..e46db84 100644 --- a/pkg/slsa/slsa_types.go +++ b/pkg/slsa/slsa_types.go @@ -221,8 +221,10 @@ func (cs *ControlSet) ToProvenanceControls() []*provenance.Control { continue } c := &provenance.Control{ - Name: ctl.Name.String(), - Since: timestamppb.New(*ctl.Since), + Name: ctl.Name.String(), + } + if ctl.Since != nil { + c.Since = timestamppb.New(*ctl.Since) } ret = append(ret, c) } From deb3611d3a3f0b182631f53891b3902b81b5b281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 18:34:16 -0600 Subject: [PATCH 23/41] Don't add .git con GH URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/sourcetool/models/models.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index f9dea81..9879bab 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -102,9 +102,6 @@ func (r *Repository) GetHttpURL() string { return "" } u := fmt.Sprintf("https://%s/%s", r.Hostname, r.Path) - if r.Hostname == "github.com" { - u += ".git" - } return u } From a49b1a1151ab98c59788d0a9b004e1b7e60ce454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 19:29:16 -0600 Subject: [PATCH 24/41] Wire authenticator to repos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/attest/attester.go | 19 +++++++++---- pkg/attest/provenance.go | 30 ++++++++++++++++++-- pkg/sourcetool/backends/vcs/github/github.go | 8 +++--- pkg/sourcetool/tool.go | 9 +++--- 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/pkg/attest/attester.go b/pkg/attest/attester.go index f01a95b..7b9af51 100644 --- a/pkg/attest/attester.go +++ b/pkg/attest/attester.go @@ -11,13 +11,13 @@ import ( "time" "github.com/carabiner-dev/attestation" - "github.com/carabiner-dev/collector" intoto "github.com/in-toto/attestation/go/v1" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/slsa-framework/source-tool/pkg/auth" "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" @@ -40,11 +40,11 @@ var defaultAttesterOptions = AttesterOptions{ } type Attester struct { - verifier Verifier - backend models.VcsBackend - Options AttesterOptions - collector *collector.Agent - storer attestation.Storer + verifier Verifier + backend models.VcsBackend + Options AttesterOptions + storer attestation.Storer + authenticator *auth.Authenticator } type optFn func(*Attester) error @@ -68,6 +68,13 @@ func WithRetries(r uint8) optFn { } } +func WithAuthenticator(athn *auth.Authenticator) optFn { + return func(a *Attester) error { + a.authenticator = athn + return nil + } +} + func WithBackend(b models.VcsBackend) optFn { return func(a *Attester) error { a.backend = b diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index dbe1a79..3394b1e 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -13,6 +13,8 @@ import ( "github.com/carabiner-dev/attestation" "github.com/carabiner-dev/collector" "github.com/carabiner-dev/collector/filters" + "github.com/carabiner-dev/collector/repository/github" + "github.com/carabiner-dev/collector/repository/note" vsa "github.com/in-toto/attestation/go/predicates/vsa/v1" intoto "github.com/in-toto/attestation/go/v1" "google.golang.org/protobuf/encoding/protojson" @@ -86,14 +88,38 @@ func (a *Attester) getCollector(branch *models.Branch) (*collector.Agent, error) return nil, err } + var token string + if a.Options.InitNotesCollector || a.Options.InitGHCollector { + token, err = a.authenticator.ReadToken() + if err != nil { + return nil, fmt.Errorf("fetching token from authenticator: %w", err) + } + } + + // These two require the authenticator token if a.Options.InitNotesCollector { - if err := agent.AddRepositoryFromString(fmt.Sprintf("dnote:%s", branch.Repository.GetHttpURL())); err != nil { + repo, err := note.NewDynamic( + note.DynamicRepoURL(fmt.Sprintf("dnote:%s", branch.Repository.GetHttpURL())), + note.WithHttpAuth("github", token), + ) + if err != nil { + return nil, fmt.Errorf("creating notes collector: %w", err) + } + + if err := agent.AddRepository(repo); err != nil { return nil, err } } if a.Options.InitGHCollector { - if err := agent.AddRepositoryFromString(fmt.Sprintf("github:%s", branch.Repository.Path)); err != nil { + repo, err := github.New( + github.WithRepo(branch.Repository.Path), + github.WithToken(token), + ) + if err != nil { + return nil, fmt.Errorf("creating github collector: %w", err) + } + if err := agent.AddRepository(repo); err != nil { return nil, err } } diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index fd9016b..17f2643 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -168,13 +168,13 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. // it > 0 (unix origin) var t *time.Time if ctrl := attestation.GetControl(slsa.SLSA_SOURCE_SCS_PROVENANCE.String()); ctrl != nil { - if ctrl.Since != nil && ctrl.Since.AsTime().Unix() != 0 { - rt := ctrl.Since.AsTime() + if ctrl.GetSince() != nil && ctrl.GetSince().AsTime().Unix() != 0 { + rt := ctrl.GetSince().AsTime() t = &rt } } else if ctrl := attestation.GetControl(slsa.DEPRECATED_ProvenanceAvailable.String()); ctrl != nil { - if ctrl.Since != nil && ctrl.Since.AsTime().Unix() != 0 { - rt := ctrl.Since.AsTime() + if ctrl.GetSince() != nil && ctrl.GetSince().AsTime().Unix() != 0 { + rt := ctrl.GetSince().AsTime() t = &rt } } diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index 80821f4..6d2a424 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -42,10 +42,10 @@ func New(funcs ...ConfigFn) (*Tool, error) { } } - // Propagate options to the backend - if ghBackend, ok := t.backend.(*github.Backend); ok { - ghBackend.Options.AllowMergeCommits = t.Options.AllowMergeCommits - } + // // Propagate options to the backend + // if ghBackend, ok := t.backend.(*github.Backend); ok { + // ghBackend.Options.AllowMergeCommits = t.Options.AllowMergeCommits + // } // Create the tool's attester attester, err := attest.NewAttester( @@ -53,6 +53,7 @@ func New(funcs ...ConfigFn) (*Tool, error) { attest.WithBackend(t.backend), attest.WithGithubCollector(t.Options.InitGHCollector), attest.WithNotesCollector(t.Options.InitNotesCollector), + attest.WithAuthenticator(t.Authenticator), ) if err != nil { return nil, fmt.Errorf("creating attester: %w", err) From d576f10723bbcddc26a00e5688a0c20581f8ceac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 19:50:24 -0600 Subject: [PATCH 25/41] Lazy init of VCS backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This modifies the VCS backend init to later in the tool creation to allow all options to be applied before starting it. Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/attest/provenance.go | 2 +- pkg/sourcetool/backends/vcs/github/github.go | 1 + pkg/sourcetool/options.go | 2 +- pkg/sourcetool/options/options.go | 1 + pkg/sourcetool/tool.go | 6 +----- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index 3394b1e..45f4647 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -99,7 +99,7 @@ func (a *Attester) getCollector(branch *models.Branch) (*collector.Agent, error) // These two require the authenticator token if a.Options.InitNotesCollector { repo, err := note.NewDynamic( - note.DynamicRepoURL(fmt.Sprintf("dnote:%s", branch.Repository.GetHttpURL())), + note.DynamicRepoURL(branch.Repository.GetHttpURL()), note.WithHttpAuth("github", token), ) if err != nil { diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index 17f2643..8d75028 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -153,6 +153,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. // handled by ghcontrol attester, err := attest.NewAttester( attest.WithBackend(b), attest.WithVerifier(attest.GetDefaultVerifier()), + attest.WithAuthenticator(b.authenticator), ) if err != nil { return nil, err diff --git a/pkg/sourcetool/options.go b/pkg/sourcetool/options.go index e9978df..3dbf3f8 100644 --- a/pkg/sourcetool/options.go +++ b/pkg/sourcetool/options.go @@ -65,7 +65,7 @@ func WithPolicyRepo(slug string) ConfigFn { func WithAllowMergeCommits(allow bool) ConfigFn { return func(t *Tool) error { - t.Options.AllowMergeCommits = allow + t.Options.BackendOptions.AllowMergeCommits = allow return nil } } diff --git a/pkg/sourcetool/options/options.go b/pkg/sourcetool/options/options.go index 7566496..75fe3c1 100644 --- a/pkg/sourcetool/options/options.go +++ b/pkg/sourcetool/options/options.go @@ -37,4 +37,5 @@ var Default = Options{ UseSSH: true, CreatePolicyPR: true, InitNotesCollector: true, + BackendOptions: models.BackendOptions{}, } diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index 6d2a424..a2e6191 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -32,7 +32,6 @@ var ControlConfigurations = []models.ControlConfiguration{ func New(funcs ...ConfigFn) (*Tool, error) { t := &Tool{ Options: options.Default, - backend: github.New(&options.Default.BackendOptions), // For now this is fixed to the github backend impl: &defaultToolImplementation{}, } @@ -42,10 +41,7 @@ func New(funcs ...ConfigFn) (*Tool, error) { } } - // // Propagate options to the backend - // if ghBackend, ok := t.backend.(*github.Backend); ok { - // ghBackend.Options.AllowMergeCommits = t.Options.AllowMergeCommits - // } + t.backend = github.New(&t.Options.BackendOptions) // Create the tool's attester attester, err := attest.NewAttester( From cb4c0a87eb7b069054e10c3fdb80b4189f08507d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 20:06:40 -0600 Subject: [PATCH 26/41] Update auditor to use components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/audit.go | 51 ++++++++++++--------------- internal/cmd/audit_test.go | 29 +++++++--------- pkg/audit/audit.go | 71 ++++++++++++++++++++++++++------------ pkg/sourcetool/tool.go | 5 +++ 4 files changed, 88 insertions(+), 68 deletions(-) diff --git a/internal/cmd/audit.go b/internal/cmd/audit.go index 8613c21..06f092b 100644 --- a/internal/cmd/audit.go +++ b/internal/cmd/audit.go @@ -9,9 +9,7 @@ import ( "github.com/spf13/cobra" - "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/audit" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/sourcetool" ) @@ -74,9 +72,9 @@ type AuditCommitResultJSON struct { VerifiedLevels []string `json:"verified_levels,omitempty"` PrevCommitMatches *bool `json:"prev_commit_matches,omitempty"` ProvControls interface{} `json:"prov_controls,omitempty"` - GhControls interface{} `json:"gh_controls,omitempty"` + Controls interface{} `json:"controls,omitempty"` PrevCommit string `json:"prev_commit,omitempty"` - GhPriorCommit string `json:"gh_prior_commit,omitempty"` + PriorCommit string `json:"prior_commit,omitempty"` Link string `json:"link,omitempty"` Error string `json:"error,omitempty"` } @@ -168,13 +166,12 @@ Future: return err } - ghc := ghcontrol.NewGhConnection(opts.owner, opts.repository, ghcontrol.BranchToFullRef(opts.branch)).WithAuthToken(githubToken) - - auditor := audit.NewAuditor(ghc, srctool.Attester(), attest.GetDefaultVerifier()) - - latestCommit, err := ghc.GetLatestCommit(cmd.Context(), opts.branch) + auditor, err := audit.NewAuditor( + audit.WithAttester(srctool.Attester()), + audit.WithBackend(srctool.Backend()), + ) if err != nil { - return fmt.Errorf("could not get latest commit for %s", opts.branch) + return err } // Initialize JSON result structure if needed @@ -184,12 +181,10 @@ Future: Owner: opts.owner, Repository: opts.repository, Branch: opts.branch, - LatestCommit: latestCommit, CommitResults: []AuditCommitResultJSON{}, } } else { - // Print header for text output - opts.writeTextf("Auditing branch %s starting from revision %s\n", opts.branch, latestCommit) + opts.writeTextf("Auditing branch %s\n", opts.branch) } // Single loop for both JSON and text output @@ -204,7 +199,7 @@ Future: // Process result based on output format if opts.outputFormatIsJSON() { - commitResult := convertAuditResultToJSON(ghc, ar, opts.auditMode) + commitResult := convertAuditResultToJSON(opts.owner, opts.repository, ar, opts.auditMode) if err != nil { commitResult.Error = err.Error() } @@ -219,7 +214,7 @@ Future: if err != nil { opts.writeTextf("\terror: %v\n", err) } - printResult(ghc, ar, opts.auditMode) + printResult(opts.owner, opts.repository, ar, opts.auditMode) } // Check for early termination conditions @@ -255,7 +250,7 @@ Future: parentCmd.AddCommand(auditCmd) } -func printResult(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCommitResult, mode AuditMode) { +func printResult(owner, repo string, ar *audit.AuditCommitResult, mode AuditMode) { good := ar.IsGood() status := statusPassed if !good { @@ -275,22 +270,22 @@ func printResult(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCommitResult, m if ar.ProvPred != nil { fmt.Print("\tprov:\n") fmt.Printf("\t\tcontrols: %v\n", ar.ProvPred.GetControls()) - if ar.ProvPred.GetPrevCommit() == ar.GhPriorCommit { - fmt.Printf("\t\tPrevCommit matches GH commit: true\n") + if ar.ProvPred.GetPrevCommit() == ar.PriorCommit { + fmt.Printf("\t\tPrevCommit matches prior commit: true\n") } else { - fmt.Printf("\t\tPrevCommit matches GH commit: false: %s != %s\n", ar.ProvPred.GetPrevCommit(), ar.GhPriorCommit) + fmt.Printf("\t\tPrevCommit matches prior commit: false: %s != %s\n", ar.ProvPred.GetPrevCommit(), ar.PriorCommit) } } else { fmt.Printf("\tprov: none\n") } - if ar.GhControlStatus != nil { - fmt.Printf("\tgh controls: %v\n", ar.GhControlStatus.Controls) + if ar.ControlStatus != nil { + fmt.Printf("\tcontrols: %v\n", ar.ControlStatus.Controls) } - fmt.Printf("\tlink: https://github.com/%s/%s/commit/%s\n", ghc.Owner(), ghc.Repo(), ar.GhPriorCommit) + fmt.Printf("\tlink: https://github.com/%s/%s/commit/%s\n", owner, repo, ar.PriorCommit) } -func convertAuditResultToJSON(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCommitResult, mode AuditMode) AuditCommitResultJSON { +func convertAuditResultToJSON(owner, repo string, ar *audit.AuditCommitResult, mode AuditMode) AuditCommitResultJSON { good := ar.IsGood() status := statusPassed if !good { @@ -300,7 +295,7 @@ func convertAuditResultToJSON(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCo result := AuditCommitResultJSON{ Commit: ar.Commit, Status: status, - Link: fmt.Sprintf("https://github.com/%s/%s/commit/%s", ghc.Owner(), ghc.Repo(), ar.GhPriorCommit), + Link: fmt.Sprintf("https://github.com/%s/%s/commit/%s", owner, repo, ar.PriorCommit), } // Only include details if mode is Full or status is failed @@ -312,13 +307,13 @@ func convertAuditResultToJSON(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCo if ar.ProvPred != nil { result.ProvControls = ar.ProvPred.GetControls() result.PrevCommit = ar.ProvPred.GetPrevCommit() - result.GhPriorCommit = ar.GhPriorCommit - matches := ar.ProvPred.GetPrevCommit() == ar.GhPriorCommit + result.PriorCommit = ar.PriorCommit + matches := ar.ProvPred.GetPrevCommit() == ar.PriorCommit result.PrevCommitMatches = &matches } - if ar.GhControlStatus != nil { - result.GhControls = ar.GhControlStatus.Controls + if ar.ControlStatus != nil { + result.Controls = ar.ControlStatus.Controls } } diff --git a/internal/cmd/audit_test.go b/internal/cmd/audit_test.go index 20a8d6c..e6c1633 100644 --- a/internal/cmd/audit_test.go +++ b/internal/cmd/audit_test.go @@ -11,7 +11,6 @@ import ( vpb "github.com/in-toto/attestation/go/predicates/vsa/v1" "github.com/slsa-framework/source-tool/pkg/audit" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" ) @@ -140,8 +139,6 @@ func TestAuditResultJSON_JSONMarshaling(t *testing.T) { } func TestConvertAuditResultToJSON(t *testing.T) { - ghc := ghcontrol.NewGhConnection("test-owner", "test-repo", "refs/heads/main") - tests := []struct { name string result *audit.AuditCommitResult @@ -158,7 +155,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { ProvPred: &provenance.SourceProvenancePred{ PrevCommit: "def456", }, - GhPriorCommit: "def456", + PriorCommit: "def456", }, mode: AuditModeBasic, want: AuditCommitResultJSON{ @@ -178,10 +175,8 @@ func TestConvertAuditResultToJSON(t *testing.T) { PrevCommit: "def456", Controls: []*provenance.Control{{Name: "test_control"}}, }, - GhPriorCommit: "def456", - GhControlStatus: &ghcontrol.GhControlStatus{ - Controls: &slsa.ControlSet{}, - }, + PriorCommit: "def456", + ControlStatus: &slsa.ControlSet{}, }, mode: AuditModeFull, want: func() AuditCommitResultJSON { @@ -192,9 +187,9 @@ func TestConvertAuditResultToJSON(t *testing.T) { VerifiedLevels: []string{"SLSA_SOURCE_LEVEL_3"}, PrevCommitMatches: &matches, ProvControls: []*provenance.Control{{Name: "test_control"}}, - GhControls: slsa.ControlSet{}, + Controls: slsa.ControlSet{}, PrevCommit: "def456", - GhPriorCommit: "def456", + PriorCommit: "def456", Link: "https://github.com/test-owner/test-repo/commit/def456", } }(), @@ -202,10 +197,10 @@ func TestConvertAuditResultToJSON(t *testing.T) { { name: "failed audit shows details even in basic mode", result: &audit.AuditCommitResult{ - Commit: "abc123", - VsaPred: nil, - ProvPred: nil, - GhPriorCommit: "def456", + Commit: "abc123", + VsaPred: nil, + ProvPred: nil, + PriorCommit: "def456", }, mode: AuditModeBasic, want: AuditCommitResultJSON{ @@ -225,7 +220,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { PrevCommit: "wrong123", Controls: []*provenance.Control{{Name: "test_control"}}, }, - GhPriorCommit: "def456", + PriorCommit: "def456", }, mode: AuditModeFull, want: func() AuditCommitResultJSON { @@ -237,7 +232,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { PrevCommitMatches: &matches, ProvControls: []*provenance.Control{{Name: "test_control"}}, PrevCommit: "wrong123", - GhPriorCommit: "def456", + PriorCommit: "def456", Link: "https://github.com/test-owner/test-repo/commit/def456", } }(), @@ -246,7 +241,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := convertAuditResultToJSON(ghc, tt.result, tt.mode) + got := convertAuditResultToJSON("test-owner", "test-repo", tt.result, tt.mode) if got.Commit != tt.want.Commit { t.Errorf("Commit = %v, want %v", got.Commit, tt.want.Commit) diff --git a/pkg/audit/audit.go b/pkg/audit/audit.go index 230f929..3df4dd3 100644 --- a/pkg/audit/audit.go +++ b/pkg/audit/audit.go @@ -5,6 +5,7 @@ package audit import ( "context" + "errors" "fmt" "iter" "os" @@ -12,26 +13,39 @@ import ( vpb "github.com/in-toto/attestation/go/predicates/vsa/v1" "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/provenance" + "github.com/slsa-framework/source-tool/pkg/slsa" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type Auditor struct { - ghc *ghcontrol.GitHubConnection - // TODO: This should probably be turned into a pointer. - verifier attest.Verifier attester *attest.Attester backend models.VcsBackend } +type optFn func(*Auditor) error + +func WithAttester(a *attest.Attester) optFn { + return func(au *Auditor) error { + au.attester = a + return nil + } +} + +func WithBackend(b models.VcsBackend) optFn { + return func(au *Auditor) error { + au.backend = b + return nil + } +} + type AuditCommitResult struct { Commit string VsaPred *vpb.VerificationSummary ProvPred *provenance.SourceProvenancePred - // The previous commit reported by GH. - GhPriorCommit string - GhControlStatus *ghcontrol.GhControlStatus + // The previous commit reported by the VCS backend. + PriorCommit string + ControlStatus *slsa.ControlSet } func (ar *AuditCommitResult) IsGood() bool { @@ -41,7 +55,7 @@ func (ar *AuditCommitResult) IsGood() bool { // Have to have provenance if ar.ProvPred == nil { good = false - } else if ar.ProvPred.GetPrevCommit() != ar.GhPriorCommit { + } else if ar.ProvPred.GetPrevCommit() != ar.PriorCommit { // Commits need to be the same. good = false } @@ -49,12 +63,25 @@ func (ar *AuditCommitResult) IsGood() bool { return good } -func NewAuditor(ghc *ghcontrol.GitHubConnection, pa *attest.Attester, verifier attest.Verifier) *Auditor { - return &Auditor{ - ghc: ghc, - verifier: verifier, - attester: pa, +func NewAuditor(fn ...optFn) (*Auditor, error) { + a := &Auditor{} + for _, f := range fn { + if err := f(a); err != nil { + return nil, err + } + } + + errs := []error{} + if a.attester == nil { + errs = append(errs, errors.New("auditor has no attester")) + } + if a.backend == nil { + errs = append(errs, errors.New("auditor has no backend")) + } + if err := errors.Join(errs...); err != nil { + return nil, err } + return a, nil } func (a *Auditor) AuditCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (ar *AuditCommitResult, err error) { @@ -72,25 +99,23 @@ func (a *Auditor) AuditCommit(ctx context.Context, branch *models.Branch, commit } ar.ProvPred = prov - ghPrior, err := a.ghc.GetPriorCommit(ctx, commit.SHA) + prior, err := a.backend.GetPreviousCommit(ctx, branch, commit) if err != nil { return nil, fmt.Errorf("could not get prior commit for revision %s: %w", commit, err) } - ar.GhPriorCommit = ghPrior + ar.PriorCommit = prior.SHA - var controlStatus *ghcontrol.GhControlStatus if prov == nil { - // If there's no provenance, let's check the controls to see how they're looking. + // If there's no provenance, check the controls to see how they're looking. // It could be that provenance generation failed, but the controls were still // in place. - // TODO: (use backend method here) - controlStatus, err = a.ghc.GetBranchControlsAtCommit(ctx, commit.SHA, a.ghc.GetFullRef()) + controlStatus, err := a.backend.GetBranchControlsAtCommit(ctx, branch, commit) if err != nil { - // Let's still return ar so they can continue if they want. - return ar, fmt.Errorf("could not get controls for %s on %s: %w", commit, a.ghc.GetFullRef(), err) + // Still return ar so callers can continue if they want. + return ar, fmt.Errorf("could not get controls for %s on %s: %w", commit.SHA, branch.FullRef(), err) } + ar.ControlStatus = controlStatus } - ar.GhControlStatus = controlStatus return ar, nil } @@ -113,7 +138,7 @@ func (a *Auditor) AuditBranch(ctx context.Context, branch *models.Branch) iter.S if !yield(ar, err) { return } - nextCommit = &models.Commit{SHA: ar.GhPriorCommit} + nextCommit = &models.Commit{SHA: ar.PriorCommit} } } } diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index a2e6191..985920f 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -312,6 +312,11 @@ func (t *Tool) Attester() *attest.Attester { return t.attester } +// Backend returns the VCS backend +func (t *Tool) Backend() models.VcsBackend { + return t.backend +} + // GetPreviousCommit returns the previous commit of the passed commit func (t *Tool) GetPreviousCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*models.Commit, error) { return t.backend.GetPreviousCommit(ctx, branch, commit) From 76edad6f355cad57163d998a7b1cec7697bc1d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 20:17:04 -0600 Subject: [PATCH 27/41] Fix linter nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/checklevelprov.go | 10 ---------- internal/cmd/prov.go | 11 ----------- internal/cmd/verifycommit.go | 2 -- pkg/attest/provenance.go | 3 +-- pkg/attest/vsa_test.go | 1 + pkg/audit/audit.go | 4 ++-- pkg/auth/implementation.go | 2 +- pkg/sourcetool/options.go | 2 +- 8 files changed, 6 insertions(+), 29 deletions(-) diff --git a/internal/cmd/checklevelprov.go b/internal/cmd/checklevelprov.go index 418c6d9..6a81e68 100644 --- a/internal/cmd/checklevelprov.go +++ b/internal/cmd/checklevelprov.go @@ -201,16 +201,6 @@ and pushed to its remote (--push=note). return err } - // var prevCommit *models.Commit - // if opts.prevCommit != "" { - // prevCommit = &models.Commit{SHA: opts.prevCommit} - // } else { - // prevCommit, err = srctool.GetPreviousCommit(cmd.Context(), opts.GetBranch(), opts.GetCommit()) - // if err != nil { - // return err - // } - // } - // Create the provenance attestation prov, err := srctool.Attester().CreateSourceProvenance( cmd.Context(), opts.GetBranch(), opts.GetCommit(), diff --git a/internal/cmd/prov.go b/internal/cmd/prov.go index a0ff3c6..9a85f92 100644 --- a/internal/cmd/prov.go +++ b/internal/cmd/prov.go @@ -78,17 +78,6 @@ func addProv(parentCmd *cobra.Command) { return err } - // var prevCommit *models.Commit - // if opts.prevCommit != "" { - // prevCommit = &models.Commit{SHA: opts.prevCommit} - // } else { - // prevCommit, err = srctool.GetPreviousCommit(cmd.Context(), opts.GetBranch(), opts.GetCommit()) - // if err != nil { - // return err - // } - // } - - // opts.prevAttPath, newProv, err := srctool.Attester().CreateSourceProvenance(cmd.Context(), opts.GetBranch(), opts.GetCommit()) if err != nil { return err diff --git a/internal/cmd/verifycommit.go b/internal/cmd/verifycommit.go index 6563169..0e6981f 100644 --- a/internal/cmd/verifycommit.go +++ b/internal/cmd/verifycommit.go @@ -103,11 +103,9 @@ func addVerifyCommit(cmd *cobra.Command) { var refName string switch { case opts.branch != "": - // ref = ghcontrol.BranchToFullRef(opts.branch) refType = "branch" refName = opts.branch case opts.tag != "": - // ref = ghcontrol.TagToFullRef(opts.tag) refType = "tag" refName = opts.tag default: diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index 45f4647..dd7897d 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -164,7 +164,7 @@ func (a *Attester) GetRevisionVSA(ctx context.Context, branch *models.Branch, co time.Sleep(time.Duration(i*5) * time.Second) } if attErr != nil { - return nil, nil, fmt.Errorf("fetching attestations: %w", err) + return nil, nil, fmt.Errorf("fetching attestations: %w", attErr) } if len(atts) == 0 { @@ -272,7 +272,6 @@ func (a *Attester) CreateSourceProvenance(ctx context.Context, branch *models.Br return nil, fmt.Errorf("creating provenance predicate: %w", err) } - // prevProvStmt, prevProvPred, err := a.GetRevisionProvenance(ctx, branch, prevCommit) prevProvPred, err := a.GetRevisionProvenance(ctx, branch, prevCommit) if err != nil { return nil, err diff --git a/pkg/attest/vsa_test.go b/pkg/attest/vsa_test.go index d77fcd4..5695ab1 100644 --- a/pkg/attest/vsa_test.go +++ b/pkg/attest/vsa_test.go @@ -14,6 +14,7 @@ import ( "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) +//nolint:unparam func newTestBranch(hostname, path, branchName string) *models.Branch { return &models.Branch{ Name: branchName, diff --git a/pkg/audit/audit.go b/pkg/audit/audit.go index 3df4dd3..0fc2f35 100644 --- a/pkg/audit/audit.go +++ b/pkg/audit/audit.go @@ -44,8 +44,8 @@ type AuditCommitResult struct { VsaPred *vpb.VerificationSummary ProvPred *provenance.SourceProvenancePred // The previous commit reported by the VCS backend. - PriorCommit string - ControlStatus *slsa.ControlSet + PriorCommit string + ControlStatus *slsa.ControlSet } func (ar *AuditCommitResult) IsGood() bool { diff --git a/pkg/auth/implementation.go b/pkg/auth/implementation.go index faae556..2c6d309 100644 --- a/pkg/auth/implementation.go +++ b/pkg/auth/implementation.go @@ -100,7 +100,7 @@ func (di *defaultImplementation) checkTokenStatus(ctx context.Context, deviceCod // device authorization webpage. func (di *defaultImplementation) openBrowser(authURL string) error { var cmd string - var args []string + var args []string //nolint:prealloc switch runtime.GOOS { case "windows": diff --git a/pkg/sourcetool/options.go b/pkg/sourcetool/options.go index 3dbf3f8..e9978df 100644 --- a/pkg/sourcetool/options.go +++ b/pkg/sourcetool/options.go @@ -65,7 +65,7 @@ func WithPolicyRepo(slug string) ConfigFn { func WithAllowMergeCommits(allow bool) ConfigFn { return func(t *Tool) error { - t.Options.BackendOptions.AllowMergeCommits = allow + t.Options.AllowMergeCommits = allow return nil } } From feb0709bcf363d251b160fc621f5d68981b5f656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Mon, 9 Mar 2026 20:22:25 -0600 Subject: [PATCH 28/41] update integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/sourcetool/tool_test.go | 100 ++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/pkg/sourcetool/tool_test.go b/pkg/sourcetool/tool_test.go index 1784f6d..4349a88 100644 --- a/pkg/sourcetool/tool_test.go +++ b/pkg/sourcetool/tool_test.go @@ -71,16 +71,6 @@ func TestConfigureControls(t *testing.T) { return i }, }, - { - name: "GetVcsBackend-fails", mustErr: true, - controls: []models.ControlConfiguration{models.CONFIG_BRANCH_RULES}, - prepare: func(t *testing.T) toolImplementation { - t.Helper() - i := &sourcetoolfakes.FakeToolImplementation{} - i.GetVcsBackendReturns(nil, syntErr) - return i - }, - }, { name: "CheckPolicyFork-fails", mustErr: true, controls: []models.ControlConfiguration{models.CONFIG_POLICY}, @@ -291,50 +281,62 @@ func TestOnboardRepository(t *testing.T) { t.Parallel() for _, tt := range []struct { name string - getSut func(t *testing.T) toolImplementation + impl func(t *testing.T) toolImplementation + backend func(t *testing.T) models.VcsBackend mustErr bool }{ - {"normal", func(t *testing.T) toolImplementation { - t.Helper() - timp := &sourcetoolfakes.FakeToolImplementation{} - timp.GetVcsBackendReturns(&modelsfakes.FakeVcsBackend{}, nil) - timp.VerifyOptionsForFullOnboardReturns(nil) - timp.ConfigureControlsReturns(nil) - return timp - }, false}, - {"get-vcs-fails", func(t *testing.T) toolImplementation { - t.Helper() - timp := &sourcetoolfakes.FakeToolImplementation{} - timp.GetVcsBackendReturns(nil, errors.New("vcsborked")) - timp.VerifyOptionsForFullOnboardReturns(nil) - timp.ConfigureControlsReturns(nil) - return timp - }, true}, - {"get-verifyoptions-fails", func(t *testing.T) toolImplementation { - t.Helper() - timp := &sourcetoolfakes.FakeToolImplementation{} - timp.GetVcsBackendReturns(&modelsfakes.FakeVcsBackend{}, nil) - timp.VerifyOptionsForFullOnboardReturns(errors.New("onboarderr")) - return timp - }, true}, - {"backend-configure-controls-fails", func(t *testing.T) toolImplementation { - t.Helper() - bend := modelsfakes.FakeVcsBackend{} - bend.ConfigureControlsReturns(errors.New("configure-error")) - - timp := &sourcetoolfakes.FakeToolImplementation{} - timp.GetVcsBackendReturns(&bend, nil) - timp.VerifyOptionsForFullOnboardReturns(nil) - return timp - }, true}, + { + name: "normal", + impl: func(t *testing.T) toolImplementation { + t.Helper() + timp := &sourcetoolfakes.FakeToolImplementation{} + timp.VerifyOptionsForFullOnboardReturns(nil) + return timp + }, + backend: func(t *testing.T) models.VcsBackend { + t.Helper() + return &modelsfakes.FakeVcsBackend{} + }, + mustErr: false, + }, + { + name: "get-verifyoptions-fails", + impl: func(t *testing.T) toolImplementation { + t.Helper() + timp := &sourcetoolfakes.FakeToolImplementation{} + timp.VerifyOptionsForFullOnboardReturns(errors.New("onboarderr")) + return timp + }, + backend: func(t *testing.T) models.VcsBackend { + t.Helper() + return &modelsfakes.FakeVcsBackend{} + }, + mustErr: true, + }, + { + name: "backend-configure-controls-fails", + impl: func(t *testing.T) toolImplementation { + t.Helper() + timp := &sourcetoolfakes.FakeToolImplementation{} + timp.VerifyOptionsForFullOnboardReturns(nil) + return timp + }, + backend: func(t *testing.T) models.VcsBackend { + t.Helper() + bend := &modelsfakes.FakeVcsBackend{} + bend.ConfigureControlsReturns(errors.New("configure-error")) + return bend + }, + mustErr: true, + }, } { t.Run(tt.name, func(t *testing.T) { t.Parallel() - impl := tt.getSut(t) - tool, err := New() - require.NoError(t, err) - tool.impl = impl - err = tool.OnboardRepository(t.Context(), &models.Repository{Path: "example/repo"}, []*models.Branch{{Name: "main"}}) + tool := &Tool{ + impl: tt.impl(t), + backend: tt.backend(t), + } + err := tool.OnboardRepository(t.Context(), &models.Repository{Path: "example/repo"}, []*models.Branch{{Name: "main"}}) if tt.mustErr { require.Error(t, err) return From bbd0bbc033f14ba4f79ea2f7347d455848f05db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Tue, 10 Mar 2026 15:42:11 -0600 Subject: [PATCH 29/41] Bump go in dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2251b69..1980375 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,3 @@ # This is used to we scrap the go version and use in CI to get the latest go version # and we use dependabot to keep the go version up to date -FROM golang:1.25.7 +FROM golang:1.25.8 From 36314bfc7ef9bc93383c6c88b06b28ff4cbfe033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Tue, 10 Mar 2026 15:46:51 -0600 Subject: [PATCH 30/41] Regenerate fakes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- .../fake_tool_implementation.go | 68 ------------------- pkg/sourcetool/tool_test.go | 1 - 2 files changed, 69 deletions(-) diff --git a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go index dc766c7..0d38f3d 100644 --- a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go +++ b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go @@ -126,18 +126,6 @@ type FakeToolImplementation struct { result1 *slsa.Control result2 error } - GetVcsBackendStub func() (models.VcsBackend, error) - getVcsBackendMutex sync.RWMutex - getVcsBackendArgsForCall []struct { - } - getVcsBackendReturns struct { - result1 models.VcsBackend - result2 error - } - getVcsBackendReturnsOnCall map[int]struct { - result1 models.VcsBackend - result2 error - } SearchPullRequestStub func(context.Context, *auth.Authenticator, *models.Repository, string) (*models.PullRequest, error) searchPullRequestMutex sync.RWMutex searchPullRequestArgsForCall []struct { @@ -697,62 +685,6 @@ func (fake *FakeToolImplementation) GetPolicyStatusReturnsOnCall(i int, result1 }{result1, result2} } -func (fake *FakeToolImplementation) GetVcsBackend() (models.VcsBackend, error) { - fake.getVcsBackendMutex.Lock() - ret, specificReturn := fake.getVcsBackendReturnsOnCall[len(fake.getVcsBackendArgsForCall)] - fake.getVcsBackendArgsForCall = append(fake.getVcsBackendArgsForCall, struct { - }{}) - stub := fake.GetVcsBackendStub - fakeReturns := fake.getVcsBackendReturns - fake.recordInvocation("GetVcsBackend", []interface{}{}) - fake.getVcsBackendMutex.Unlock() - if stub != nil { - return stub() - } - if specificReturn { - return ret.result1, ret.result2 - } - return fakeReturns.result1, fakeReturns.result2 -} - -func (fake *FakeToolImplementation) GetVcsBackendCallCount() int { - fake.getVcsBackendMutex.RLock() - defer fake.getVcsBackendMutex.RUnlock() - return len(fake.getVcsBackendArgsForCall) -} - -func (fake *FakeToolImplementation) GetVcsBackendCalls(stub func() (models.VcsBackend, error)) { - fake.getVcsBackendMutex.Lock() - defer fake.getVcsBackendMutex.Unlock() - fake.GetVcsBackendStub = stub -} - -func (fake *FakeToolImplementation) GetVcsBackendReturns(result1 models.VcsBackend, result2 error) { - fake.getVcsBackendMutex.Lock() - defer fake.getVcsBackendMutex.Unlock() - fake.GetVcsBackendStub = nil - fake.getVcsBackendReturns = struct { - result1 models.VcsBackend - result2 error - }{result1, result2} -} - -func (fake *FakeToolImplementation) GetVcsBackendReturnsOnCall(i int, result1 models.VcsBackend, result2 error) { - fake.getVcsBackendMutex.Lock() - defer fake.getVcsBackendMutex.Unlock() - fake.GetVcsBackendStub = nil - if fake.getVcsBackendReturnsOnCall == nil { - fake.getVcsBackendReturnsOnCall = make(map[int]struct { - result1 models.VcsBackend - result2 error - }) - } - fake.getVcsBackendReturnsOnCall[i] = struct { - result1 models.VcsBackend - result2 error - }{result1, result2} -} - func (fake *FakeToolImplementation) SearchPullRequest(arg1 context.Context, arg2 *auth.Authenticator, arg3 *models.Repository, arg4 string) (*models.PullRequest, error) { fake.searchPullRequestMutex.Lock() ret, specificReturn := fake.searchPullRequestReturnsOnCall[len(fake.searchPullRequestArgsForCall)] diff --git a/pkg/sourcetool/tool_test.go b/pkg/sourcetool/tool_test.go index 4349a88..ee73c22 100644 --- a/pkg/sourcetool/tool_test.go +++ b/pkg/sourcetool/tool_test.go @@ -87,7 +87,6 @@ func TestConfigureControls(t *testing.T) { prepare: func(t *testing.T) toolImplementation { t.Helper() i := &sourcetoolfakes.FakeToolImplementation{} - i.GetVcsBackendReturns(&modelsfakes.FakeVcsBackend{}, nil) i.CreatePolicyPRReturns(nil, syntErr) return i }, From a9b8bd761373165aaaf8bb77091c659f30c9c506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Tue, 10 Mar 2026 21:01:12 -0600 Subject: [PATCH 31/41] Formalize attestation push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- go.mod | 4 +- go.sum | 4 + internal/cmd/checklevelprov.go | 93 +------------ pkg/attest/attester.go | 17 +-- pkg/sourcetool/options.go | 21 +++ pkg/sourcetool/options/options.go | 8 +- pkg/sourcetool/tool.go | 216 ++++++++++++++++++++++++++++++ 7 files changed, 261 insertions(+), 102 deletions(-) diff --git a/go.mod b/go.mod index f9817c0..9546bda 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.25.8 require ( github.com/carabiner-dev/attestation v0.2.1 - github.com/carabiner-dev/collector v0.2.10-0.20260309053530-b55bbe428700 - github.com/carabiner-dev/signer v0.3.7 + github.com/carabiner-dev/collector v0.2.10-0.20260310234513-8d637f10649f + github.com/carabiner-dev/signer v0.3.8-0.20260310160610-a37998585604 github.com/carabiner-dev/vcslocator v0.4.0 github.com/fatih/color v1.18.0 github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f diff --git a/go.sum b/go.sum index 9306205..692fd88 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,8 @@ github.com/carabiner-dev/attestation v0.2.1 h1:VhjV5YlO9TsW50Sr/Zd54bdbZhhDAqgxC github.com/carabiner-dev/attestation v0.2.1/go.mod h1:O84vF84RZG3pJO/6BYrPs718bZviHF5DKajP1HsrDpw= github.com/carabiner-dev/collector v0.2.10-0.20260309053530-b55bbe428700 h1:S4NFLl3/UJvA4aUuPVY3MECUQS0jhHdT//+excPvWUc= github.com/carabiner-dev/collector v0.2.10-0.20260309053530-b55bbe428700/go.mod h1:ltqIryrtd+Ev+zAW9Q2h/UHNVMhT2nR1Fmchk+8lbs8= +github.com/carabiner-dev/collector v0.2.10-0.20260310234513-8d637f10649f h1:2Q5VzbSVNKPwhP9rUjwH3ZWajDYwxB+3ZT0MTU/80ho= +github.com/carabiner-dev/collector v0.2.10-0.20260310234513-8d637f10649f/go.mod h1:nqe5n0soDuenKtOt9i4G8D/anZiRTiZywzo3pNqf5As= github.com/carabiner-dev/ghrfs v0.3.4 h1:XJoDXkuw+8KQPTC4oI0da8vLpnx7cfQBGgyjzo+Eqrc= github.com/carabiner-dev/ghrfs v0.3.4/go.mod h1:u9We7molIUX6sCe4ox70juKOnbNAUpDv+B5Cerbqhio= github.com/carabiner-dev/github v0.2.2 h1:Ykrlcct71fRQm4j37LhAz9FyzG4n1nlm2e+V62MIoJM= @@ -107,6 +109,8 @@ github.com/carabiner-dev/predicates v0.1.0 h1:t6tQF9gFdr6TIccWtuNk3kFasx8eu88INF github.com/carabiner-dev/predicates v0.1.0/go.mod h1:jL6EAD+LiI6GW/rOdRYAJF4HaA88/V2Q4n7yUGNQ7XM= github.com/carabiner-dev/signer v0.3.7 h1:oEmOg17Szs5+x0oVVGXluPNsVaQyROlXrBPxgwxfOHg= github.com/carabiner-dev/signer v0.3.7/go.mod h1:gReZbCZlINz8Pm/hrD1HtRcGFaO9MGUAu/v+iZp7R6Y= +github.com/carabiner-dev/signer v0.3.8-0.20260310160610-a37998585604 h1:2PeAzIFCqbCF/upSb4Hqj5HwTvuJ6D6KjP2F5yH7WrA= +github.com/carabiner-dev/signer v0.3.8-0.20260310160610-a37998585604/go.mod h1:kqmUAFHKgQXFsIsIFjbk4UlOMXNte7J1IVxIgKAnY/M= github.com/carabiner-dev/vcslocator v0.4.0 h1:HxU8F7FWJatnIhR6NowsPpfki8xw2uNDRLuikcBHFao= github.com/carabiner-dev/vcslocator v0.4.0/go.mod h1:B3JFnwypdrRVQPJKNK3BOlRw6DbgmsiHGmoT3ZELpp8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= diff --git a/internal/cmd/checklevelprov.go b/internal/cmd/checklevelprov.go index 6a81e68..0869c8f 100644 --- a/internal/cmd/checklevelprov.go +++ b/internal/cmd/checklevelprov.go @@ -11,11 +11,6 @@ import ( "slices" "strings" - "github.com/carabiner-dev/attestation" - "github.com/carabiner-dev/collector" - "github.com/carabiner-dev/collector/envelope" - "github.com/carabiner-dev/collector/repository/github" - "github.com/carabiner-dev/collector/repository/note" "github.com/spf13/cobra" "google.golang.org/protobuf/encoding/protojson" @@ -59,61 +54,6 @@ func (po *pushOptions) AddFlags(cmd *cobra.Command) { cmd.PersistentFlags().StringSliceVar(&po.pushLocation, "push", []string{}, fmt.Sprintf("Push signed attestations to storage %v", supportedPushRepos)) } -func (po *pushOptions) GetCollectorAgent(opts commitOptions, token string) (*collector.Agent, error) { - if len(po.pushLocation) == 0 { - return nil, nil - } - - // Create the attestation storage repositories - agent, err := collector.New() - if err != nil { - return nil, err - } - - for _, uri := range po.pushLocation { - var repo attestation.Repository - var err error - - // Translate just "note" or "github" to the full repo spec acting on - // the sepcified commit - switch uri { - case note.TypeMoniker: - uri = fmt.Sprintf( - "note:git+https://github.com/%s/%s@%s", - opts.owner, opts.repository, opts.commit, - ) - case github.TypeMoniker: - uri = fmt.Sprintf("github:%s/%s", opts.owner, opts.repository) - } - switch { - case strings.HasPrefix(uri, "github:"): - repo, err = github.New( - // Initialize the github repository - github.WithInit(uri), - // We pass the token to use in the githu client - github.WithToken(token), - ) - case strings.HasPrefix(uri, "note:"): - repo, err = note.New( - // Initialize the notes repository - note.WithInit(uri), - // Push is enabled as we will append the note to the remote - note.WithPush(true), - // Push via http, using the GH access token - note.WithHttpAuth("x-access-token", token), - ) - default: - return nil, fmt.Errorf("repository type not supported") - } - if err != nil { - return nil, fmt.Errorf("creating storage repository: %w", err) - } - agent.AddRepository(repo) //nolint:errcheck,gosec // always returns nil - } - - return agent, nil -} - type checkLevelProvOpts struct { commitOptions verifierOptions @@ -187,18 +127,15 @@ and pushed to its remote (--push=note). return err } - t, err := authenticator.ReadToken() - if err != nil { - return err - } - // Create a new sourcetool object srctool, err := sourcetool.New( sourcetool.WithAuthenticator(authenticator), sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), + sourcetool.WithNotesStorer(slices.Contains(opts.pushLocation, "notes")), + sourcetool.WithGithubStorer(slices.Contains(opts.pushLocation, "github")), ) if err != nil { - return err + return fmt.Errorf("creating sourcetool: %w", err) } // Create the provenance attestation @@ -272,30 +209,6 @@ and pushed to its remote (--push=note). return fmt.Errorf("writing bundle data: %w", err) } } - - cl, err := opts.GetCollectorAgent(opts.commitOptions, t) - if err != nil { - return fmt.Errorf("creating storage repositories: %w", err) - } - - // If there are any storage repositories configured, push the attestations - if cl != nil { - // Parse the attestations into envelopes - envProv, err := envelope.Parsers.Parse(strings.NewReader(signedProv)) - if err != nil || len(envProv) == 0 { - return fmt.Errorf("parsing provenance: %w", err) - } - envVsa, err := envelope.Parsers.Parse(strings.NewReader(signedVsa)) - if err != nil || len(envVsa) == 0 { - return fmt.Errorf("parsing VSA: %w", err) - } - - // And store them - err = cl.Store(cmd.Context(), []attestation.Envelope{envProv[0], envVsa[0]}) - if err != nil { - return fmt.Errorf("storing attestations: %w", err) - } - } default: log.Printf("unsigned prov: %s\n", unsignedProv) log.Printf("unsigned vsa: %s\n", unsignedVsa) diff --git a/pkg/attest/attester.go b/pkg/attest/attester.go index 7b9af51..8fef6a8 100644 --- a/pkg/attest/attester.go +++ b/pkg/attest/attester.go @@ -10,7 +10,6 @@ import ( "slices" "time" - "github.com/carabiner-dev/attestation" intoto "github.com/in-toto/attestation/go/v1" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" @@ -24,12 +23,15 @@ import ( ) type AttesterOptions struct { - // Initialize dynamic notes collector + // Initialize dynamic notes fetcher and storer InitNotesCollector bool - // Initialize attestations store collector + + // Initialize attestations store collector and storer InitGHCollector bool + // Additional read repositories Repos []string + // Times to retry fetching attestations Retries uint8 } @@ -43,7 +45,6 @@ type Attester struct { verifier Verifier backend models.VcsBackend Options AttesterOptions - storer attestation.Storer authenticator *auth.Authenticator } @@ -106,22 +107,22 @@ func WithNotesCollector(yesno bool) optFn { // Validate checks that the attester configuration is complete func (a *Attester) Validate() error { errs := []error{} + + // Check a backend is configured if a.backend == nil { errs = append(errs, errors.New("attester has no backend defined")) } + // Check we have attestation repos to read if len(a.Options.Repos) == 0 && !a.Options.InitGHCollector && !a.Options.InitNotesCollector { errs = append(errs, errors.New("no attestation repository configured")) } + // Check we have a signature verifier if a.verifier == nil { errs = append(errs, errors.New("attester has no verifier")) } - if a.storer == nil { - // errs = append(errs, errors.New("attester has no attestation storer defined")) - } - return errors.Join(errs...) } diff --git a/pkg/sourcetool/options.go b/pkg/sourcetool/options.go index e9978df..94c7d62 100644 --- a/pkg/sourcetool/options.go +++ b/pkg/sourcetool/options.go @@ -18,6 +18,13 @@ func WithGithubCollector(yesno bool) ConfigFn { } } +func WithGithubStorer(yesno bool) ConfigFn { + return func(t *Tool) error { + t.Options.InitGHStorer = yesno + return nil + } +} + func WithNotesCollector(yesno bool) ConfigFn { return func(t *Tool) error { t.Options.InitNotesCollector = yesno @@ -25,6 +32,20 @@ func WithNotesCollector(yesno bool) ConfigFn { } } +func WithNotesStorer(yesno bool) ConfigFn { + return func(t *Tool) error { + t.Options.InitNotesStorer = yesno + return nil + } +} + +func WithStorageLocation(l ...string) ConfigFn { + return func(t *Tool) error { + t.Options.StorageLocations = l + return nil + } +} + func WithAuthenticator(a *auth.Authenticator) ConfigFn { return func(t *Tool) error { if a == nil { diff --git a/pkg/sourcetool/options/options.go b/pkg/sourcetool/options/options.go index 75fe3c1..d506c42 100644 --- a/pkg/sourcetool/options/options.go +++ b/pkg/sourcetool/options/options.go @@ -22,11 +22,15 @@ type Options struct { // PolicyRepo is the repository where the policies are stored PolicyRepo string - // Initialize GitHub attestations store collector + // Initialize GitHub attestations storer and fetcher InitGHCollector bool + InitGHStorer bool - // Initialize Dynamic notes collector + // Initialize Dynamic notes storer and fetcher InitNotesCollector bool + InitNotesStorer bool + + StorageLocations []string models.BackendOptions } diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index 985920f..3526339 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -9,10 +9,15 @@ import ( "context" "errors" "fmt" + "os" "slices" "strings" "time" + "github.com/carabiner-dev/collector" + cgithub "github.com/carabiner-dev/collector/repository/github" + "github.com/carabiner-dev/collector/repository/note" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/timestamppb" "github.com/slsa-framework/source-tool/pkg/attest" @@ -321,3 +326,214 @@ func (t *Tool) Backend() models.VcsBackend { func (t *Tool) GetPreviousCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*models.Commit, error) { return t.backend.GetPreviousCommit(ctx, branch, commit) } + +type AttestOptions struct { + LocalPolicy string + Sign bool + OutputPath string + UseStdOut bool + Push bool +} + +type AttOpFn func(*AttestOptions) error + +func WithLocalPolicy(p string) AttOpFn { + return func(ao *AttestOptions) error { + ao.LocalPolicy = p + return nil + } +} + +func WithSign(s bool) AttOpFn { + return func(ao *AttestOptions) error { + ao.Sign = s + return nil + } +} + +func WithOutputPath(p string) AttOpFn { + return func(ao *AttestOptions) error { + ao.OutputPath = p + return nil + } +} + +func WithUseStdout(s bool) AttOpFn { + return func(ao *AttestOptions) error { + ao.UseStdOut = s + return nil + } +} + +func WithPush(s bool) AttOpFn { + return func(ao *AttestOptions) error { + ao.Push = s + return nil + } +} + +var defaultAttestOptions = AttestOptions{ + Sign: true, + UseStdOut: true, +} + +// AttestCommit checks the source control system status, the repository policy +// and generates the repository attestations (provenance & VSA). +func (t *Tool) AttestCommit(ctx context.Context, branch *models.Branch, commit *models.Commit, funcs ...AttOpFn) error { + var agent *collector.Agent + var err error + + // Initialize the attest options + opts := defaultAttestOptions + for _, f := range funcs { + if err := f(&opts); err != nil { + return err + } + } + + // Create the agent if we are pushing + if opts.Push { + agent, err = t.getAttestationStore(branch) + if err != nil { + return fmt.Errorf("unable to intitializer storate agent: %w", err) + } + } + + // Create the provenance attestation + prov, err := t.Attester().CreateSourceProvenance(ctx, branch, commit) + if err != nil { + return err + } + + // check p against policy + pe := policy.NewPolicyEvaluator() + pe.UseLocalPolicy = opts.LocalPolicy + verifiedLevels, policyPath, err := pe.EvaluateSourceProv(ctx, branch.Repository, branch, prov) + if err != nil { + return err + } + + var vsaData string + var provenanceData []byte + + // create vsa + vsaData, err = attest.CreateUnsignedSourceVsa( + branch, commit, verifiedLevels, policyPath, + ) + if err != nil { + return fmt.Errorf("creating VSA: %w", err) + } + + provenanceData, err = protojson.Marshal(prov) + if err != nil { + return fmt.Errorf("generating provenance attestation: %w", err) + } + + if opts.Sign { + provenanceDataString, err := attest.Sign(string(provenanceData)) + if err != nil { + return err + } + provenanceData = []byte(provenanceDataString) + + vsaData, err = attest.Sign(vsaData) + if err != nil { + return err + } + } + + if opts.UseStdOut { + fmt.Printf("%s\n%s\n", string(provenanceData), vsaData) + } + + fpath := opts.OutputPath + if fpath == "" { + f, err := os.CreateTemp("", "attestations-") + if err != nil { + return fmt.Errorf("opening tmp file: %w", err) + } + f.Close() //nolint:errcheck,gosec + fpath = f.Name() + } + + defer func() { + if opts.OutputPath == "" { + os.Remove(fpath) //nolint:errcheck,gosec + } + }() + + if err := os.WriteFile( + fpath, fmt.Appendf(nil, "%s\n%s\n", string(provenanceData), vsaData), os.FileMode(0o600), + ); err != nil { + return fmt.Errorf("writing attestations: %w", err) + } + + if opts.Push { + if err := agent.StoreFromFiles(ctx, []string{fpath}); err != nil { + return fmt.Errorf("pushing attestations: %w", err) + } + } + + if opts.OutputPath != "" { + err = os.WriteFile( + opts.OutputPath, fmt.Appendf(nil, "%s\n%s\n", string(provenanceData), vsaData), os.FileMode(0o600), + ) + } + return err +} + +// getAttestationStore returns a collector with storer reposistories to push +// the generated attestations. +func (t *Tool) getAttestationStore(branch *models.Branch) (*collector.Agent, error) { + if len(t.Options.StorageLocations) == 0 && !t.Options.InitGHStorer && !t.Options.InitNotesStorer { + return nil, errors.New("no storage locations defined") + } + + if t.Authenticator == nil { + return nil, errors.New("tool has no authenticator configured") + } + + token, err := t.Authenticator.ReadToken() + if err != nil { + return nil, fmt.Errorf("unable to read auth token: %w", err) + } + copts := []collector.InitFunction{} + // Init GitHub storer + if t.Options.InitGHStorer { + ghrepo, err := cgithub.New( + cgithub.WithRepo(branch.Repository.GetHttpURL()), + cgithub.WithToken(token), + ) + if err != nil { + return nil, fmt.Errorf("initializing gitrhub repo: %w", err) + } + copts = append(copts, collector.WithRepository(ghrepo)) + } + + // Init commit notes storer + if t.Options.InitNotesStorer { + notesrepo, err := note.NewDynamic( + note.WithLocator(branch.Repository.GetHttpURL()), + note.WithHttpAuth("github", token), + note.WithPush(true), + ) + if err != nil { + return nil, fmt.Errorf("initializing notes collector: %w", err) + } + copts = append(copts, collector.WithRepository(notesrepo)) + } + + // Retrurn the agent with the configured storers + c, err := collector.New(copts...) + if err != nil { + return nil, fmt.Errorf("initializing storage agent: %w", err) + } + + // Add any additional storage locations + for _, str := range t.Options.StorageLocations { + if err := c.AddRepositoryFromString(str); err != nil { + return nil, fmt.Errorf("initializing repo %q: %w", str, err) + } + } + return c, nil +} From b389ef41cd3f3e751ff6fe8e9a42834dc041511b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 11 Mar 2026 12:33:44 -0600 Subject: [PATCH 32/41] Default policy now defaults to SLSA0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/policy/policy.go | 11 ++++++++++- pkg/slsa/levels.go | 1 + pkg/slsa/slsa_types.go | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 4530bf1..584a915 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -51,7 +51,7 @@ func createDefaultBranchPolicy(branch *models.Branch) *ProtectedBranch { return &ProtectedBranch{ Name: branch.Name, Since: timestamppb.Now(), - TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), + TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel0), RequireReview: false, } } @@ -338,13 +338,22 @@ func laterTime(time1, time2 time.Time) time.Time { // Computes the time since these controls have been eligible for the level, nil if not eligible. func ComputeEligibleSince(controls *slsa.ControlSet, level slsa.SlsaSourceLevel) (*time.Time, error) { + // Get the required controls for the taget SLSA level requiredControls := slsa.GetRequiredControlsForLevel(level) var newestTime time.Time + // Range the controls and get the latest time. This is the time when + // the repo started being elegible for the target level for _, rc := range requiredControls { ac := controls.GetControl(rc) if ac == nil { + // TODO(puerco): Here we should report which controls are missing + // to inform the user somehow. return nil, nil } + + // If a control is missing it since date, then ignore it for "ElegibleSince" + // computation. Here we have a problem on how we compute since for provenance. + // See https://github.com/slsa-framework/source-tool/issues/365 since := ac.GetSince() if since == nil { continue diff --git a/pkg/slsa/levels.go b/pkg/slsa/levels.go index e89e107..b7d6c86 100644 --- a/pkg/slsa/levels.go +++ b/pkg/slsa/levels.go @@ -8,6 +8,7 @@ type ( ) const ( + SlsaSourceLevel0 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_0" SlsaSourceLevel1 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_1" SlsaSourceLevel2 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_2" SlsaSourceLevel3 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_3" diff --git a/pkg/slsa/slsa_types.go b/pkg/slsa/slsa_types.go index e46db84..f785e84 100644 --- a/pkg/slsa/slsa_types.go +++ b/pkg/slsa/slsa_types.go @@ -38,9 +38,26 @@ func IsLevelHigherOrEqualTo(level1, level2 SlsaSourceLevel) bool { // These can be any string, not just SlsaLevels type SourceVerifiedLevels []ControlName +// Levels returns the contrlols that are only SLSA levels (ignoring others) +func (svl SourceVerifiedLevels) Levels() SourceVerifiedLevels { + ret := SourceVerifiedLevels{} + for _, c := range svl { + if c == ControlName(SlsaSourceLevel0) || + c == ControlName(SlsaSourceLevel1) || + c == ControlName(SlsaSourceLevel2) || + c == ControlName(SlsaSourceLevel3) || + c == ControlName(SlsaSourceLevel4) { + ret = append(ret, c) + } + } + return ret +} + // Returns the list of control names that must be set for the given slsa level. func GetRequiredControlsForLevel(level SlsaSourceLevel) ControlNameSet { switch level { + case SlsaSourceLevel0: + return Level0 case SlsaSourceLevel1: return Level1 case SlsaSourceLevel2: From 7db8ed8a235a5016dad69d344b8391341ef6f09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 11 Mar 2026 12:41:35 -0600 Subject: [PATCH 33/41] Move checklevelprov to use tool.Attest() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/checklevelprov.go | 121 +++++++------------ pkg/sourcetool/backends/vcs/github/github.go | 2 +- pkg/sourcetool/tool.go | 38 +++--- 3 files changed, 60 insertions(+), 101 deletions(-) diff --git a/internal/cmd/checklevelprov.go b/internal/cmd/checklevelprov.go index 0869c8f..57f9da4 100644 --- a/internal/cmd/checklevelprov.go +++ b/internal/cmd/checklevelprov.go @@ -6,21 +6,17 @@ package cmd import ( "errors" "fmt" - "log" - "os" "slices" "strings" "github.com/spf13/cobra" - "google.golang.org/protobuf/encoding/protojson" - "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/policy" "github.com/slsa-framework/source-tool/pkg/sourcetool" ) type pushOptions struct { - pushLocation []string + pushLocation []string + pushRepositories []string } // Support repository types to push @@ -122,98 +118,63 @@ and pushed to its remote (--push=note). return nil }, RunE: func(cmd *cobra.Command, args []string) error { + // Here we need to translate the CLI options to the sourcetool + // options. Some of these will be deprecated as some point for + // a more concise options set. + signAttestation := false + outputPath := "" + + switch { + case opts.outputSignedBundle != "": + outputPath = opts.outputSignedBundle + signAttestation = true + case opts.outputUnsignedBundle != "": + outputPath = opts.outputUnsignedBundle + } + + var githubStorer, notesStorer, pushAttestations bool + switch { + case slices.Contains(opts.pushLocation, "github"): + pushAttestations = true + githubStorer = true + case slices.Contains(opts.pushLocation, "notes"): + pushAttestations = true + notesStorer = true + case len(opts.pushRepositories) > 0: + pushAttestations = true + } + + // Create the authenticator authenticator, err := CheckAuth() if err != nil { return err } - // Create a new sourcetool object + // Initialize sourcetool srctool, err := sourcetool.New( sourcetool.WithAuthenticator(authenticator), sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), - sourcetool.WithNotesStorer(slices.Contains(opts.pushLocation, "notes")), - sourcetool.WithGithubStorer(slices.Contains(opts.pushLocation, "github")), + sourcetool.WithNotesStorer(notesStorer), + sourcetool.WithGithubStorer(githubStorer), ) if err != nil { return fmt.Errorf("creating sourcetool: %w", err) } - // Create the provenance attestation - prov, err := srctool.Attester().CreateSourceProvenance( + // Attest the commit passing the options + verifiedLevels, err := srctool.AttestCommit( cmd.Context(), opts.GetBranch(), opts.GetCommit(), + sourcetool.WithLocalPolicy(opts.useLocalPolicy), + sourcetool.WithOutputPath(outputPath), + sourcetool.WithSign(signAttestation), + sourcetool.WithUseStdout(true), + sourcetool.WithPush(pushAttestations), ) if err != nil { - return err + return fmt.Errorf("attesting commit: %w", err) } - // check p against policy - pe := policy.NewPolicyEvaluator() - pe.UseLocalPolicy = opts.useLocalPolicy - verifiedLevels, policyPath, err := pe.EvaluateSourceProv(cmd.Context(), opts.GetRepository(), opts.GetBranch(), prov) - if err != nil { - return err - } - - // create vsa - unsignedVsa, err := attest.CreateUnsignedSourceVsa( - opts.GetBranch(), opts.GetCommit(), verifiedLevels, policyPath, - ) - if err != nil { - return err - } - - unsignedProv, err := protojson.Marshal(prov) - if err != nil { - return err - } - - // Store both the unsigned provenance and vsa - switch { - case opts.outputUnsignedBundle != "": - f, err := os.OpenFile(opts.outputUnsignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec - if err != nil { - return err - } - defer f.Close() //nolint:errcheck - - if _, err := f.WriteString(string(unsignedProv) + "\n" + unsignedVsa + "\n"); err != nil { - return fmt.Errorf("writing signed bundle: %w", err) - } - case opts.outputSignedBundle != "" || len(opts.pushLocation) > 0: - var f *os.File - if opts.outputSignedBundle != "" { - f, err = os.OpenFile(opts.outputSignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec - if err != nil { - return err - } - } - defer func() { - if f != nil { - f.Close() //nolint:errcheck,gosec - } - }() - - signedProv, err := attest.Sign(string(unsignedProv)) - if err != nil { - return err - } - - signedVsa, err := attest.Sign(unsignedVsa) - if err != nil { - return err - } - - // If a file was specified, write the attestations - if f != nil { - if _, err := f.WriteString(signedProv + "\n" + signedVsa + "\n"); err != nil { - return fmt.Errorf("writing bundle data: %w", err) - } - } - default: - log.Printf("unsigned prov: %s\n", unsignedProv) - log.Printf("unsigned vsa: %s\n", unsignedVsa) - } - fmt.Print(verifiedLevels) + fmt.Print(verifiedLevels.Levels()) return nil }, } diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index 8d75028..bae2646 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -212,7 +212,7 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models. // NewControlSet returns all the controls for the framework in // StateNotEnabled. status := slsa.NewControlSet() - sinceForever := time.Unix(1, 0) + sinceForever := time.Unix(1207836000, 0) // April 10, 2008 (when github came online) for i, ctrl := range status.Controls { // Check if it's an inherent control, turn it on and don't look back if c := InherentControls.GetControl(slsa.ControlName(ctrl.Name.String())); c != "" { diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index 3526339..d783de0 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -379,7 +379,9 @@ var defaultAttestOptions = AttestOptions{ // AttestCommit checks the source control system status, the repository policy // and generates the repository attestations (provenance & VSA). -func (t *Tool) AttestCommit(ctx context.Context, branch *models.Branch, commit *models.Commit, funcs ...AttOpFn) error { +func (t *Tool) AttestCommit( + ctx context.Context, branch *models.Branch, commit *models.Commit, funcs ...AttOpFn, +) (slsa.SourceVerifiedLevels, error) { var agent *collector.Agent var err error @@ -387,7 +389,7 @@ func (t *Tool) AttestCommit(ctx context.Context, branch *models.Branch, commit * opts := defaultAttestOptions for _, f := range funcs { if err := f(&opts); err != nil { - return err + return nil, err } } @@ -395,22 +397,23 @@ func (t *Tool) AttestCommit(ctx context.Context, branch *models.Branch, commit * if opts.Push { agent, err = t.getAttestationStore(branch) if err != nil { - return fmt.Errorf("unable to intitializer storate agent: %w", err) + return nil, fmt.Errorf("unable to intitializer storate agent: %w", err) } } - // Create the provenance attestation + // 1. Create the provenance attestation prov, err := t.Attester().CreateSourceProvenance(ctx, branch, commit) if err != nil { - return err + return nil, err } - // check p against policy + // 2. Run the provenance against the policy to determine if the controls + // are still valid. pe := policy.NewPolicyEvaluator() pe.UseLocalPolicy = opts.LocalPolicy verifiedLevels, policyPath, err := pe.EvaluateSourceProv(ctx, branch.Repository, branch, prov) if err != nil { - return err + return nil, fmt.Errorf("evaluating provenance with policy: %w", err) } var vsaData string @@ -421,24 +424,24 @@ func (t *Tool) AttestCommit(ctx context.Context, branch *models.Branch, commit * branch, commit, verifiedLevels, policyPath, ) if err != nil { - return fmt.Errorf("creating VSA: %w", err) + return nil, fmt.Errorf("creating VSA: %w", err) } provenanceData, err = protojson.Marshal(prov) if err != nil { - return fmt.Errorf("generating provenance attestation: %w", err) + return nil, fmt.Errorf("generating provenance attestation: %w", err) } if opts.Sign { provenanceDataString, err := attest.Sign(string(provenanceData)) if err != nil { - return err + return nil, err } provenanceData = []byte(provenanceDataString) vsaData, err = attest.Sign(vsaData) if err != nil { - return err + return nil, err } } @@ -450,7 +453,7 @@ func (t *Tool) AttestCommit(ctx context.Context, branch *models.Branch, commit * if fpath == "" { f, err := os.CreateTemp("", "attestations-") if err != nil { - return fmt.Errorf("opening tmp file: %w", err) + return nil, fmt.Errorf("opening tmp file: %w", err) } f.Close() //nolint:errcheck,gosec fpath = f.Name() @@ -465,21 +468,16 @@ func (t *Tool) AttestCommit(ctx context.Context, branch *models.Branch, commit * if err := os.WriteFile( fpath, fmt.Appendf(nil, "%s\n%s\n", string(provenanceData), vsaData), os.FileMode(0o600), ); err != nil { - return fmt.Errorf("writing attestations: %w", err) + return nil, fmt.Errorf("writing attestations: %w", err) } if opts.Push { if err := agent.StoreFromFiles(ctx, []string{fpath}); err != nil { - return fmt.Errorf("pushing attestations: %w", err) + return nil, fmt.Errorf("pushing attestations: %w", err) } } - if opts.OutputPath != "" { - err = os.WriteFile( - opts.OutputPath, fmt.Appendf(nil, "%s\n%s\n", string(provenanceData), vsaData), os.FileMode(0o600), - ) - } - return err + return verifiedLevels, nil } // getAttestationStore returns a collector with storer reposistories to push From 47ac14a0b194502ef1ff2a0037082867ae35268c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 11 Mar 2026 12:58:10 -0600 Subject: [PATCH 34/41] Models: Add Reference interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/sourcetool/models/models.go | 40 +++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index 9879bab..dc25ec1 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -74,6 +74,18 @@ type Commit struct { Message string } +// Both tags and branches must implement the reference interface +var ( + _ Reference = (*Branch)(nil) + _ Reference = (*Tag)(nil) +) + +type Reference interface { + FullRef() string + GetRepository() *Repository + GetName() string +} + func (c *Commit) ToResourceDescriptor() *attestation.ResourceDescriptor { return &attestation.ResourceDescriptor{ Digest: map[string]string{ @@ -87,6 +99,14 @@ type Branch struct { Repository *Repository } +func (b *Branch) GetRepository() *Repository { + return b.Repository +} + +func (b *Branch) GetName() string { + return b.Name +} + func (b *Branch) FullRef() string { return fmt.Sprintf("refs/heads/%s", b.Name) } @@ -125,8 +145,24 @@ func (r *Repository) PathAsGitHubOwnerName() (owner, name string, err error) { } type Tag struct { - Name string - Commit *Commit + Name string + Commit *Commit + Repository *Repository +} + +func (t *Tag) GetName() string { + return t.Name +} + +func (t *Tag) GetRepository() *Repository { + return t.Repository +} + +func (t *Tag) FullRef() string { + if t.Name == "" { + return "" + } + return "refs/tags/" + t.Name } // PullRequest models a GitHub pull request. From d9e4f4063293f8856dbfe2c9694abb7567860632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 11 Mar 2026 13:11:20 -0600 Subject: [PATCH 35/41] Add revision interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/sourcetool/models/models.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index dc25ec1..846fccb 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -78,14 +78,24 @@ type Commit struct { var ( _ Reference = (*Branch)(nil) _ Reference = (*Tag)(nil) + _ Revision = (*Tag)(nil) + _ Revision = (*Commit)(nil) ) +type Revision interface { + GetCommit() *Commit +} + type Reference interface { FullRef() string GetRepository() *Repository GetName() string } +func (c *Commit) GetCommit() *Commit { + return c +} + func (c *Commit) ToResourceDescriptor() *attestation.ResourceDescriptor { return &attestation.ResourceDescriptor{ Digest: map[string]string{ @@ -158,6 +168,10 @@ func (t *Tag) GetRepository() *Repository { return t.Repository } +func (t *Tag) GetCommit() *Commit { + return t.Commit +} + func (t *Tag) FullRef() string { if t.Name == "" { return "" From bea844ba39eb6dd7c9031cd2b5785029d19ef27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 11 Mar 2026 13:21:42 -0600 Subject: [PATCH 36/41] Add GetDefaultBranch method to backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/sourcetool/backends/vcs/github/github.go | 17 ++++ pkg/sourcetool/models/models.go | 1 + .../models/modelsfakes/fake_vcs_backend.go | 79 +++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index bae2646..d0fa709 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -411,3 +411,20 @@ func (b *Backend) GetPreviousCommit(ctx context.Context, branch *models.Branch, SHA: rawCommit, }, nil } + +// GetDefaultBranch returns the default branch +func (b *Backend) GetDefaultBranch(ctx context.Context, repo *models.Repository) (*models.Branch, error) { + ghx, err := b.getGitHubConnection(repo, "") + if err != nil { + return nil, err + } + branchName, err := ghx.GetDefaultBranch(ctx) + if err != nil { + return nil, fmt.Errorf("fetching default branch: %w", err) + } + + return &models.Branch{ + Name: branchName, + Repository: repo, + }, nil +} diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index 846fccb..8b5a18a 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -47,6 +47,7 @@ type VcsBackend interface { GetLatestCommit(context.Context, *Repository, *Branch) (*Commit, error) ControlPrecheck(*Repository, []*Branch, ControlConfiguration) (bool, string, ControlPreRemediationFn, error) GetPreviousCommit(context.Context, *Branch, *Commit) (*Commit, error) + GetDefaultBranch(context.Context, *Repository) (*Branch, error) } type BackendOptions struct { diff --git a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go index ed678ac..2201135 100644 --- a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go +++ b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go @@ -83,6 +83,20 @@ type FakeVcsBackend struct { result1 *slsa.ControlSet result2 error } + GetDefaultBranchStub func(context.Context, *models.Repository) (*models.Branch, error) + getDefaultBranchMutex sync.RWMutex + getDefaultBranchArgsForCall []struct { + arg1 context.Context + arg2 *models.Repository + } + getDefaultBranchReturns struct { + result1 *models.Branch + result2 error + } + getDefaultBranchReturnsOnCall map[int]struct { + result1 *models.Branch + result2 error + } GetLatestCommitStub func(context.Context, *models.Repository, *models.Branch) (*models.Commit, error) getLatestCommitMutex sync.RWMutex getLatestCommitArgsForCall []struct { @@ -474,6 +488,71 @@ func (fake *FakeVcsBackend) GetBranchControlsAtCommitReturnsOnCall(i int, result }{result1, result2} } +func (fake *FakeVcsBackend) GetDefaultBranch(arg1 context.Context, arg2 *models.Repository) (*models.Branch, error) { + fake.getDefaultBranchMutex.Lock() + ret, specificReturn := fake.getDefaultBranchReturnsOnCall[len(fake.getDefaultBranchArgsForCall)] + fake.getDefaultBranchArgsForCall = append(fake.getDefaultBranchArgsForCall, struct { + arg1 context.Context + arg2 *models.Repository + }{arg1, arg2}) + stub := fake.GetDefaultBranchStub + fakeReturns := fake.getDefaultBranchReturns + fake.recordInvocation("GetDefaultBranch", []interface{}{arg1, arg2}) + fake.getDefaultBranchMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVcsBackend) GetDefaultBranchCallCount() int { + fake.getDefaultBranchMutex.RLock() + defer fake.getDefaultBranchMutex.RUnlock() + return len(fake.getDefaultBranchArgsForCall) +} + +func (fake *FakeVcsBackend) GetDefaultBranchCalls(stub func(context.Context, *models.Repository) (*models.Branch, error)) { + fake.getDefaultBranchMutex.Lock() + defer fake.getDefaultBranchMutex.Unlock() + fake.GetDefaultBranchStub = stub +} + +func (fake *FakeVcsBackend) GetDefaultBranchArgsForCall(i int) (context.Context, *models.Repository) { + fake.getDefaultBranchMutex.RLock() + defer fake.getDefaultBranchMutex.RUnlock() + argsForCall := fake.getDefaultBranchArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeVcsBackend) GetDefaultBranchReturns(result1 *models.Branch, result2 error) { + fake.getDefaultBranchMutex.Lock() + defer fake.getDefaultBranchMutex.Unlock() + fake.GetDefaultBranchStub = nil + fake.getDefaultBranchReturns = struct { + result1 *models.Branch + result2 error + }{result1, result2} +} + +func (fake *FakeVcsBackend) GetDefaultBranchReturnsOnCall(i int, result1 *models.Branch, result2 error) { + fake.getDefaultBranchMutex.Lock() + defer fake.getDefaultBranchMutex.Unlock() + fake.GetDefaultBranchStub = nil + if fake.getDefaultBranchReturnsOnCall == nil { + fake.getDefaultBranchReturnsOnCall = make(map[int]struct { + result1 *models.Branch + result2 error + }) + } + fake.getDefaultBranchReturnsOnCall[i] = struct { + result1 *models.Branch + result2 error + }{result1, result2} +} + func (fake *FakeVcsBackend) GetLatestCommit(arg1 context.Context, arg2 *models.Repository, arg3 *models.Branch) (*models.Commit, error) { fake.getLatestCommitMutex.Lock() ret, specificReturn := fake.getLatestCommitReturnsOnCall[len(fake.getLatestCommitArgsForCall)] From f24ad489020ebb6b5e489e728484d8b663da7f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 11 Mar 2026 15:13:09 -0600 Subject: [PATCH 37/41] Add TagCommit fetch to backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/ghcontrol/connection.go | 32 ++++++++ pkg/sourcetool/backends/vcs/github/github.go | 24 ++++++ pkg/sourcetool/models/models.go | 1 + .../models/modelsfakes/fake_vcs_backend.go | 81 +++++++++++++++++++ 4 files changed, 138 insertions(+) diff --git a/pkg/ghcontrol/connection.go b/pkg/ghcontrol/connection.go index 7a19b84..8d66dba 100644 --- a/pkg/ghcontrol/connection.go +++ b/pkg/ghcontrol/connection.go @@ -10,6 +10,8 @@ import ( "github.com/google/go-github/v69/github" "github.com/hashicorp/go-retryablehttp" + + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) const tokenEnvVar = "GITHUB_TOKEN" //nolint:gosec // These are not credentials @@ -117,3 +119,33 @@ func (ghc *GitHubConnection) GetDefaultBranch(ctx context.Context) (string, erro return repo.GetDefaultBranch(), nil } + +func (ghc *GitHubConnection) GetTagCommit(ctx context.Context, tagName string) (*models.Commit, error) { + ref, _, err := ghc.Client().Git.GetRef(ctx, ghc.owner, ghc.repo, "tags/"+tagName) + if err != nil { + return nil, fmt.Errorf("fetching ref %s: %w", ghc.ref, err) + } + + obj := ref.GetObject() + + // If the ref points directly to a commit, return it + if obj.GetType() == "commit" { + return &models.Commit{ + SHA: obj.GetSHA(), + }, nil + } + + // For annotated tags, the ref points to a tag object. + // Dereference it to get the commit. + tag, _, err := ghc.Client().Git.GetTag(ctx, ghc.owner, ghc.repo, obj.GetSHA()) + if err != nil { + return nil, fmt.Errorf("fetching tag object %s: %w", obj.GetSHA(), err) + } + + return &models.Commit{ + SHA: tag.GetObject().GetSHA(), + Author: tag.GetTagger().GetLogin(), + Time: tag.GetTagger().Date.GetTime(), + Message: tag.GetMessage(), + }, nil +} diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index d0fa709..4f1aba5 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -428,3 +428,27 @@ func (b *Backend) GetDefaultBranch(ctx context.Context, repo *models.Repository) Repository: repo, }, nil } + +// GetRevisionCommit returns the commit of a revision (or error) +func (b *Backend) GetRevisionCommit(ctx context.Context, repo *models.Repository, rev models.Revision) (*models.Commit, error) { + switch inst := rev.(type) { + case *models.Commit: + return inst, nil + case *models.Tag: + if inst.Commit == nil { + ghx, err := b.getGitHubConnection(repo, "") + if err != nil { + return nil, err + } + tagCommit, err := ghx.GetTagCommit(ctx, inst.GetName()) + if err != nil { + return nil, err + } + + inst.Commit = tagCommit + } + return inst.Commit, nil + default: + return nil, errors.New("not implemented yet or invalid revision") + } +} diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index 8b5a18a..a2d6277 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -48,6 +48,7 @@ type VcsBackend interface { ControlPrecheck(*Repository, []*Branch, ControlConfiguration) (bool, string, ControlPreRemediationFn, error) GetPreviousCommit(context.Context, *Branch, *Commit) (*Commit, error) GetDefaultBranch(context.Context, *Repository) (*Branch, error) + GetRevisionCommit(context.Context, *Repository, Revision) (*Commit, error) } type BackendOptions struct { diff --git a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go index 2201135..c4ec68c 100644 --- a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go +++ b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go @@ -127,6 +127,21 @@ type FakeVcsBackend struct { result1 *models.Commit result2 error } + GetRevisionCommitStub func(context.Context, *models.Repository, models.Revision) (*models.Commit, error) + getRevisionCommitMutex sync.RWMutex + getRevisionCommitArgsForCall []struct { + arg1 context.Context + arg2 *models.Repository + arg3 models.Revision + } + getRevisionCommitReturns struct { + result1 *models.Commit + result2 error + } + getRevisionCommitReturnsOnCall map[int]struct { + result1 *models.Commit + result2 error + } GetTagControlsStub func(context.Context, *models.Tag) (*slsa.ControlSet, error) getTagControlsMutex sync.RWMutex getTagControlsArgsForCall []struct { @@ -685,6 +700,72 @@ func (fake *FakeVcsBackend) GetPreviousCommitReturnsOnCall(i int, result1 *model }{result1, result2} } +func (fake *FakeVcsBackend) GetRevisionCommit(arg1 context.Context, arg2 *models.Repository, arg3 models.Revision) (*models.Commit, error) { + fake.getRevisionCommitMutex.Lock() + ret, specificReturn := fake.getRevisionCommitReturnsOnCall[len(fake.getRevisionCommitArgsForCall)] + fake.getRevisionCommitArgsForCall = append(fake.getRevisionCommitArgsForCall, struct { + arg1 context.Context + arg2 *models.Repository + arg3 models.Revision + }{arg1, arg2, arg3}) + stub := fake.GetRevisionCommitStub + fakeReturns := fake.getRevisionCommitReturns + fake.recordInvocation("GetRevisionCommit", []interface{}{arg1, arg2, arg3}) + fake.getRevisionCommitMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVcsBackend) GetRevisionCommitCallCount() int { + fake.getRevisionCommitMutex.RLock() + defer fake.getRevisionCommitMutex.RUnlock() + return len(fake.getRevisionCommitArgsForCall) +} + +func (fake *FakeVcsBackend) GetRevisionCommitCalls(stub func(context.Context, *models.Repository, models.Revision) (*models.Commit, error)) { + fake.getRevisionCommitMutex.Lock() + defer fake.getRevisionCommitMutex.Unlock() + fake.GetRevisionCommitStub = stub +} + +func (fake *FakeVcsBackend) GetRevisionCommitArgsForCall(i int) (context.Context, *models.Repository, models.Revision) { + fake.getRevisionCommitMutex.RLock() + defer fake.getRevisionCommitMutex.RUnlock() + argsForCall := fake.getRevisionCommitArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVcsBackend) GetRevisionCommitReturns(result1 *models.Commit, result2 error) { + fake.getRevisionCommitMutex.Lock() + defer fake.getRevisionCommitMutex.Unlock() + fake.GetRevisionCommitStub = nil + fake.getRevisionCommitReturns = struct { + result1 *models.Commit + result2 error + }{result1, result2} +} + +func (fake *FakeVcsBackend) GetRevisionCommitReturnsOnCall(i int, result1 *models.Commit, result2 error) { + fake.getRevisionCommitMutex.Lock() + defer fake.getRevisionCommitMutex.Unlock() + fake.GetRevisionCommitStub = nil + if fake.getRevisionCommitReturnsOnCall == nil { + fake.getRevisionCommitReturnsOnCall = make(map[int]struct { + result1 *models.Commit + result2 error + }) + } + fake.getRevisionCommitReturnsOnCall[i] = struct { + result1 *models.Commit + result2 error + }{result1, result2} +} + func (fake *FakeVcsBackend) GetTagControls(arg1 context.Context, arg2 *models.Tag) (*slsa.ControlSet, error) { fake.getTagControlsMutex.Lock() ret, specificReturn := fake.getTagControlsReturnsOnCall[len(fake.getTagControlsArgsForCall)] From 203cc3b442a1be95c3e6ac932d21cebcadedcf25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 11 Mar 2026 15:13:39 -0600 Subject: [PATCH 38/41] Fix verifycommit to work with any revision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/options.go | 145 ++++++++++++++++++++++++++++++++--- internal/cmd/verifycommit.go | 9 +-- pkg/attest/provenance.go | 6 +- 3 files changed, 140 insertions(+), 20 deletions(-) diff --git a/internal/cmd/options.go b/internal/cmd/options.go index 8c10237..3922f7a 100644 --- a/internal/cmd/options.go +++ b/internal/cmd/options.go @@ -14,6 +14,7 @@ import ( "github.com/slsa-framework/source-tool/pkg/auth" "github.com/slsa-framework/source-tool/pkg/ghcontrol" + "github.com/slsa-framework/source-tool/pkg/sourcetool" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) @@ -123,21 +124,19 @@ func (bo *branchOptions) EnsureDefaults() error { return nil } - t := githubToken - var err error - if t == "" { - t, err = auth.New().ReadToken() - if err != nil { - return err - } + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(auth.New()), + ) + if err != nil { + return err } - gcx := ghcontrol.NewGhConnection(bo.owner, bo.repository, "").WithAuthToken(t) - branch, err := gcx.GetDefaultBranch(context.Background()) + branch, err := srctool.Backend().GetDefaultBranch(context.Background(), bo.GetRepository()) if err != nil { - return fmt.Errorf("reading repository default branch: %w", err) + return fmt.Errorf("getting default branch: %w", err) } - bo.branch = branch + bo.branch = branch.GetName() return nil } @@ -229,3 +228,127 @@ func (vo *verifierOptions) AddFlags(cmd *cobra.Command) { cmd.PersistentFlags().StringVar(&vo.expectedIssuer, "expected_issuer", "", "The expected issuer of the attestation signer certificate") cmd.PersistentFlags().StringVar(&vo.expectedSan, "expected_san", "", "The expected SAN string in the attestation signer certificate") } + +type tagOptions struct { + branchOptions + tag string +} + +func (to *tagOptions) Validate() error { + return nil +} + +func (to *tagOptions) AddFlags(cmd *cobra.Command) { + cmd.PersistentFlags().StringVar(&to.tag, "tag", "", "Git tag within the repository") +} + +type revisionOpts struct { + repoOptions + tagOptions + branchOptions + commitOptions +} + +func (ro *revisionOpts) Validate() error { + errs := []error{} + if ro.tag == "" && ro.commit == "" { + errs = append(errs, errors.New("unable to compute revision: no commit or tag set")) + } + return errors.Join(errs...) +} + +func (ro *revisionOpts) AddFlags(cmd *cobra.Command) { + ro.branchOptions.AddFlags(cmd) + ro.tagOptions.AddFlags(cmd) + ro.repoOptions.AddFlags(cmd) +} + +func (ro *revisionOpts) GetRevision() models.Revision { + if ro.tag != "" { + return &models.Tag{ + Name: ro.tag, + Commit: &models.Commit{}, + Repository: &models.Repository{}, + } + } + return nil +} + +func (ro *revisionOpts) ParseLocator(lString string) error { + if err := ro.branchOptions.ParseLocator(lString); err != nil { + return err + } + components, err := vcslocator.Locator(lString).Parse() + if err != nil { + return fmt.Errorf("parsing repository slug: %w", err) + } + + if err := ro.ParseSlug(components.RepoPath); err != nil { + return err + } + + if components.Commit != "" { + ro.commit = components.Commit + } + + if components.Tag != "" { + ro.tag = components.Tag + } + + if components.Branch != "" { + ro.tag = components.Branch + } + + return nil +} + +func (ro *revisionOpts) EnsureDefaults() error { + if err := ro.branchOptions.EnsureDefaults(); err != nil { + return err + } + + if ro.tag != "" { + ro.tagOptions.branchOptions = ro.branchOptions + + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(auth.New()), + ) + if err != nil { + return err + } + + commit, err := srctool.Backend().GetRevisionCommit(context.Background(), ro.GetRepository(), &models.Tag{ + Name: ro.tag, + }) + if err != nil { + return err + } + + ro.commit = commit.SHA + } + + if ro.commit != "" { + ro.commitOptions.branchOptions = ro.branchOptions + if err := ro.commitOptions.EnsureDefaults(); err != nil { + return err + } + } + + // If no tag and not commit where specified, get the commit at the branch tip + if ro.commit == "" && ro.tag == "" { + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(auth.New()), + ) + if err != nil { + return err + } + + commit, err := srctool.Backend().GetLatestCommit(context.Background(), ro.GetRepository(), ro.GetBranch()) + if err != nil { + return err + } + ro.commit = commit.SHA + } + + return nil +} diff --git a/internal/cmd/verifycommit.go b/internal/cmd/verifycommit.go index 0e6981f..2342f20 100644 --- a/internal/cmd/verifycommit.go +++ b/internal/cmd/verifycommit.go @@ -13,10 +13,9 @@ import ( ) type verifyCommitOptions struct { - commitOptions verifierOptions outputOptions - tag string + revisionOpts } // VerifyCommitResult represents the result of a commit verification @@ -41,7 +40,7 @@ func (v VerifyCommitResult) String() string { func (vco *verifyCommitOptions) Validate() error { errs := []error{ - vco.commitOptions.Validate(), + vco.revisionOpts.Validate(), vco.verifierOptions.Validate(), vco.outputOptions.Validate(), } @@ -49,12 +48,10 @@ func (vco *verifyCommitOptions) Validate() error { } func (vco *verifyCommitOptions) AddFlags(cmd *cobra.Command) { + vco.tagOptions.AddFlags(cmd) vco.commitOptions.AddFlags(cmd) vco.verifierOptions.AddFlags(cmd) vco.outputOptions.AddFlags(cmd) - cmd.PersistentFlags().StringVar( - &vco.tag, "tag", "", "The tag within the repository", - ) } func addVerifyCommit(cmd *cobra.Command) { diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index dd7897d..12199ab 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -134,8 +134,8 @@ func (a *Attester) getCollector(branch *models.Branch) (*collector.Agent, error) } // GetRevisionVSA returns a revision's VSA attestation -func (a *Attester) GetRevisionVSA(ctx context.Context, branch *models.Branch, commit *models.Commit) (attestation.Envelope, *vsa.VerificationSummary, error) { - if commit == nil { +func (a *Attester) GetRevisionVSA(ctx context.Context, branch *models.Branch, revision models.Revision) (attestation.Envelope, *vsa.VerificationSummary, error) { + if revision.GetCommit() == nil { return nil, nil, errors.New("commit is nil") } c, err := a.getCollector(branch) @@ -155,7 +155,7 @@ func (a *Attester) GetRevisionVSA(ctx context.Context, branch *models.Branch, co for i := 0; i <= int(a.Options.Retries); i++ { // Fetch the attestations from the configured repos atts, attErr = c.FetchAttestationsBySubject( - ctx, []attestation.Subject{commit.ToResourceDescriptor()}, + ctx, []attestation.Subject{revision.GetCommit().ToResourceDescriptor()}, collector.WithQuery(attestation.NewQuery().WithFilter(matcher)), ) if attErr == nil { From 1835fb61206f9795e9a942fe0059d1dac732727d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 11 Mar 2026 16:06:13 -0600 Subject: [PATCH 39/41] Make aloow-merge-comm reusable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/checklevel.go | 4 ++-- internal/cmd/checklevelprov.go | 4 ++-- internal/cmd/options.go | 9 +++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/internal/cmd/checklevel.go b/internal/cmd/checklevel.go index 6bef0bb..1a8aab0 100644 --- a/internal/cmd/checklevel.go +++ b/internal/cmd/checklevel.go @@ -19,8 +19,8 @@ import ( type checkLevelOpts struct { commitOptions + allowMergeCommitsOptions outputVsa, outputUnsignedVsa, useLocalPolicy string - allowMergeCommits bool } func (clo *checkLevelOpts) Validate() error { @@ -33,10 +33,10 @@ func (clo *checkLevelOpts) Validate() error { func (clo *checkLevelOpts) AddFlags(cmd *cobra.Command) { clo.commitOptions.AddFlags(cmd) + clo.allowMergeCommitsOptions.AddFlags(cmd) cmd.PersistentFlags().StringVar(&clo.outputVsa, "output_vsa", "", "The path to write a signed VSA with the determined level.") cmd.PersistentFlags().StringVar(&clo.outputUnsignedVsa, "output_unsigned_vsa", "", "The path to write an unsigned vsa with the determined level.") cmd.PersistentFlags().StringVar(&clo.useLocalPolicy, "use_local_policy", "", "UNSAFE: Use the policy at this local path instead of the official one.") - cmd.PersistentFlags().BoolVar(&clo.allowMergeCommits, "allow-merge-commits", false, "[EXPERIMENTAL] Allow merge commits in branch.") } func addCheckLevel(parentCmd *cobra.Command) { diff --git a/internal/cmd/checklevelprov.go b/internal/cmd/checklevelprov.go index 57f9da4..483b1b2 100644 --- a/internal/cmd/checklevelprov.go +++ b/internal/cmd/checklevelprov.go @@ -54,12 +54,12 @@ type checkLevelProvOpts struct { commitOptions verifierOptions pushOptions + allowMergeCommitsOptions prevBundlePath string prevCommit string outputUnsignedBundle string outputSignedBundle string useLocalPolicy string - allowMergeCommits bool } func (clp *checkLevelProvOpts) Validate() error { @@ -72,12 +72,12 @@ func (clp *checkLevelProvOpts) Validate() error { func (clp *checkLevelProvOpts) AddFlags(cmd *cobra.Command) { clp.commitOptions.AddFlags(cmd) clp.pushOptions.AddFlags(cmd) + clp.allowMergeCommitsOptions.AddFlags(cmd) cmd.PersistentFlags().StringVar(&clp.prevBundlePath, "prev_bundle_path", "", "Path to the file with the attestations for the previous commit (as an in-toto bundle).") cmd.PersistentFlags().StringVar(&clp.prevCommit, "prev_commit", "", "The commit to check.") cmd.PersistentFlags().StringVar(&clp.outputUnsignedBundle, "output_unsigned_bundle", "", "The path to write a bundle of unsigned attestations.") cmd.PersistentFlags().StringVar(&clp.outputSignedBundle, "output_signed_bundle", "", "The path to write a bundle of signed attestations.") cmd.PersistentFlags().StringVar(&clp.useLocalPolicy, "use_local_policy", "", "UNSAFE: Use the policy at this local path instead of the official one.") - cmd.PersistentFlags().BoolVar(&clp.allowMergeCommits, "allow-merge-commits", false, "[EXPERIMENTAL] Allow merge commits in branch") } func addCheckLevelProv(parentCmd *cobra.Command) { diff --git a/internal/cmd/options.go b/internal/cmd/options.go index 3922f7a..0486abd 100644 --- a/internal/cmd/options.go +++ b/internal/cmd/options.go @@ -352,3 +352,12 @@ func (ro *revisionOpts) EnsureDefaults() error { return nil } + +type allowMergeCommitsOptions struct { + allowMergeCommits bool +} + +// AddFlags adds the subcommands flags +func (o *allowMergeCommitsOptions) AddFlags(cmd *cobra.Command) { + cmd.PersistentFlags().BoolVar(&o.allowMergeCommits, "allow-merge-commits", false, "[EXPERIMENTAL] Allow merge commits in branch.") +} From e206c7317ae6c406a3bae1fe1838f90f91a4d6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 11 Mar 2026 16:33:21 -0600 Subject: [PATCH 40/41] Use revision opts flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/checklevel.go | 4 ++-- internal/cmd/checklevelprov.go | 6 +++--- internal/cmd/options.go | 7 ++++++- internal/cmd/prov.go | 10 ++++++---- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/internal/cmd/checklevel.go b/internal/cmd/checklevel.go index 1a8aab0..a6ea4de 100644 --- a/internal/cmd/checklevel.go +++ b/internal/cmd/checklevel.go @@ -18,14 +18,14 @@ import ( ) type checkLevelOpts struct { - commitOptions + revisionOpts allowMergeCommitsOptions outputVsa, outputUnsignedVsa, useLocalPolicy string } func (clo *checkLevelOpts) Validate() error { errs := []error{ - clo.commitOptions.Validate(), + clo.revisionOpts.Validate(), } return errors.Join(errs...) diff --git a/internal/cmd/checklevelprov.go b/internal/cmd/checklevelprov.go index 483b1b2..e76e515 100644 --- a/internal/cmd/checklevelprov.go +++ b/internal/cmd/checklevelprov.go @@ -51,7 +51,7 @@ func (po *pushOptions) AddFlags(cmd *cobra.Command) { } type checkLevelProvOpts struct { - commitOptions + revisionOpts verifierOptions pushOptions allowMergeCommitsOptions @@ -64,13 +64,13 @@ type checkLevelProvOpts struct { func (clp *checkLevelProvOpts) Validate() error { return errors.Join([]error{ - clp.commitOptions.Validate(), + clp.revisionOpts.Validate(), clp.verifierOptions.Validate(), }...) } func (clp *checkLevelProvOpts) AddFlags(cmd *cobra.Command) { - clp.commitOptions.AddFlags(cmd) + clp.revisionOpts.AddFlags(cmd) clp.pushOptions.AddFlags(cmd) clp.allowMergeCommitsOptions.AddFlags(cmd) cmd.PersistentFlags().StringVar(&clp.prevBundlePath, "prev_bundle_path", "", "Path to the file with the attestations for the previous commit (as an in-toto bundle).") diff --git a/internal/cmd/options.go b/internal/cmd/options.go index 0486abd..98114c8 100644 --- a/internal/cmd/options.go +++ b/internal/cmd/options.go @@ -258,9 +258,14 @@ func (ro *revisionOpts) Validate() error { } func (ro *revisionOpts) AddFlags(cmd *cobra.Command) { + // branchOptions.AddFlags already registers repo, owner, and branch flags ro.branchOptions.AddFlags(cmd) + // Only register tag; tagOptions.AddFlags doesn't chain to branchOptions ro.tagOptions.AddFlags(cmd) - ro.repoOptions.AddFlags(cmd) + // Register commit flag directly to avoid re-registering branch/repo flags + cmd.PersistentFlags().StringVarP( + &ro.commit, "commit", "c", "", "commit digest (sha1)", + ) } func (ro *revisionOpts) GetRevision() models.Revision { diff --git a/internal/cmd/prov.go b/internal/cmd/prov.go index 9a85f92..0bd1e47 100644 --- a/internal/cmd/prov.go +++ b/internal/cmd/prov.go @@ -14,22 +14,23 @@ import ( ) type provOptions struct { - commitOptions + revisionOpts verifierOptions + allowMergeCommitsOptions prevAttPath, prevCommit string } func (po *provOptions) Validate() error { return errors.Join([]error{ - po.commitOptions.Validate(), + po.revisionOpts.Validate(), po.verifierOptions.Validate(), }...) } func (po *provOptions) AddFlags(cmd *cobra.Command) { - po.commitOptions.AddFlags(cmd) + po.revisionOpts.AddFlags(cmd) po.verifierOptions.AddFlags(cmd) - + po.allowMergeCommitsOptions.AddFlags(cmd) cmd.PersistentFlags().StringVar(&po.prevAttPath, "prev_att_path", "", "Path to the file with the attestations for the previous commit (as an in-toto bundle).") cmd.PersistentFlags().StringVar(&po.prevCommit, "prev_commit", "", "The commit prior to 'commit'.") } @@ -73,6 +74,7 @@ func addProv(parentCmd *cobra.Command) { // Create a new sourcetool object srctool, err := sourcetool.New( sourcetool.WithAuthenticator(authenticator), + sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), ) if err != nil { return err From 6d092d4a2566ecd0a222ced322cc350fb925a7b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 11 Mar 2026 19:21:06 -0600 Subject: [PATCH 41/41] Finish the tag controls implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/checklevelprov.go | 4 +- internal/cmd/checktag.go | 150 +++++++++--------- internal/cmd/options.go | 6 +- pkg/attest/provenance.go | 2 +- pkg/sourcetool/backends/vcs/github/github.go | 7 +- pkg/sourcetool/models/models.go | 2 +- .../models/modelsfakes/fake_vcs_backend.go | 22 +-- pkg/sourcetool/tool.go | 77 ++++++--- 8 files changed, 155 insertions(+), 115 deletions(-) diff --git a/internal/cmd/checklevelprov.go b/internal/cmd/checklevelprov.go index e76e515..d47bf17 100644 --- a/internal/cmd/checklevelprov.go +++ b/internal/cmd/checklevelprov.go @@ -162,8 +162,8 @@ and pushed to its remote (--push=note). } // Attest the commit passing the options - verifiedLevels, err := srctool.AttestCommit( - cmd.Context(), opts.GetBranch(), opts.GetCommit(), + verifiedLevels, err := srctool.AttestRevision( + cmd.Context(), opts.GetBranch(), opts.GetRevision(), sourcetool.WithLocalPolicy(opts.useLocalPolicy), sourcetool.WithOutputPath(outputPath), sourcetool.WithSign(signAttestation), diff --git a/internal/cmd/checktag.go b/internal/cmd/checktag.go index 116c2a1..057e03c 100644 --- a/internal/cmd/checktag.go +++ b/internal/cmd/checktag.go @@ -6,56 +6,46 @@ package cmd import ( "errors" "fmt" - "log" - "os" + "slices" "github.com/spf13/cobra" - "google.golang.org/protobuf/encoding/protojson" - "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/policy" "github.com/slsa-framework/source-tool/pkg/sourcetool" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type checkTagOptions struct { - repoOptions verifierOptions - commitOptions - tagName string - actor string - outputSignedBundle string - useLocalPolicy string - vsaRetries uint8 + revisionOpts + pushOptions + allowMergeCommitsOptions + actor string + outputSignedBundle string + outputUnsignedBundle string + useLocalPolicy string + vsaRetries uint8 } func (cto *checkTagOptions) Validate() error { errs := []error{ - cto.commitOptions.Validate(), - cto.repoOptions.Validate(), + cto.revisionOpts.Validate(), cto.verifierOptions.Validate(), } return errors.Join(errs...) } func (cto *checkTagOptions) AddFlags(cmd *cobra.Command) { - cto.repoOptions.AddFlags(cmd) + cto.revisionOpts.AddFlags(cmd) cto.verifierOptions.AddFlags(cmd) - cmd.PersistentFlags().StringVar(&cto.commit, "commit", "", "The commit to check - required.") - cmd.PersistentFlags().StringVar(&cto.tagName, "tag_name", "", "The name of the new tag - required.") + cto.pushOptions.AddFlags(cmd) + cto.allowMergeCommitsOptions.AddFlags(cmd) cmd.PersistentFlags().StringVar(&cto.actor, "actor", "", "The username of the actor that pushed the tag.") cmd.PersistentFlags().StringVar(&cto.outputSignedBundle, "output_signed_bundle", "", "The path to write a bundle of signed attestations.") + cmd.PersistentFlags().StringVar(&cto.outputUnsignedBundle, "output_unsigned_bundle", "", "The path to write a bundle of unsigned attestations.") cmd.PersistentFlags().StringVar(&cto.useLocalPolicy, "use_local_policy", "", "UNSAFE: Use the policy at this local path instead of the official one.") cmd.PersistentFlags().Uint8Var(&cto.vsaRetries, "retries", 3, "Number of times to retry fetching the commit's VSA") } -func (cto *checkTagOptions) GetTag() *models.Tag { - return &models.Tag{ - Name: cto.tagName, - Commit: cto.GetCommit(), - } -} - func addCheckTag(parentCmd *cobra.Command) { opts := &checkTagOptions{} @@ -63,74 +53,88 @@ func addCheckTag(parentCmd *cobra.Command) { Use: "checktag", GroupID: "assessment", Short: "Checks to see if the tag operation should be allowed and issues a VSA", - RunE: func(cmd *cobra.Command, args []string) error { - authenticator, err := CheckAuth() - if err != nil { + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + if err := opts.ParseLocator(args[0]); err != nil { + return err + } + } + + if err := opts.repoOptions.Validate(); err != nil { return err } - // Create a new sourcetool object - srctool, err := sourcetool.New( - sourcetool.WithAuthenticator(authenticator), - ) - if err != nil { + if err := opts.EnsureDefaults(); err != nil { return err } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + // Here we need to translate the CLI options to the sourcetool + // options. Some of these will be deprecated as some point for + // a more concise options set. + signAttestation := false + outputPath := "" + + switch { + case opts.outputSignedBundle != "": + outputPath = opts.outputSignedBundle + signAttestation = true + case opts.outputUnsignedBundle != "": + outputPath = opts.outputUnsignedBundle + } - // Create tag provenance. - // pa.Options.VsaRetries = opts.vsaRetries // Retry fetching the commit's VSA + var githubStorer, notesStorer, pushAttestations bool + switch { + case slices.Contains(opts.pushLocation, "github"): + pushAttestations = true + githubStorer = true + case slices.Contains(opts.pushLocation, "notes"): + pushAttestations = true + notesStorer = true + case len(opts.pushRepositories) > 0: + pushAttestations = true + } - prov, err := srctool.Attester().CreateTagProvenance(cmd.Context(), opts.GetBranch(), opts.GetTag(), opts.actor) - if err != nil { - return fmt.Errorf("creating tag provenance metadata: %w", err) + rev := opts.GetRevision() + if rev == nil { + return errors.New("unable to get revision from configured options") + } + if _, ok := rev.(*models.Tag); !ok { + return errors.New("revision is not a tag") } - // check p against policy - pe := policy.NewPolicyEvaluator() - pe.UseLocalPolicy = opts.useLocalPolicy - verifiedLevels, policyPath, err := pe.EvaluateTagProv(cmd.Context(), opts.GetRepository(), prov) + // Create the authenticator + authenticator, err := CheckAuth() if err != nil { - return fmt.Errorf("evaluating the tag provenance metadata: %w", err) + return err } - // create vsa - unsignedVsa, err := attest.CreateUnsignedSourceVsa( - opts.GetBranch(), opts.GetCommit(), verifiedLevels, policyPath, + // Initialize sourcetool + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), + sourcetool.WithNotesStorer(notesStorer), + sourcetool.WithGithubStorer(githubStorer), ) if err != nil { - return err + return fmt.Errorf("creating sourcetool: %w", err) } - unsignedProv, err := protojson.Marshal(prov) + // Attest the commit passing the options + verifiedLevels, err := srctool.AttestRevision( + cmd.Context(), opts.GetBranch(), opts.GetRevision(), + sourcetool.WithLocalPolicy(opts.useLocalPolicy), + sourcetool.WithOutputPath(outputPath), + sourcetool.WithSign(signAttestation), + sourcetool.WithUseStdout(true), + sourcetool.WithPush(pushAttestations), + ) if err != nil { - return err + return fmt.Errorf("attesting commit: %w", err) } - if opts.outputSignedBundle != "" { - f, err := os.OpenFile(opts.outputSignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec - if err != nil { - return err - } - defer f.Close() //nolint:errcheck - - signedProv, err := attest.Sign(string(unsignedProv)) - if err != nil { - return err - } - - signedVsa, err := attest.Sign(unsignedVsa) - if err != nil { - return err - } - - if _, err := f.WriteString(signedProv + "\n" + signedVsa + "\n"); err != nil { - return fmt.Errorf("writing bundledata: %w", err) - } - } else { - log.Printf("unsigned prov: %s\n", unsignedProv) - log.Printf("unsigned vsa: %s\n", unsignedVsa) - } - fmt.Print(verifiedLevels) + fmt.Print(verifiedLevels.Levels()) return nil }, } diff --git a/internal/cmd/options.go b/internal/cmd/options.go index 98114c8..d902607 100644 --- a/internal/cmd/options.go +++ b/internal/cmd/options.go @@ -272,9 +272,11 @@ func (ro *revisionOpts) GetRevision() models.Revision { if ro.tag != "" { return &models.Tag{ Name: ro.tag, - Commit: &models.Commit{}, - Repository: &models.Repository{}, + Commit: ro.GetCommit(), + Repository: ro.GetRepository(), } + } else if ro.commit != "" { + return ro.GetCommit() } return nil } diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index 12199ab..2ffefb5 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -314,7 +314,7 @@ func (a *Attester) CreateTagProvenance(ctx context.Context, branch *models.Branc // 1. Check that the tag hygiene control is still enabled and how long it's been enabled, store it in the prov. // 2. Get a VSA associated with this commit, if any. // 3. Record the levels and branches covered by that VSA in the provenance. - controls, err := a.backend.GetTagControls(ctx, tag) + controls, err := a.backend.GetTagControls(ctx, branch, tag) if err != nil { return nil, fmt.Errorf("getting tag controls: %w", err) } diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index 4f1aba5..215cc8c 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -301,8 +301,11 @@ func (b *Backend) controlImplementationMessage(ctrlName slsa.ControlName) string } } -func (b *Backend) GetTagControls(context.Context, *models.Tag) (*slsa.ControlSet, error) { - return nil, fmt.Errorf("not yet implemented") +func (b *Backend) GetTagControls(ctx context.Context, branch *models.Branch, tag *models.Tag) (*slsa.ControlSet, error) { + if tag.Commit == nil || tag.Commit.SHA == "" { + return nil, errors.New("tag commit is empty") + } + return b.GetBranchControlsAtCommit(ctx, branch, tag.Commit) } func (b *Backend) ControlConfigurationDescr(branch *models.Branch, config models.ControlConfiguration) string { diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index a2d6277..06b9246 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -41,7 +41,7 @@ type AttestationStorageReader interface { type VcsBackend interface { GetBranchControls(context.Context, *Branch) (*slsa.ControlSet, error) GetBranchControlsAtCommit(context.Context, *Branch, *Commit) (*slsa.ControlSet, error) - GetTagControls(context.Context, *Tag) (*slsa.ControlSet, error) + GetTagControls(context.Context, *Branch, *Tag) (*slsa.ControlSet, error) ControlConfigurationDescr(*Branch, ControlConfiguration) string ConfigureControls(*Repository, []*Branch, []ControlConfiguration) error GetLatestCommit(context.Context, *Repository, *Branch) (*Commit, error) diff --git a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go index c4ec68c..3740caf 100644 --- a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go +++ b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go @@ -142,11 +142,12 @@ type FakeVcsBackend struct { result1 *models.Commit result2 error } - GetTagControlsStub func(context.Context, *models.Tag) (*slsa.ControlSet, error) + GetTagControlsStub func(context.Context, *models.Branch, *models.Tag) (*slsa.ControlSet, error) getTagControlsMutex sync.RWMutex getTagControlsArgsForCall []struct { arg1 context.Context - arg2 *models.Tag + arg2 *models.Branch + arg3 *models.Tag } getTagControlsReturns struct { result1 *slsa.ControlSet @@ -766,19 +767,20 @@ func (fake *FakeVcsBackend) GetRevisionCommitReturnsOnCall(i int, result1 *model }{result1, result2} } -func (fake *FakeVcsBackend) GetTagControls(arg1 context.Context, arg2 *models.Tag) (*slsa.ControlSet, error) { +func (fake *FakeVcsBackend) GetTagControls(arg1 context.Context, arg2 *models.Branch, arg3 *models.Tag) (*slsa.ControlSet, error) { fake.getTagControlsMutex.Lock() ret, specificReturn := fake.getTagControlsReturnsOnCall[len(fake.getTagControlsArgsForCall)] fake.getTagControlsArgsForCall = append(fake.getTagControlsArgsForCall, struct { arg1 context.Context - arg2 *models.Tag - }{arg1, arg2}) + arg2 *models.Branch + arg3 *models.Tag + }{arg1, arg2, arg3}) stub := fake.GetTagControlsStub fakeReturns := fake.getTagControlsReturns - fake.recordInvocation("GetTagControls", []interface{}{arg1, arg2}) + fake.recordInvocation("GetTagControls", []interface{}{arg1, arg2, arg3}) fake.getTagControlsMutex.Unlock() if stub != nil { - return stub(arg1, arg2) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1, ret.result2 @@ -792,17 +794,17 @@ func (fake *FakeVcsBackend) GetTagControlsCallCount() int { return len(fake.getTagControlsArgsForCall) } -func (fake *FakeVcsBackend) GetTagControlsCalls(stub func(context.Context, *models.Tag) (*slsa.ControlSet, error)) { +func (fake *FakeVcsBackend) GetTagControlsCalls(stub func(context.Context, *models.Branch, *models.Tag) (*slsa.ControlSet, error)) { fake.getTagControlsMutex.Lock() defer fake.getTagControlsMutex.Unlock() fake.GetTagControlsStub = stub } -func (fake *FakeVcsBackend) GetTagControlsArgsForCall(i int) (context.Context, *models.Tag) { +func (fake *FakeVcsBackend) GetTagControlsArgsForCall(i int) (context.Context, *models.Branch, *models.Tag) { fake.getTagControlsMutex.RLock() defer fake.getTagControlsMutex.RUnlock() argsForCall := fake.getTagControlsArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } func (fake *FakeVcsBackend) GetTagControlsReturns(result1 *slsa.ControlSet, result2 error) { diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index d783de0..039b12c 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -377,10 +377,10 @@ var defaultAttestOptions = AttestOptions{ UseStdOut: true, } -// AttestCommit checks the source control system status, the repository policy -// and generates the repository attestations (provenance & VSA). -func (t *Tool) AttestCommit( - ctx context.Context, branch *models.Branch, commit *models.Commit, funcs ...AttOpFn, +// AttestRevision checks the source control system status, the repository policy +// and generates the repository attestations (provenance & VSA) for a revision +func (t *Tool) AttestRevision( + ctx context.Context, branch *models.Branch, rev models.Revision, funcs ...AttOpFn, ) (slsa.SourceVerifiedLevels, error) { var agent *collector.Agent var err error @@ -401,37 +401,66 @@ func (t *Tool) AttestCommit( } } - // 1. Create the provenance attestation - prov, err := t.Attester().CreateSourceProvenance(ctx, branch, commit) - if err != nil { - return nil, err - } + var vsaData string + var provenanceData []byte + var verifiedLevels slsa.SourceVerifiedLevels + var policyPath string - // 2. Run the provenance against the policy to determine if the controls - // are still valid. - pe := policy.NewPolicyEvaluator() - pe.UseLocalPolicy = opts.LocalPolicy - verifiedLevels, policyPath, err := pe.EvaluateSourceProv(ctx, branch.Repository, branch, prov) - if err != nil { - return nil, fmt.Errorf("evaluating provenance with policy: %w", err) + _, isCommit := rev.(*models.Commit) + tag, isTag := rev.(*models.Tag) + + if isCommit { + // 1. Create the provenance attestation + prov, err := t.Attester().CreateSourceProvenance(ctx, branch, rev.GetCommit()) + if err != nil { + return nil, err + } + + // 2. Run the provenance against the policy to determine if the controls + // are still in the context and timelines of the policy. + pe := policy.NewPolicyEvaluator() + pe.UseLocalPolicy = opts.LocalPolicy + verifiedLevels, policyPath, err = pe.EvaluateSourceProv(ctx, branch.Repository, branch, prov) + if err != nil { + return nil, fmt.Errorf("evaluating provenance with policy: %w", err) + } + + provenanceData, err = protojson.Marshal(prov) + if err != nil { + return nil, fmt.Errorf("generating provenance attestation: %w", err) + } } - var vsaData string - var provenanceData []byte + if isTag { + // 1. Create the provenance attestation + prov, err := t.Attester().CreateTagProvenance(ctx, branch, tag, "") + if err != nil { + return nil, err + } + + // 2. Run the provenance against the policy to determine if the controls + // are still in the context and timelines of the policy. + pe := policy.NewPolicyEvaluator() + pe.UseLocalPolicy = opts.LocalPolicy + verifiedLevels, policyPath, err = pe.EvaluateTagProv(ctx, branch.Repository, prov) + if err != nil { + return nil, fmt.Errorf("evaluating provenance with policy: %w", err) + } + + provenanceData, err = protojson.Marshal(prov) + if err != nil { + return nil, fmt.Errorf("generating tag provenance attestation: %w", err) + } + } // create vsa vsaData, err = attest.CreateUnsignedSourceVsa( - branch, commit, verifiedLevels, policyPath, + branch, rev.GetCommit(), verifiedLevels, policyPath, ) if err != nil { return nil, fmt.Errorf("creating VSA: %w", err) } - provenanceData, err = protojson.Marshal(prov) - if err != nil { - return nil, fmt.Errorf("generating provenance attestation: %w", err) - } - if opts.Sign { provenanceDataString, err := attest.Sign(string(provenanceData)) if err != nil {