-
Notifications
You must be signed in to change notification settings - Fork 513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ Support results output as in-toto statement #4491
base: main
Are you sure you want to change the base?
Changes from all commits
f7b2b31
123a70e
c1ee9ed
f87963a
0d5d2c6
05f6ed4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// Copyright 2025 OpenSSF Scorecard Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package scorecard | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
|
||
intoto "github.com/in-toto/attestation/go/v1" | ||
|
||
docs "github.com/ossf/scorecard/v5/docs/checks" | ||
sce "github.com/ossf/scorecard/v5/errors" | ||
"github.com/ossf/scorecard/v5/log" | ||
) | ||
|
||
const ( | ||
PredicateType = "https://scorecard.dev/result/v0.1" | ||
) | ||
|
||
type Statement struct { | ||
intoto.Statement | ||
Predicate Predicate `json:"predicate"` | ||
} | ||
|
||
// Predicate overrides JSONScorecardResultV2 with a nullable Repo field. | ||
type Predicate struct { | ||
Repo *jsonRepoV2 `json:"repo,omitempty"` | ||
Comment on lines
+38
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for my own understanding, this removes the repo field from the JSON since intoto already has a subject in the statement? |
||
JSONScorecardResultV2 | ||
} | ||
Comment on lines
+29
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we expect someone to reference these? Or in general, do we need to export anything here other than |
||
|
||
// AsStatementResultOption wraps AsJSON2ResultOption preparing it for. | ||
type AsStatementResultOption struct { | ||
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
AsJSON2ResultOption | ||
} | ||
|
||
// AsStatement converts the results as an in-toto statement. | ||
func (r *Result) AsStatement(writer io.Writer, checkDocs docs.Doc, opt *AsStatementResultOption) error { | ||
Comment on lines
+49
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AsIntoto |
||
// Build the attestation subject from the result Repo. | ||
subject := intoto.ResourceDescriptor{ | ||
Name: r.Repo.Name, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this set the URI field by any chance? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, it now populates the URi with an spdx-style downlaod locator (check the updated example above) |
||
Uri: fmt.Sprintf("git+https://%s@%s", r.Repo.Name, r.Repo.CommitSHA), | ||
Digest: map[string]string{ | ||
"gitCommit": r.Repo.CommitSHA, | ||
}, | ||
} | ||
|
||
if opt == nil { | ||
opt = &AsStatementResultOption{ | ||
AsJSON2ResultOption{ | ||
LogLevel: log.DefaultLevel, | ||
Details: false, | ||
Annotations: false, | ||
}, | ||
} | ||
} | ||
|
||
json2, err := r.resultsToJSON2(checkDocs, &opt.AsJSON2ResultOption) | ||
if err != nil { | ||
return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) | ||
} | ||
|
||
out := Statement{ | ||
Statement: intoto.Statement{ | ||
Type: intoto.StatementTypeUri, | ||
Subject: []*intoto.ResourceDescriptor{ | ||
&subject, | ||
}, | ||
PredicateType: PredicateType, | ||
}, | ||
Predicate: Predicate{ | ||
JSONScorecardResultV2: json2, | ||
Repo: nil, | ||
}, | ||
} | ||
|
||
encoder := json.NewEncoder(writer) | ||
if err := encoder.Encode(&out); err != nil { | ||
return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("encoder.Encode: %v", err)) | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Copyright 2024 OpenSSF Scorecard Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package scorecard | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"slices" | ||
"testing" | ||
"time" | ||
|
||
"github.com/ossf/scorecard/v5/finding" | ||
) | ||
|
||
func TestStatement(t *testing.T) { | ||
t.Parallel() | ||
// The intoto statement generation relies on the same generation as | ||
// the json output, so here we just check for correct assignments | ||
result := Result{ | ||
Repo: RepoInfo{ | ||
Name: "github.com/example/example", | ||
CommitSHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | ||
}, | ||
Scorecard: ScorecardInfo{ | ||
Version: "1.2.3", | ||
CommitSHA: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", | ||
}, | ||
Date: time.Date(2024, time.February, 1, 13, 48, 0, 0, time.UTC), | ||
Findings: []finding.Finding{ | ||
{ | ||
Probe: "check for X", | ||
Outcome: finding.OutcomeTrue, | ||
Message: "found X", | ||
Location: &finding.Location{ | ||
Path: "some/path/to/file", | ||
Type: finding.FileTypeText, | ||
}, | ||
}, | ||
{ | ||
Probe: "check for Y", | ||
Outcome: finding.OutcomeFalse, | ||
Message: "did not find Y", | ||
}, | ||
}, | ||
} | ||
var w bytes.Buffer | ||
err := result.AsStatement(&w, jsonMockDocRead(), nil) | ||
if err != nil { | ||
t.Error("unexpected error: ", err) | ||
} | ||
|
||
// Unmarshal the written json to a generic map | ||
stmt := Statement{} | ||
if err := json.Unmarshal(w.Bytes(), &stmt); err != nil { | ||
t.Error("error unmarshaling statement", err) | ||
return | ||
} | ||
|
||
// Check the data | ||
if len(stmt.Subject) != 1 { | ||
t.Error("unexpected statement subject length") | ||
} | ||
if stmt.Subject[0].GetDigest()["gitCommit"] != result.Repo.CommitSHA { | ||
t.Error("mismatched statement subject digest") | ||
} | ||
if stmt.Subject[0].GetName() != result.Repo.Name { | ||
t.Error("mismatched statement subject name") | ||
} | ||
|
||
if stmt.PredicateType != PredicateType { | ||
t.Error("incorrect predicate type", stmt.PredicateType) | ||
} | ||
|
||
// Check the predicate | ||
if stmt.Predicate.Scorecard.Commit != result.Scorecard.CommitSHA { | ||
t.Error("mismatch in scorecard commit") | ||
} | ||
if stmt.Predicate.Scorecard.Version != result.Scorecard.Version { | ||
t.Error("mismatch in scorecard version") | ||
} | ||
if stmt.Predicate.Repo != nil { | ||
t.Error("repo should be null") | ||
} | ||
if !slices.Equal(stmt.Predicate.Metadata, result.Metadata) { | ||
t.Error("mismatched metadata") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
discussion: any preference on
statement
vsintoto
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also considered
attestation
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think
intoto
would make it crystal clear what the output format would be.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's do
FormatIntoto = "intoto"
, I'll leave a few other comments on names.