-
Notifications
You must be signed in to change notification settings - Fork 496
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🌱 Feature: Add scorecard attestation policy module (#2240)
* Add ability to parse policy.yaml Temporary commit Temporary commit Temporary commit Temporary commit Temporary commit Temporary commit * Remove hidden options * Fix cilint problems * Add tests * Add tests * Address PR comments * Refactor to standalone module * Don't depend on evaluation package * Remove everything but the Binary-Artifact * Fix test failures Signed-off-by: Raghav Kaul <raghavkaul@google.com> * Address PR comments * Use glob for binary artifact ignores * Makefile Signed-off-by: Raghav Kaul <raghavkaul@google.com> Signed-off-by: Raghav Kaul <raghavkaul@google.com>
- Loading branch information
1 parent
d6bef98
commit 9e269b8
Showing
10 changed files
with
1,510 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
// Copyright 2021 Security 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 policy | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
|
||
"github.com/gobwas/glob" | ||
"gopkg.in/yaml.v2" | ||
|
||
"github.com/ossf/scorecard/v4/checker" | ||
sce "github.com/ossf/scorecard/v4/errors" | ||
) | ||
|
||
//nolint:govet | ||
type AttestationPolicy struct { | ||
// PreventBinaryArtifacts : set to true to require that this project's SCM repo is | ||
// free of binary artifacts | ||
PreventBinaryArtifacts bool `yaml:"preventBinaryArtifacts"` | ||
|
||
// AllowedBinaryArtifacts : List of binary artifact paths to ignore | ||
// when checking for binary artifacts in a repo | ||
AllowedBinaryArtifacts []string `yaml:"allowedBinaryArtifacts"` | ||
} | ||
|
||
// Run attestation policy checks on raw data. | ||
func RunChecksForPolicy(policy *AttestationPolicy, raw *checker.RawResults, | ||
dl checker.DetailLogger, | ||
) (PolicyResult, error) { | ||
if policy.PreventBinaryArtifacts { | ||
checkResult, err := CheckPreventBinaryArtifacts(policy.AllowedBinaryArtifacts, raw, dl) | ||
|
||
if !checkResult || err != nil { | ||
return checkResult, err | ||
} | ||
} | ||
|
||
return Pass, nil | ||
} | ||
|
||
type PolicyResult = bool | ||
|
||
const ( | ||
Pass PolicyResult = true | ||
Fail PolicyResult = false | ||
) | ||
|
||
func CheckPreventBinaryArtifacts( | ||
allowedBinaryArtifacts []string, | ||
results *checker.RawResults, | ||
dl checker.DetailLogger, | ||
) (PolicyResult, error) { | ||
for i := range results.BinaryArtifactResults.Files { | ||
artifactFile := results.BinaryArtifactResults.Files[i] | ||
|
||
ignoreArtifact := false | ||
|
||
for j := range allowedBinaryArtifacts { | ||
// Treat user input as paths and try to match prefixes | ||
// This is a bit easier to use than forcing things to be file names | ||
allowGlob := allowedBinaryArtifacts[j] | ||
|
||
if g := glob.MustCompile(allowGlob); g.Match(artifactFile.Path) { | ||
ignoreArtifact = true | ||
dl.Info(&checker.LogMessage{Text: fmt.Sprintf( | ||
"ignoring binary artifact at %s due to ignored glob path %s", | ||
artifactFile.Path, | ||
g, | ||
)}) | ||
} | ||
} | ||
|
||
if !ignoreArtifact { | ||
dl.Info(&checker.LogMessage{ | ||
Path: artifactFile.Path, Type: checker.FileTypeBinary, | ||
Offset: artifactFile.Offset, | ||
Text: "binary detected", | ||
}) | ||
return Fail, nil | ||
} | ||
} | ||
|
||
return Pass, nil | ||
} | ||
|
||
// ParseFromFile takes a policy file and returns an AttestationPolicy. | ||
func ParseAttestationPolicyFromFile(policyFile string) (*AttestationPolicy, error) { | ||
if policyFile != "" { | ||
data, err := os.ReadFile(policyFile) | ||
if err != nil { | ||
return nil, sce.WithMessage(sce.ErrScorecardInternal, | ||
fmt.Sprintf("os.ReadFile: %v", err)) | ||
} | ||
|
||
sp, err := ParseAttestationPolicyFromYAML(data) | ||
if err != nil { | ||
return nil, | ||
sce.WithMessage( | ||
sce.ErrScorecardInternal, | ||
fmt.Sprintf("spol.ParseFromYAML: %v", err), | ||
) | ||
} | ||
|
||
return sp, nil | ||
} | ||
|
||
return nil, nil | ||
} | ||
|
||
// Parses a policy file and returns a AttestationPolicy. | ||
func ParseAttestationPolicyFromYAML(b []byte) (*AttestationPolicy, error) { | ||
retPolicy := AttestationPolicy{} | ||
|
||
err := yaml.Unmarshal(b, &retPolicy) | ||
if err != nil { | ||
return &retPolicy, sce.WithMessage(sce.ErrScorecardInternal, err.Error()) | ||
} | ||
|
||
return &retPolicy, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
// Copyright 2022 Security 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 policy | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"testing" | ||
|
||
"github.com/ossf/scorecard/v4/checker" | ||
sce "github.com/ossf/scorecard/v4/errors" | ||
scut "github.com/ossf/scorecard/v4/utests" | ||
) | ||
|
||
func (a AttestationPolicy) ToJSON() string { | ||
jsonbytes, err := json.Marshal(a) | ||
if err != nil { | ||
return "" | ||
} | ||
|
||
return string(jsonbytes) | ||
} | ||
|
||
func TestCheckPreventBinaryArtifacts(t *testing.T) { | ||
t.Parallel() | ||
|
||
dl := scut.TestDetailLogger{} | ||
|
||
tests := []struct { | ||
name string | ||
raw *checker.RawResults | ||
err error | ||
allowedBinaryArtifacts []string | ||
expected PolicyResult | ||
}{ | ||
{ | ||
name: "test with no artifacts", | ||
raw: &checker.RawResults{ | ||
BinaryArtifactResults: checker.BinaryArtifactData{Files: []checker.File{}}, | ||
}, | ||
expected: Pass, | ||
err: nil, | ||
}, | ||
{ | ||
name: "test with multiple artifacts", | ||
raw: &checker.RawResults{ | ||
BinaryArtifactResults: checker.BinaryArtifactData{Files: []checker.File{ | ||
{Path: "a"}, | ||
{Path: "b"}, | ||
}}, | ||
}, | ||
expected: Fail, | ||
err: nil, | ||
}, | ||
{ | ||
name: "test with multiple ignored artifacts", | ||
allowedBinaryArtifacts: []string{"a", "b"}, | ||
raw: &checker.RawResults{ | ||
BinaryArtifactResults: checker.BinaryArtifactData{Files: []checker.File{ | ||
{Path: "a"}, | ||
{Path: "b"}, | ||
}}, | ||
}, | ||
expected: Pass, | ||
err: nil, | ||
}, | ||
{ | ||
name: "test with some artifacts", | ||
allowedBinaryArtifacts: []string{"a"}, | ||
raw: &checker.RawResults{ | ||
BinaryArtifactResults: checker.BinaryArtifactData{Files: []checker.File{ | ||
{Path: "a"}, | ||
{Path: "b/a"}, | ||
}}, | ||
}, | ||
expected: Fail, | ||
err: nil, | ||
}, | ||
|
||
{ | ||
name: "test with glob ignored", | ||
allowedBinaryArtifacts: []string{"a/*", "b/*"}, | ||
raw: &checker.RawResults{ | ||
BinaryArtifactResults: checker.BinaryArtifactData{Files: []checker.File{ | ||
{Path: "a/c/foo.txt"}, | ||
{Path: "b/c/foo.txt"}, | ||
}}, | ||
}, | ||
expected: Pass, | ||
err: nil, | ||
}, | ||
} | ||
|
||
for i := range tests { | ||
tt := &tests[i] | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
actual, err := CheckPreventBinaryArtifacts(tt.allowedBinaryArtifacts, tt.raw, &dl) | ||
|
||
if !errors.Is(err, tt.err) { | ||
t.Fatalf("%s: expected %v, got %v", tt.name, tt.err, err) | ||
} | ||
if err != nil { | ||
return | ||
} | ||
|
||
// Compare outputs only if the error is nil. | ||
// TODO: compare objects. | ||
if actual != tt.expected { | ||
t.Fatalf("%s: invalid result", tt.name) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestAttestationPolicyRead(t *testing.T) { | ||
t.Parallel() | ||
|
||
tests := []struct { | ||
err error | ||
name string | ||
filename string | ||
result AttestationPolicy | ||
}{ | ||
{ | ||
name: "default attestation policy with everything on", | ||
filename: "./testdata/policy-binauthz.yaml", | ||
err: nil, | ||
result: AttestationPolicy{ | ||
PreventBinaryArtifacts: true, | ||
AllowedBinaryArtifacts: []string{}, | ||
}, | ||
}, | ||
{ | ||
name: "invalid attestation policy", | ||
filename: "./testdata/policy-binauthz-invalid.yaml", | ||
err: sce.ErrScorecardInternal, | ||
}, | ||
{ | ||
name: "policy with allowlist of binary artifacts", | ||
filename: "./testdata/policy-binauthz-allowlist.yaml", | ||
err: nil, | ||
result: AttestationPolicy{ | ||
PreventBinaryArtifacts: true, | ||
AllowedBinaryArtifacts: []string{"/a/b/c", "d"}, | ||
}, | ||
}, | ||
{ | ||
name: "policy with allowlist of binary artifacts", | ||
filename: "./testdata/policy-binauthz-missingparam.yaml", | ||
err: nil, | ||
result: AttestationPolicy{ | ||
PreventBinaryArtifacts: true, | ||
AllowedBinaryArtifacts: nil, | ||
}, | ||
}, | ||
} | ||
|
||
for i := range tests { | ||
tt := &tests[i] | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
p, err := ParseAttestationPolicyFromFile(tt.filename) | ||
|
||
if !errors.Is(err, tt.err) { | ||
t.Fatalf("%s: expected %v, got %v", tt.name, tt.err, err) | ||
} | ||
if err != nil { | ||
return | ||
} | ||
|
||
// Compare outputs only if the error is nil. | ||
// TODO: compare objects. | ||
if p.ToJSON() != tt.result.ToJSON() { | ||
t.Fatalf("%s: invalid result", tt.name) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.