mirror of
https://github.com/ossf/scorecard.git
synced 2024-11-04 03:52:31 +03:00
Generalize CheckFileContent functions (#1670)
Co-authored-by: Azeem Shaikh <azeems@google.com>
This commit is contained in:
parent
5656c3ed45
commit
e41f8595cb
@ -98,23 +98,37 @@ func DangerousWorkflow(c *checker.CheckRequest) checker.CheckResult {
|
|||||||
data := patternCbData{
|
data := patternCbData{
|
||||||
workflowPattern: make(map[dangerousResults]bool),
|
workflowPattern: make(map[dangerousResults]bool),
|
||||||
}
|
}
|
||||||
err := fileparser.CheckFilesContent(".github/workflows/*", false,
|
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||||
c, validateGitHubActionWorkflowPatterns, &data)
|
Pattern: ".github/workflows/*",
|
||||||
|
CaseSensitive: false,
|
||||||
|
},
|
||||||
|
validateGitHubActionWorkflowPatterns, c.Dlogger, &data)
|
||||||
return createResultForDangerousWorkflowPatterns(data, err)
|
return createResultForDangerousWorkflowPatterns(data, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check file content.
|
// Check file content.
|
||||||
func validateGitHubActionWorkflowPatterns(path string, content []byte, dl checker.DetailLogger,
|
var validateGitHubActionWorkflowPatterns fileparser.DoWhileTrueOnFileContent = func(path string,
|
||||||
data fileparser.FileCbData) (bool, error) {
|
content []byte,
|
||||||
|
args ...interface{}) (bool, error) {
|
||||||
if !fileparser.IsWorkflowFile(path) {
|
if !fileparser.IsWorkflowFile(path) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(args) != 2 {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"validateGitHubActionWorkflowPatterns requires exactly 2 arguments: %w", errInvalidArgLength)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify the type of the data.
|
// Verify the type of the data.
|
||||||
pdata, ok := data.(*patternCbData)
|
pdata, ok := args[1].(*patternCbData)
|
||||||
if !ok {
|
if !ok {
|
||||||
// This never happens.
|
return false, fmt.Errorf(
|
||||||
panic("invalid type")
|
"validateGitHubActionWorkflowPatterns expects arg[0] of type *patternCbData: %w", errInvalidArgType)
|
||||||
|
}
|
||||||
|
dl, ok := args[0].(checker.DetailLogger)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"validateGitHubActionWorkflowPatterns expects arg[1] of type checker.DetailLogger: %w", errInvalidArgType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fileparser.CheckFileContainsCommands(content, "#") {
|
if !fileparser.CheckFileContainsCommands(content, "#") {
|
||||||
|
@ -20,16 +20,16 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ossf/scorecard/v4/checker"
|
|
||||||
"github.com/ossf/scorecard/v4/clients"
|
"github.com/ossf/scorecard/v4/clients"
|
||||||
sce "github.com/ossf/scorecard/v4/errors"
|
sce "github.com/ossf/scorecard/v4/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// isMatchingPath uses 'pattern' to shell-match the 'path' and its filename
|
// isMatchingPath uses 'pattern' to shell-match the 'path' and its filename
|
||||||
// 'caseSensitive' indicates the match should be case-sensitive. Default: no.
|
// 'caseSensitive' indicates the match should be case-sensitive. Default: no.
|
||||||
func isMatchingPath(pattern, fullpath string, caseSensitive bool) (bool, error) {
|
func isMatchingPath(fullpath string, matchPathTo PathMatcher) (bool, error) {
|
||||||
if !caseSensitive {
|
pattern := matchPathTo.Pattern
|
||||||
pattern = strings.ToLower(pattern)
|
if !matchPathTo.CaseSensitive {
|
||||||
|
pattern = strings.ToLower(matchPathTo.Pattern)
|
||||||
fullpath = strings.ToLower(fullpath)
|
fullpath = strings.ToLower(fullpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,87 +55,30 @@ func isTestdataFile(fullpath string) bool {
|
|||||||
strings.Contains(fullpath, "/testdata/")
|
strings.Contains(fullpath, "/testdata/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileCbData is any data the caller can act upon
|
// PathMatcher represents a query for a filepath.
|
||||||
// to keep state.
|
type PathMatcher struct {
|
||||||
type FileCbData interface{}
|
Pattern string
|
||||||
|
CaseSensitive bool
|
||||||
// FileContentCb is the callback.
|
|
||||||
// The bool returned indicates whether the CheckFilesContent2
|
|
||||||
// should continue iterating over files or not.
|
|
||||||
type FileContentCb func(path string, content []byte,
|
|
||||||
dl checker.DetailLogger, data FileCbData) (bool, error)
|
|
||||||
|
|
||||||
// CheckFilesContent downloads the tar of the repository and calls the onFileContent() function
|
|
||||||
// shellPathFnPattern is used for https://golang.org/pkg/path/#Match
|
|
||||||
// Warning: the pattern is used to match (1) the entire path AND (2) the filename alone. This means:
|
|
||||||
// - To scope the search to a directory, use "./dirname/*". Example, for the root directory,
|
|
||||||
// use "./*".
|
|
||||||
// - A pattern such as "*mypatern*" will match files containing mypattern in *any* directory.
|
|
||||||
func CheckFilesContent(shellPathFnPattern string,
|
|
||||||
caseSensitive bool,
|
|
||||||
c *checker.CheckRequest,
|
|
||||||
onFileContent FileContentCb,
|
|
||||||
data FileCbData,
|
|
||||||
) error {
|
|
||||||
predicate := func(filepath string) (bool, error) {
|
|
||||||
// Filter out test files.
|
|
||||||
if isTestdataFile(filepath) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
// Filter out files based on path/names using the pattern.
|
|
||||||
b, err := isMatchingPath(shellPathFnPattern, filepath, caseSensitive)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
matchedFiles, err := c.RepoClient.ListFiles(predicate)
|
|
||||||
if err != nil {
|
|
||||||
// nolint: wrapcheck
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range matchedFiles {
|
|
||||||
content, err := c.RepoClient.GetFileContent(file)
|
|
||||||
if err != nil {
|
|
||||||
//nolint
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
continueIter, err := onFileContent(file, content, c.Dlogger, data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !continueIter {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileContentCbV6 is the callback.
|
// DoWhileTrueOnFileContent takes a filepath, its content and
|
||||||
// The bool returned indicates whether the CheckFilesContent2
|
// optional variadic args. It returns a boolean indicating whether
|
||||||
// should continue iterating over files or not.
|
// iterating over next files should continue.
|
||||||
type FileContentCbV6 func(path string, content []byte, data FileCbData) (bool, error)
|
type DoWhileTrueOnFileContent func(path string, content []byte, args ...interface{}) (bool, error)
|
||||||
|
|
||||||
// CheckFilesContentV6 is the same as CheckFilesContent
|
// OnMatchingFileContentDo matches all files listed by `repoClient` against `matchPathTo`
|
||||||
// but for use with separated check/policy code.
|
// and on every successful match, runs onFileContent fn on the file's contents.
|
||||||
func CheckFilesContentV6(shellPathFnPattern string,
|
// Continues iterating along the matched files until onFileContent returns
|
||||||
caseSensitive bool,
|
// either a false value or an error.
|
||||||
repoClient clients.RepoClient,
|
func OnMatchingFileContentDo(repoClient clients.RepoClient, matchPathTo PathMatcher,
|
||||||
onFileContent FileContentCbV6,
|
onFileContent DoWhileTrueOnFileContent, args ...interface{}) error {
|
||||||
data FileCbData,
|
|
||||||
) error {
|
|
||||||
predicate := func(filepath string) (bool, error) {
|
predicate := func(filepath string) (bool, error) {
|
||||||
// Filter out test files.
|
// Filter out test files.
|
||||||
if isTestdataFile(filepath) {
|
if isTestdataFile(filepath) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
// Filter out files based on path/names using the pattern.
|
// Filter out files based on path/names using the pattern.
|
||||||
b, err := isMatchingPath(shellPathFnPattern, filepath, caseSensitive)
|
b, err := isMatchingPath(filepath, matchPathTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -144,18 +87,16 @@ func CheckFilesContentV6(shellPathFnPattern string,
|
|||||||
|
|
||||||
matchedFiles, err := repoClient.ListFiles(predicate)
|
matchedFiles, err := repoClient.ListFiles(predicate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// nolint: wrapcheck
|
return fmt.Errorf("error during ListFiles: %w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range matchedFiles {
|
for _, file := range matchedFiles {
|
||||||
content, err := repoClient.GetFileContent(file)
|
content, err := repoClient.GetFileContent(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//nolint
|
return fmt.Errorf("error during GetFileContent: %w", err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
continueIter, err := onFileContent(file, content, data)
|
continueIter, err := onFileContent(file, content, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ import (
|
|||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
|
||||||
"github.com/ossf/scorecard/v4/checker"
|
|
||||||
mockrepo "github.com/ossf/scorecard/v4/clients/mockclients"
|
mockrepo "github.com/ossf/scorecard/v4/clients/mockclients"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -316,7 +315,10 @@ func Test_isMatchingPath(t *testing.T) {
|
|||||||
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
got, err := isMatchingPath(tt.args.pattern, tt.args.fullpath, tt.args.caseSensitive)
|
got, err := isMatchingPath(tt.args.fullpath, PathMatcher{
|
||||||
|
Pattern: tt.args.pattern,
|
||||||
|
CaseSensitive: tt.args.caseSensitive,
|
||||||
|
})
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("isMatchingPath() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("isMatchingPath() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
@ -385,8 +387,8 @@ func Test_isTestdataFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCheckFilesContentV6 tests the CheckFilesContentV6 function.
|
// TestOnMatchingFileContentDo tests the OnMatchingFileContent function.
|
||||||
func TestCheckFilesContentV6(t *testing.T) {
|
func TestOnMatchingFileContent(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
//nolint
|
//nolint
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -448,50 +450,6 @@ func TestCheckFilesContentV6(t *testing.T) {
|
|||||||
"Dockerfile.template.template.template.template",
|
"Dockerfile.template.template.template.template",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
x := func(path string, content []byte, data FileCbData) (bool, error) {
|
|
||||||
if tt.shouldFuncFail {
|
|
||||||
//nolint
|
|
||||||
return false, errors.New("test error")
|
|
||||||
}
|
|
||||||
if tt.shouldGetPredicateFail {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
mockRepo := mockrepo.NewMockRepoClient(ctrl)
|
|
||||||
mockRepo.EXPECT().ListFiles(gomock.Any()).Return(tt.files, nil).AnyTimes()
|
|
||||||
mockRepo.EXPECT().GetFileContent(gomock.Any()).Return(nil, nil).AnyTimes()
|
|
||||||
|
|
||||||
result := CheckFilesContentV6(tt.shellPattern, tt.caseSensitive, mockRepo, x, x)
|
|
||||||
|
|
||||||
if tt.wantErr && result == nil {
|
|
||||||
t.Errorf("CheckFilesContentV6() = %v, want %v test name %v", result, tt.wantErr, tt.name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCheckFilesContent tests the CheckFilesContent function.
|
|
||||||
func TestCheckFilesContent(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
//nolint
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
wantErr bool
|
|
||||||
shellPattern string
|
|
||||||
caseSensitive bool
|
|
||||||
shouldFuncFail bool
|
|
||||||
shouldGetPredicateFail bool
|
|
||||||
files []string
|
|
||||||
}{
|
|
||||||
{
|
{
|
||||||
name: "no files",
|
name: "no files",
|
||||||
shellPattern: "Dockerfile",
|
shellPattern: "Dockerfile",
|
||||||
@ -548,8 +506,7 @@ func TestCheckFilesContent(t *testing.T) {
|
|||||||
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
x := func(path string, content []byte,
|
x := func(path string, content []byte, args ...interface{}) (bool, error) {
|
||||||
dl checker.DetailLogger, data FileCbData) (bool, error) {
|
|
||||||
if tt.shouldFuncFail {
|
if tt.shouldFuncFail {
|
||||||
//nolint
|
//nolint
|
||||||
return false, errors.New("test error")
|
return false, errors.New("test error")
|
||||||
@ -565,14 +522,13 @@ func TestCheckFilesContent(t *testing.T) {
|
|||||||
mockRepo.EXPECT().ListFiles(gomock.Any()).Return(tt.files, nil).AnyTimes()
|
mockRepo.EXPECT().ListFiles(gomock.Any()).Return(tt.files, nil).AnyTimes()
|
||||||
mockRepo.EXPECT().GetFileContent(gomock.Any()).Return(nil, nil).AnyTimes()
|
mockRepo.EXPECT().GetFileContent(gomock.Any()).Return(nil, nil).AnyTimes()
|
||||||
|
|
||||||
c := checker.CheckRequest{
|
result := OnMatchingFileContentDo(mockRepo, PathMatcher{
|
||||||
RepoClient: mockRepo,
|
Pattern: tt.shellPattern,
|
||||||
}
|
CaseSensitive: tt.caseSensitive,
|
||||||
|
}, x)
|
||||||
result := CheckFilesContent(tt.shellPattern, tt.caseSensitive, &c, x, x)
|
|
||||||
|
|
||||||
if tt.wantErr && result == nil {
|
if tt.wantErr && result == nil {
|
||||||
t.Errorf("CheckFilesContentV6() = %v, want %v test name %v", result, tt.wantErr, tt.name)
|
t.Errorf("OnMatchingFileContentDo() = %v, want %v test name %v", result, tt.wantErr, tt.name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -36,11 +36,13 @@ func init() {
|
|||||||
|
|
||||||
func checkCFLite(c *checker.CheckRequest) (bool, error) {
|
func checkCFLite(c *checker.CheckRequest) (bool, error) {
|
||||||
result := false
|
result := false
|
||||||
e := fileparser.CheckFilesContent(".clusterfuzzlite/Dockerfile", true, c,
|
e := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||||
func(path string, content []byte, dl checker.DetailLogger, data fileparser.FileCbData) (bool, error) {
|
Pattern: ".clusterfuzzlite/Dockerfile",
|
||||||
result = fileparser.CheckFileContainsCommands(content, "#")
|
CaseSensitive: true,
|
||||||
return false, nil
|
}, func(path string, content []byte, args ...interface{}) (bool, error) {
|
||||||
}, nil)
|
result = fileparser.CheckFileContainsCommands(content, "#")
|
||||||
|
return false, nil
|
||||||
|
}, nil)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return result, fmt.Errorf("%w", e)
|
return result, fmt.Errorf("%w", e)
|
||||||
}
|
}
|
||||||
|
@ -82,11 +82,68 @@ func TokenPermissions(c *checker.CheckRequest) checker.CheckResult {
|
|||||||
data := permissionCbData{
|
data := permissionCbData{
|
||||||
workflows: make(map[string]permissions),
|
workflows: make(map[string]permissions),
|
||||||
}
|
}
|
||||||
err := fileparser.CheckFilesContent(".github/workflows/*", false,
|
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||||
c, validateGitHubActionTokenPermissions, &data)
|
Pattern: ".github/workflows/*",
|
||||||
|
CaseSensitive: false,
|
||||||
|
}, validateGitHubActionTokenPermissions, c.Dlogger, &data)
|
||||||
return createResultForLeastPrivilegeTokens(data, err)
|
return createResultForLeastPrivilegeTokens(data, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check file content.
|
||||||
|
var validateGitHubActionTokenPermissions fileparser.DoWhileTrueOnFileContent = func(path string,
|
||||||
|
content []byte,
|
||||||
|
args ...interface{}) (bool, error) {
|
||||||
|
if !fileparser.IsWorkflowFile(path) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
// Verify the type of the data.
|
||||||
|
if len(args) != 2 {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"validateGitHubActionTokenPermissions requires exactly 2 arguments: %w", errInvalidArgLength)
|
||||||
|
}
|
||||||
|
pdata, ok := args[1].(*permissionCbData)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"validateGitHubActionTokenPermissions requires arg[0] of type *permissionCbData: %w", errInvalidArgType)
|
||||||
|
}
|
||||||
|
dl, ok := args[0].(checker.DetailLogger)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"validateGitHubActionTokenPermissions requires arg[1] of type checker.DetailLogger: %w", errInvalidArgType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fileparser.CheckFileContainsCommands(content, "#") {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow, errs := actionlint.Parse(content)
|
||||||
|
if len(errs) > 0 && workflow == nil {
|
||||||
|
return false, fileparser.FormatActionlintError(errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Top-level permission definitions.
|
||||||
|
//nolint
|
||||||
|
// https://docs.github.com/en/actions/reference/authentication-in-a-workflow#example-1-passing-the-github_token-as-an-input,
|
||||||
|
// https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/,
|
||||||
|
// https://docs.github.com/en/actions/reference/authentication-in-a-workflow#modifying-the-permissions-for-the-github_token.
|
||||||
|
if err := validateTopLevelPermissions(workflow, path, dl, pdata); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Run-level permission definitions,
|
||||||
|
// see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idpermissions.
|
||||||
|
ignoredPermissions := createIgnoredPermissions(workflow, path, dl)
|
||||||
|
if err := validatejobLevelPermissions(workflow, path, dl, pdata, ignoredPermissions); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(laurent): 2. Identify github actions that require write and add checks.
|
||||||
|
|
||||||
|
// TODO(laurent): 3. Read a few runs and ensures they have the same permissions.
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func validatePermission(permissionKey permission, permissionValue *actionlint.PermissionScope,
|
func validatePermission(permissionKey permission, permissionValue *actionlint.PermissionScope,
|
||||||
permLevel, path string, dl checker.DetailLogger, pPermissions map[permission]bool,
|
permLevel, path string, dl checker.DetailLogger, pPermissions map[permission]bool,
|
||||||
ignoredPermissions map[permission]bool) error {
|
ignoredPermissions map[permission]bool) error {
|
||||||
@ -378,51 +435,6 @@ func createResultForLeastPrivilegeTokens(result permissionCbData, err error) che
|
|||||||
"tokens are read-only in GitHub workflows")
|
"tokens are read-only in GitHub workflows")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check file content.
|
|
||||||
func validateGitHubActionTokenPermissions(path string, content []byte,
|
|
||||||
dl checker.DetailLogger, data fileparser.FileCbData) (bool, error) {
|
|
||||||
if !fileparser.IsWorkflowFile(path) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
// Verify the type of the data.
|
|
||||||
pdata, ok := data.(*permissionCbData)
|
|
||||||
if !ok {
|
|
||||||
// This never happens.
|
|
||||||
panic("invalid type")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !fileparser.CheckFileContainsCommands(content, "#") {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
workflow, errs := actionlint.Parse(content)
|
|
||||||
if len(errs) > 0 && workflow == nil {
|
|
||||||
return false, fileparser.FormatActionlintError(errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Top-level permission definitions.
|
|
||||||
//nolint
|
|
||||||
// https://docs.github.com/en/actions/reference/authentication-in-a-workflow#example-1-passing-the-github_token-as-an-input,
|
|
||||||
// https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/,
|
|
||||||
// https://docs.github.com/en/actions/reference/authentication-in-a-workflow#modifying-the-permissions-for-the-github_token.
|
|
||||||
if err := validateTopLevelPermissions(workflow, path, dl, pdata); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Run-level permission definitions,
|
|
||||||
// see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idpermissions.
|
|
||||||
ignoredPermissions := createIgnoredPermissions(workflow, path, dl)
|
|
||||||
if err := validatejobLevelPermissions(workflow, path, dl, pdata, ignoredPermissions); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(laurent): 2. Identify github actions that require write and add checks.
|
|
||||||
|
|
||||||
// TODO(laurent): 3. Read a few runs and ensures they have the same permissions.
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createIgnoredPermissions(workflow *actionlint.Workflow, fp string, dl checker.DetailLogger) map[permission]bool {
|
func createIgnoredPermissions(workflow *actionlint.Workflow, fp string, dl checker.DetailLogger) map[permission]bool {
|
||||||
ignoredPermissions := make(map[permission]bool)
|
ignoredPermissions := make(map[permission]bool)
|
||||||
if requiresPackagesPermissions(workflow, fp, dl) {
|
if requiresPackagesPermissions(workflow, fp, dl) {
|
||||||
|
@ -142,7 +142,7 @@ func addPinnedResult(r *pinnedResult, to bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dataAsWorkflowResultPointer(data fileparser.FileCbData) *worklowPinningResult {
|
func dataAsWorkflowResultPointer(data interface{}) *worklowPinningResult {
|
||||||
pdata, ok := data.(*worklowPinningResult)
|
pdata, ok := data.(*worklowPinningResult)
|
||||||
if !ok {
|
if !ok {
|
||||||
// panic if it is not correct type
|
// panic if it is not correct type
|
||||||
@ -151,6 +151,24 @@ func dataAsWorkflowResultPointer(data fileparser.FileCbData) *worklowPinningResu
|
|||||||
return pdata
|
return pdata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dataAsResultPointer(data interface{}) *pinnedResult {
|
||||||
|
pdata, ok := data.(*pinnedResult)
|
||||||
|
if !ok {
|
||||||
|
// This never happens.
|
||||||
|
panic("invalid type")
|
||||||
|
}
|
||||||
|
return pdata
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataAsDetailLogger(data interface{}) checker.DetailLogger {
|
||||||
|
pdata, ok := data.(checker.DetailLogger)
|
||||||
|
if !ok {
|
||||||
|
// This never happens.
|
||||||
|
panic("invalid type")
|
||||||
|
}
|
||||||
|
return pdata
|
||||||
|
}
|
||||||
|
|
||||||
func createReturnValuesForGitHubActionsWorkflowPinned(r worklowPinningResult, infoMsg string,
|
func createReturnValuesForGitHubActionsWorkflowPinned(r worklowPinningResult, infoMsg string,
|
||||||
dl checker.DetailLogger, err error) (int, error) {
|
dl checker.DetailLogger, err error) (int, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -180,15 +198,6 @@ func createReturnValuesForGitHubActionsWorkflowPinned(r worklowPinningResult, in
|
|||||||
return score, nil
|
return score, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dataAsResultPointer(data fileparser.FileCbData) *pinnedResult {
|
|
||||||
pdata, ok := data.(*pinnedResult)
|
|
||||||
if !ok {
|
|
||||||
// This never happens.
|
|
||||||
panic("invalid type")
|
|
||||||
}
|
|
||||||
return pdata
|
|
||||||
}
|
|
||||||
|
|
||||||
func createReturnValues(r pinnedResult, infoMsg string, dl checker.DetailLogger, err error) (int, error) {
|
func createReturnValues(r pinnedResult, infoMsg string, dl checker.DetailLogger, err error) (int, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return checker.InconclusiveResultScore, err
|
return checker.InconclusiveResultScore, err
|
||||||
@ -210,8 +219,10 @@ func createReturnValues(r pinnedResult, infoMsg string, dl checker.DetailLogger,
|
|||||||
|
|
||||||
func isShellScriptFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) {
|
func isShellScriptFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) {
|
||||||
var r pinnedResult
|
var r pinnedResult
|
||||||
err := fileparser.CheckFilesContent("*", false,
|
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||||
c, validateShellScriptIsFreeOfInsecureDownloads, &r)
|
Pattern: "*",
|
||||||
|
CaseSensitive: false,
|
||||||
|
}, validateShellScriptIsFreeOfInsecureDownloads, c.Dlogger, &r)
|
||||||
return createReturnForIsShellScriptFreeOfInsecureDownloads(r, c.Dlogger, err)
|
return createReturnForIsShellScriptFreeOfInsecureDownloads(r, c.Dlogger, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,9 +240,16 @@ func testValidateShellScriptIsFreeOfInsecureDownloads(pathfn string,
|
|||||||
return createReturnForIsShellScriptFreeOfInsecureDownloads(r, dl, err)
|
return createReturnForIsShellScriptFreeOfInsecureDownloads(r, dl, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateShellScriptIsFreeOfInsecureDownloads(pathfn string, content []byte,
|
var validateShellScriptIsFreeOfInsecureDownloads fileparser.DoWhileTrueOnFileContent = func(
|
||||||
dl checker.DetailLogger, data fileparser.FileCbData) (bool, error) {
|
pathfn string,
|
||||||
pdata := dataAsResultPointer(data)
|
content []byte,
|
||||||
|
args ...interface{}) (bool, error) {
|
||||||
|
if len(args) != 2 {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"validateShellScriptIsFreeOfInsecureDownloads requires exactly 2 arguments: %w", errInvalidArgLength)
|
||||||
|
}
|
||||||
|
pdata := dataAsResultPointer(args[1])
|
||||||
|
dl := dataAsDetailLogger(args[0])
|
||||||
|
|
||||||
// Validate the file type.
|
// Validate the file type.
|
||||||
if !isSupportedShellScriptFile(pathfn, content) {
|
if !isSupportedShellScriptFile(pathfn, content) {
|
||||||
@ -250,8 +268,10 @@ func validateShellScriptIsFreeOfInsecureDownloads(pathfn string, content []byte,
|
|||||||
|
|
||||||
func isDockerfileFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) {
|
func isDockerfileFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) {
|
||||||
var r pinnedResult
|
var r pinnedResult
|
||||||
err := fileparser.CheckFilesContent("*Dockerfile*",
|
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||||
false, c, validateDockerfileIsFreeOfInsecureDownloads, &r)
|
Pattern: "*Dockerfile*",
|
||||||
|
CaseSensitive: false,
|
||||||
|
}, validateDockerfileIsFreeOfInsecureDownloads, c.Dlogger, &r)
|
||||||
return createReturnForIsDockerfileFreeOfInsecureDownloads(r, c.Dlogger, err)
|
return createReturnForIsDockerfileFreeOfInsecureDownloads(r, c.Dlogger, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,9 +305,16 @@ func isDockerfile(pathfn string, content []byte) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateDockerfileIsFreeOfInsecureDownloads(pathfn string, content []byte,
|
var validateDockerfileIsFreeOfInsecureDownloads fileparser.DoWhileTrueOnFileContent = func(
|
||||||
dl checker.DetailLogger, data fileparser.FileCbData) (bool, error) {
|
pathfn string,
|
||||||
pdata := dataAsResultPointer(data)
|
content []byte,
|
||||||
|
args ...interface{}) (bool, error) {
|
||||||
|
if len(args) != 2 {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"validateDockerfileIsFreeOfInsecureDownloads requires exactly 2 arguments: %w", errInvalidArgLength)
|
||||||
|
}
|
||||||
|
pdata := dataAsResultPointer(args[1])
|
||||||
|
dl := dataAsDetailLogger(args[0])
|
||||||
|
|
||||||
// Return early if this is not a docker file.
|
// Return early if this is not a docker file.
|
||||||
if !isDockerfile(pathfn, content) {
|
if !isDockerfile(pathfn, content) {
|
||||||
@ -344,8 +371,10 @@ func validateDockerfileIsFreeOfInsecureDownloads(pathfn string, content []byte,
|
|||||||
|
|
||||||
func isDockerfilePinned(c *checker.CheckRequest) (int, error) {
|
func isDockerfilePinned(c *checker.CheckRequest) (int, error) {
|
||||||
var r pinnedResult
|
var r pinnedResult
|
||||||
err := fileparser.CheckFilesContent("*Dockerfile*", false,
|
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||||
c, validateDockerfileIsPinned, &r)
|
Pattern: "*Dockerfile*",
|
||||||
|
CaseSensitive: false,
|
||||||
|
}, validateDockerfileIsPinned, c.Dlogger, &r)
|
||||||
return createReturnForIsDockerfilePinned(r, c.Dlogger, err)
|
return createReturnForIsDockerfilePinned(r, c.Dlogger, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,12 +391,19 @@ func testValidateDockerfileIsPinned(pathfn string, content []byte, dl checker.De
|
|||||||
return createReturnForIsDockerfilePinned(r, dl, err)
|
return createReturnForIsDockerfilePinned(r, dl, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateDockerfileIsPinned(pathfn string, content []byte,
|
var validateDockerfileIsPinned fileparser.DoWhileTrueOnFileContent = func(
|
||||||
dl checker.DetailLogger, data fileparser.FileCbData) (bool, error) {
|
pathfn string,
|
||||||
|
content []byte,
|
||||||
|
args ...interface{}) (bool, error) {
|
||||||
// Users may use various names, e.g.,
|
// Users may use various names, e.g.,
|
||||||
// Dockerfile.aarch64, Dockerfile.template, Dockerfile_template, dockerfile, Dockerfile-name.template
|
// Dockerfile.aarch64, Dockerfile.template, Dockerfile_template, dockerfile, Dockerfile-name.template
|
||||||
|
|
||||||
pdata := dataAsResultPointer(data)
|
if len(args) != 2 {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"validateDockerfileIsPinned requires exactly 2 arguments: %w", errInvalidArgLength)
|
||||||
|
}
|
||||||
|
pdata := dataAsResultPointer(args[1])
|
||||||
|
dl := dataAsDetailLogger(args[0])
|
||||||
// Return early if this is not a dockerfile.
|
// Return early if this is not a dockerfile.
|
||||||
if !isDockerfile(pathfn, content) {
|
if !isDockerfile(pathfn, content) {
|
||||||
addPinnedResult(pdata, true)
|
addPinnedResult(pdata, true)
|
||||||
@ -472,8 +508,10 @@ func validateDockerfileIsPinned(pathfn string, content []byte,
|
|||||||
|
|
||||||
func isGitHubWorkflowScriptFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) {
|
func isGitHubWorkflowScriptFreeOfInsecureDownloads(c *checker.CheckRequest) (int, error) {
|
||||||
var r pinnedResult
|
var r pinnedResult
|
||||||
err := fileparser.CheckFilesContent(".github/workflows/*", false,
|
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||||
c, validateGitHubWorkflowIsFreeOfInsecureDownloads, &r)
|
Pattern: ".github/workflows/*",
|
||||||
|
CaseSensitive: false,
|
||||||
|
}, validateGitHubWorkflowIsFreeOfInsecureDownloads, c.Dlogger, &r)
|
||||||
return createReturnForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, c.Dlogger, err)
|
return createReturnForIsGitHubWorkflowScriptFreeOfInsecureDownloads(r, c.Dlogger, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,14 +532,20 @@ func testValidateGitHubWorkflowScriptFreeOfInsecureDownloads(pathfn string,
|
|||||||
|
|
||||||
// validateGitHubWorkflowIsFreeOfInsecureDownloads checks if the workflow file downloads dependencies that are unpinned.
|
// validateGitHubWorkflowIsFreeOfInsecureDownloads checks if the workflow file downloads dependencies that are unpinned.
|
||||||
// Returns true if the check should continue executing after this file.
|
// Returns true if the check should continue executing after this file.
|
||||||
// nolint: gocognit
|
var validateGitHubWorkflowIsFreeOfInsecureDownloads fileparser.DoWhileTrueOnFileContent = func(
|
||||||
func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []byte,
|
pathfn string,
|
||||||
dl checker.DetailLogger, data fileparser.FileCbData) (bool, error) {
|
content []byte,
|
||||||
|
args ...interface{}) (bool, error) {
|
||||||
if !fileparser.IsWorkflowFile(pathfn) {
|
if !fileparser.IsWorkflowFile(pathfn) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pdata := dataAsResultPointer(data)
|
if len(args) != 2 {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"validateGitHubWorkflowIsFreeOfInsecureDownloads requires exactly 2 arguments: %w", errInvalidArgLength)
|
||||||
|
}
|
||||||
|
pdata := dataAsResultPointer(args[1])
|
||||||
|
dl := dataAsDetailLogger(args[0])
|
||||||
|
|
||||||
if !fileparser.CheckFileContainsCommands(content, "#") {
|
if !fileparser.CheckFileContainsCommands(content, "#") {
|
||||||
addPinnedResult(pdata, true)
|
addPinnedResult(pdata, true)
|
||||||
@ -569,8 +613,10 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
|
|||||||
// Check pinning of github actions in workflows.
|
// Check pinning of github actions in workflows.
|
||||||
func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) (int, error) {
|
func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) (int, error) {
|
||||||
var r worklowPinningResult
|
var r worklowPinningResult
|
||||||
err := fileparser.CheckFilesContent(".github/workflows/*",
|
err := fileparser.OnMatchingFileContentDo(c.RepoClient, fileparser.PathMatcher{
|
||||||
true, c, validateGitHubActionWorkflow, &r)
|
Pattern: ".github/workflows/*",
|
||||||
|
CaseSensitive: true,
|
||||||
|
}, validateGitHubActionWorkflow, c.Dlogger, &r)
|
||||||
return createReturnForIsGitHubActionsWorkflowPinned(r, c.Dlogger, err)
|
return createReturnForIsGitHubActionsWorkflowPinned(r, c.Dlogger, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -597,13 +643,20 @@ func generateOwnerToDisplay(gitHubOwned bool) string {
|
|||||||
|
|
||||||
// validateGitHubActionWorkflow checks if the workflow file contains unpinned actions. Returns true if the check
|
// validateGitHubActionWorkflow checks if the workflow file contains unpinned actions. Returns true if the check
|
||||||
// should continue executing after this file.
|
// should continue executing after this file.
|
||||||
func validateGitHubActionWorkflow(pathfn string, content []byte,
|
var validateGitHubActionWorkflow fileparser.DoWhileTrueOnFileContent = func(
|
||||||
dl checker.DetailLogger, data fileparser.FileCbData) (bool, error) {
|
pathfn string,
|
||||||
|
content []byte,
|
||||||
|
args ...interface{}) (bool, error) {
|
||||||
if !fileparser.IsWorkflowFile(pathfn) {
|
if !fileparser.IsWorkflowFile(pathfn) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pdata := dataAsWorkflowResultPointer(data)
|
if len(args) != 2 {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"validateGitHubActionWorkflow requires exactly 2 arguments: %w", errInvalidArgLength)
|
||||||
|
}
|
||||||
|
pdata := dataAsWorkflowResultPointer(args[1])
|
||||||
|
dl := dataAsDetailLogger(args[0])
|
||||||
|
|
||||||
if !fileparser.CheckFileContainsCommands(content, "#") {
|
if !fileparser.CheckFileContainsCommands(content, "#") {
|
||||||
addWorkflowPinnedResult(pdata, true, true)
|
addWorkflowPinnedResult(pdata, true, true)
|
||||||
|
@ -31,7 +31,10 @@ import (
|
|||||||
// BinaryArtifacts retrieves the raw data for the Binary-Artifacts check.
|
// BinaryArtifacts retrieves the raw data for the Binary-Artifacts check.
|
||||||
func BinaryArtifacts(c clients.RepoClient) (checker.BinaryArtifactData, error) {
|
func BinaryArtifacts(c clients.RepoClient) (checker.BinaryArtifactData, error) {
|
||||||
files := []checker.File{}
|
files := []checker.File{}
|
||||||
err := fileparser.CheckFilesContentV6("*", false, c, checkBinaryFileContent, &files)
|
err := fileparser.OnMatchingFileContentDo(c, fileparser.PathMatcher{
|
||||||
|
Pattern: "*",
|
||||||
|
CaseSensitive: false,
|
||||||
|
}, checkBinaryFileContent, &files)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return checker.BinaryArtifactData{}, fmt.Errorf("%w", err)
|
return checker.BinaryArtifactData{}, fmt.Errorf("%w", err)
|
||||||
}
|
}
|
||||||
@ -40,12 +43,16 @@ func BinaryArtifacts(c clients.RepoClient) (checker.BinaryArtifactData, error) {
|
|||||||
return checker.BinaryArtifactData{Files: files}, nil
|
return checker.BinaryArtifactData{Files: files}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkBinaryFileContent(path string, content []byte,
|
var checkBinaryFileContent fileparser.DoWhileTrueOnFileContent = func(path string, content []byte,
|
||||||
data fileparser.FileCbData) (bool, error) {
|
args ...interface{}) (bool, error) {
|
||||||
pfiles, ok := data.(*[]checker.File)
|
if len(args) != 1 {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"checkBinaryFileContent requires exactly one argument: %w", errInvalidArgLength)
|
||||||
|
}
|
||||||
|
pfiles, ok := args[0].(*[]checker.File)
|
||||||
if !ok {
|
if !ok {
|
||||||
// This never happens.
|
return false, fmt.Errorf(
|
||||||
panic("invalid type")
|
"checkBinaryFileContent requires argument of type *[]checker.File: %w", errInvalidArgType)
|
||||||
}
|
}
|
||||||
|
|
||||||
binaryFileTypes := map[string]bool{
|
binaryFileTypes := map[string]bool{
|
||||||
|
Loading…
Reference in New Issue
Block a user