From 5b0ae81d49ce0a68c93586444cf21b46a138e5ec Mon Sep 17 00:00:00 2001 From: AdamKorcz <44787359+AdamKorcz@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:38:02 +0000 Subject: [PATCH 1/2] :seedling: migrate token permission check to probes (#3816) * :seedling: migrate token permission check to probes Signed-off-by: Adam Korczynski * combine seperate write-probes into two that combine them all Signed-off-by: AdamKorcz * change write probes to read and write Signed-off-by: AdamKorcz * minor nit Signed-off-by: AdamKorcz * remove WritaAll probes Signed-off-by: Adam Korczynski * Merge read-perm probe with job/top probes Signed-off-by: Adam Korczynski * minor refactoring Signed-off-by: Adam Korczynski * fix copy paste error Signed-off-by: Adam Korczynski * fix linter issues and restructure code Signed-off-by: Adam Korczynski * remove hasGitHubWorkflowPermissionNone probe Signed-off-by: Adam Korczynski * Remove 'hasGitHubWorkflowPermissionUndeclared' probe Signed-off-by: Adam Korczynski * bit of clean up Signed-off-by: Adam Korczynski * reduce code complexity and remove comment Signed-off-by: Adam Korczynski * simplify file location Signed-off-by: Adam Korczynski * change probe text Signed-off-by: Adam Korczynski * invert name of probe Signed-off-by: Adam Korczynski * OutcomeNotApplicable -> OutcomeError Signed-off-by: Adam Korczynski * OutcomeNotAvailable -> OutcomeNotApplicable Signed-off-by: Adam Korczynski * more OutcomeNotAvailable -> OutcomeNotApplicable Signed-off-by: Adam Korczynski * change name of 'notAvailableOrNotApplicable' Signed-off-by: Adam Korczynski * fix linter issues Signed-off-by: Adam Korczynski * add comments to remediation fields Signed-off-by: Adam Korczynski * add check for nil-dereference Signed-off-by: Adam Korczynski * remove the permissionLocation finding value Signed-off-by: Adam Korczynski * rename checkAndLogNotAvailableOrNotApplicable to isBothUndeclaredAndNotAvailableOrNotApplicable Signed-off-by: Adam Korczynski * use raw metadata for remediation output Signed-off-by: Adam Korczynski * change 'branch' to 'defaultBranch' Signed-off-by: Adam Korczynski * remove unused fields in rule Remediation Signed-off-by: Adam Korczynski * fix remediation Signed-off-by: Adam Korczynski * change 'metadata.defaultBranch' to 'metadata.repository.defaultBranch' Signed-off-by: Adam Korczynski --------- Signed-off-by: Adam Korczynski Signed-off-by: AdamKorcz --- checks/evaluation/permissions.go | 303 ++++++++++ .../gitHubWorkflowPermissionsStepsNoWrite.yml | 32 - checks/evaluation/permissions/permissions.go | 564 ------------------ checks/permissions.go | 18 +- checks/permissions_test.go | 2 +- checks/raw/permissions.go | 1 + probes/entries.go | 8 + .../def.yml | 20 +- .../impl.go | 75 +++ .../impl_test.go | 98 +++ .../internal/utils/permissions/permissions.go | 169 ++++++ probes/internal/utils/test/test.go | 146 +++++ probes/jobLevelPermissions/def.yml | 35 ++ probes/jobLevelPermissions/impl.go | 109 ++++ probes/jobLevelPermissions/impl_test.go | 57 ++ probes/topLevelPermissions/def.yml | 35 ++ probes/topLevelPermissions/impl.go | 118 ++++ probes/topLevelPermissions/impl_test.go | 57 ++ 18 files changed, 1235 insertions(+), 612 deletions(-) create mode 100644 checks/evaluation/permissions.go delete mode 100644 checks/evaluation/permissions/gitHubWorkflowPermissionsStepsNoWrite.yml delete mode 100644 checks/evaluation/permissions/permissions.go rename checks/evaluation/permissions/gitHubWorkflowPermissionsTopNoWrite.yml => probes/hasNoGitHubWorkflowPermissionUnknown/def.yml (58%) create mode 100644 probes/hasNoGitHubWorkflowPermissionUnknown/impl.go create mode 100644 probes/hasNoGitHubWorkflowPermissionUnknown/impl_test.go create mode 100644 probes/internal/utils/permissions/permissions.go create mode 100644 probes/jobLevelPermissions/def.yml create mode 100644 probes/jobLevelPermissions/impl.go create mode 100644 probes/jobLevelPermissions/impl_test.go create mode 100644 probes/topLevelPermissions/def.yml create mode 100644 probes/topLevelPermissions/impl.go create mode 100644 probes/topLevelPermissions/impl_test.go diff --git a/checks/evaluation/permissions.go b/checks/evaluation/permissions.go new file mode 100644 index 00000000000..c0e3b274d33 --- /dev/null +++ b/checks/evaluation/permissions.go @@ -0,0 +1,303 @@ +// Copyright 2021 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 evaluation + +import ( + "fmt" + + "github.com/ossf/scorecard/v4/checker" + sce "github.com/ossf/scorecard/v4/errors" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionUnknown" + "github.com/ossf/scorecard/v4/probes/jobLevelPermissions" + "github.com/ossf/scorecard/v4/probes/topLevelPermissions" +) + +func isWriteAll(f *finding.Finding) bool { + return (f.Values["tokenName"] == "all" || f.Values["tokenName"] == "write-all") +} + +// TokenPermissions applies the score policy for the Token-Permissions check. +// +//nolint:gocognit +func TokenPermissions(name string, + findings []finding.Finding, + dl checker.DetailLogger, +) checker.CheckResult { + expectedProbes := []string{ + hasNoGitHubWorkflowPermissionUnknown.Probe, + jobLevelPermissions.Probe, + topLevelPermissions.Probe, + } + if !finding.UniqueProbesEqual(findings, expectedProbes) { + e := sce.WithMessage(sce.ErrScorecardInternal, "invalid probe results") + return checker.CreateRuntimeErrorResult(name, e) + } + + // Start with a perfect score. + score := float32(checker.MaxResultScore) + + // hasWritePermissions is a map that holds information about the + // workflows in the project that have write permissions. It holds + // information about the write permissions of jobs and at the + // top-level too. The inner map (map[string]bool) has the + // workflow path as its key, and the value determines whether + // that workflow has write permissions at either "job" or "top" + // level. + hasWritePermissions := make(map[string]map[string]bool) + hasWritePermissions["jobLevel"] = make(map[string]bool) + hasWritePermissions["topLevel"] = make(map[string]bool) + + // undeclaredPermissions is a map that holds information about the + // workflows in the project that have undeclared permissions. It holds + // information about the undeclared permissions of jobs and at the + // top-level too. The inner map (map[string]bool) has the + // workflow path as its key, and the value determines whether + // that workflow has undeclared permissions at either "job" or "top" + // level. + undeclaredPermissions := make(map[string]map[string]bool) + undeclaredPermissions["jobLevel"] = make(map[string]bool) + undeclaredPermissions["topLevel"] = make(map[string]bool) + + for i := range findings { + f := &findings[i] + + // Log workflows with "none" permissions + if f.Values["permissionLevel"] == string(checker.PermissionLevelNone) { + dl.Info(&checker.LogMessage{ + Finding: f, + }) + continue + } + + // Log workflows with "read" permissions + if f.Values["permissionLevel"] == string(checker.PermissionLevelRead) { + dl.Info(&checker.LogMessage{ + Finding: f, + }) + } + + if isBothUndeclaredAndNotAvailableOrNotApplicable(f, dl) { + return checker.CreateInconclusiveResult(name, "Token permissions are not available") + } + + // If there are no TokenPermissions + if f.Outcome == finding.OutcomeNotApplicable { + return checker.CreateInconclusiveResult(name, "No tokens found") + } + + if f.Outcome != finding.OutcomeNegative { + continue + } + if f.Location == nil { + continue + } + fPath := f.Location.Path + + addProbeToMaps(fPath, undeclaredPermissions, hasWritePermissions) + + if f.Values["permissionLevel"] == string(checker.PermissionLevelUndeclared) { + score = updateScoreAndMapFromUndeclared(undeclaredPermissions, + hasWritePermissions, f, score, dl) + continue + } + + switch f.Probe { + case hasNoGitHubWorkflowPermissionUnknown.Probe: + dl.Debug(&checker.LogMessage{ + Finding: f, + }) + case topLevelPermissions.Probe: + if f.Values["permissionLevel"] != string(checker.PermissionLevelWrite) { + continue + } + hasWritePermissions["topLevel"][fPath] = true + + if !isWriteAll(f) { + score -= reduceBy(f, dl) + continue + } + + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + // "all" is evaluated separately. If the project also has write permissions + // or undeclared permissions at the job level, this is particularly bad. + if hasWritePermissions["jobLevel"][fPath] || + undeclaredPermissions["jobLevel"][fPath] { + return checker.CreateMinScoreResult(name, "detected GitHub workflow tokens with excessive permissions") + } + score -= 0.5 + case jobLevelPermissions.Probe: + if f.Values["permissionLevel"] != string(checker.PermissionLevelWrite) { + continue + } + + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + hasWritePermissions["jobLevel"][fPath] = true + + // If project has "all" writepermissions too at top level, this is + // particularly bad. + if hasWritePermissions["topLevel"][fPath] { + score = checker.MinResultScore + break + } + // If project has not declared permissions at top level:: + if undeclaredPermissions["topLevel"][fPath] { + score -= 0.5 + } + default: + continue + } + } + if score < checker.MinResultScore { + score = checker.MinResultScore + } + + logIfNoWritePermissionsFound(hasWritePermissions, dl) + + if score != checker.MaxResultScore { + return checker.CreateResultWithScore(name, + "detected GitHub workflow tokens with excessive permissions", int(score)) + } + + return checker.CreateMaxScoreResult(name, + "GitHub workflow tokens follow principle of least privilege") +} + +func logIfNoWritePermissionsFound(hasWritePermissions map[string]map[string]bool, + dl checker.DetailLogger, +) { + foundWritePermissions := false + for _, isWritePermission := range hasWritePermissions["jobLevel"] { + if isWritePermission { + foundWritePermissions = true + } + } + if !foundWritePermissions { + text := fmt.Sprintf("no %s write permissions found", checker.PermissionLocationJob) + dl.Info(&checker.LogMessage{ + Text: text, + }) + } +} + +func updateScoreFromUndeclaredJob(undeclaredPermissions map[string]map[string]bool, + hasWritePermissions map[string]map[string]bool, + fPath string, + score float32, +) float32 { + if hasWritePermissions["topLevel"][fPath] || + undeclaredPermissions["topLevel"][fPath] { + score = checker.MinResultScore + } + return score +} + +func updateScoreFromUndeclaredTop(undeclaredPermissions map[string]map[string]bool, + fPath string, + score float32, +) float32 { + if undeclaredPermissions["jobLevel"][fPath] { + score = checker.MinResultScore + } else { + score -= 0.5 + } + return score +} + +func isBothUndeclaredAndNotAvailableOrNotApplicable(f *finding.Finding, dl checker.DetailLogger) bool { + if f.Values["permissionLevel"] == string(checker.PermissionLevelUndeclared) { + if f.Outcome == finding.OutcomeNotAvailable { + return true + } else if f.Outcome == finding.OutcomeNotApplicable { + dl.Debug(&checker.LogMessage{ + Finding: f, + }) + return false + } + } + return false +} + +func updateScoreAndMapFromUndeclared(undeclaredPermissions map[string]map[string]bool, + hasWritePermissions map[string]map[string]bool, + f *finding.Finding, + score float32, dl checker.DetailLogger, +) float32 { + fPath := f.Location.Path + if f.Probe == jobLevelPermissions.Probe { + dl.Debug(&checker.LogMessage{ + Finding: f, + }) + undeclaredPermissions["jobLevel"][fPath] = true + score = updateScoreFromUndeclaredJob(undeclaredPermissions, + hasWritePermissions, + fPath, + score) + } else if f.Probe == topLevelPermissions.Probe { + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + undeclaredPermissions["topLevel"][fPath] = true + score = updateScoreFromUndeclaredTop(undeclaredPermissions, + fPath, + score) + } + + return score +} + +func addProbeToMaps(fPath string, hasWritePermissions, undeclaredPermissions map[string]map[string]bool) { + if _, ok := undeclaredPermissions["jobLevel"][fPath]; !ok { + undeclaredPermissions["jobLevel"][fPath] = false + } + if _, ok := undeclaredPermissions["topLevel"][fPath]; !ok { + undeclaredPermissions["topLevel"][fPath] = false + } + if _, ok := hasWritePermissions["jobLevel"][fPath]; !ok { + hasWritePermissions["jobLevel"][fPath] = false + } + if _, ok := hasWritePermissions["topLevel"][fPath]; !ok { + hasWritePermissions["topLevel"][fPath] = false + } +} + +func reduceBy(f *finding.Finding, dl checker.DetailLogger) float32 { + if f.Values["permissionLevel"] != string(checker.PermissionLevelWrite) { + return 0 + } + tokenName := f.Values["tokenName"] + switch tokenName { + case "checks", "statuses": + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + return 0.5 + case "contents", "packages", "actions": + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + return checker.MaxResultScore + case "deployments", "security-events": + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + return 1.0 + } + return 0 +} diff --git a/checks/evaluation/permissions/gitHubWorkflowPermissionsStepsNoWrite.yml b/checks/evaluation/permissions/gitHubWorkflowPermissionsStepsNoWrite.yml deleted file mode 100644 index 171f8503fb1..00000000000 --- a/checks/evaluation/permissions/gitHubWorkflowPermissionsStepsNoWrite.yml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2023 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. - -id: gitHubWorkflowPermissionsStepsNoWrite -short: Checks that GitHub workflows do not have steps with dangerous write permissions -motivation: > - Even with permissions default set to read, some scopes having write permissions in their steps brings incurs a risk to the project. - By giving write permission to the Actions you call in jobs, an external Action you call could abuse them. Depending on the permissions, - this could let the external Action commit unreviewed code, remove pre-submit checks to introduce a bug. - For more information about the scopes and the vulnerabilities involved, see https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions. - -implementation: > - The probe is implemented by checking whether the `permissions` keyword is given non-write permissions for the following - scopes: `statuses`, `checks`, `security-events`, `deployments`, `contents`, `packages`, `actions`. - Write permissions given to recognized packaging actions or commands are allowed and are considered an acceptable risk. -remediation: - effort: High - text: - - Verify which permissions are needed and consider whether you can reduce them. - markdown: - - Verify which permissions are needed and consider whether you can reduce them. diff --git a/checks/evaluation/permissions/permissions.go b/checks/evaluation/permissions/permissions.go deleted file mode 100644 index 23ee6c2a7db..00000000000 --- a/checks/evaluation/permissions/permissions.go +++ /dev/null @@ -1,564 +0,0 @@ -// Copyright 2021 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 evaluation - -import ( - "embed" - "fmt" - "strings" - - "github.com/ossf/scorecard/v4/checker" - sce "github.com/ossf/scorecard/v4/errors" - "github.com/ossf/scorecard/v4/finding" - "github.com/ossf/scorecard/v4/remediation" -) - -//go:embed *.yml -var probes embed.FS - -type permissions struct { - topLevelWritePermissions map[string]bool - jobLevelWritePermissions map[string]bool -} - -var ( - stepsNoWriteID = "gitHubWorkflowPermissionsStepsNoWrite" - topNoWriteID = "gitHubWorkflowPermissionsTopNoWrite" -) - -type permissionLevel string - -const ( - // permissionLevelNone is a permission set to `none`. - permissionLevelNone permissionLevel = "none" - // permissionLevelRead is a permission set to `read`. - permissionLevelRead permissionLevel = "read" - // permissionLevelUnknown is for other kinds of alerts, mostly to support debug messages. - // TODO: remove it once we have implemented severity (#1874). - permissionLevelUnknown permissionLevel = "unknown" - // permissionLevelUndeclared is an undeclared permission. - permissionLevelUndeclared permissionLevel = "undeclared" - // permissionLevelWrite is a permission set to `write` for a permission we consider potentially dangerous. - permissionLevelWrite permissionLevel = "write" -) - -// permissionLocation represents a declaration type. -type permissionLocationType string - -const ( - // permissionLocationNil is in case the permission is nil. - permissionLocationNil permissionLocationType = "nil" - // permissionLocationNotDeclared is for undeclared permission. - permissionLocationNotDeclared permissionLocationType = "not declared" - // permissionLocationTop is top-level workflow permission. - permissionLocationTop permissionLocationType = "top" - // permissionLocationJob is job-level workflow permission. - permissionLocationJob permissionLocationType = "job" -) - -// permissionType represents a permission type. -type permissionType string - -const ( - // permissionTypeNone represents none permission type. - permissionTypeNone permissionType = "none" - // permissionTypeNone is the "all" github permission type. - permissionTypeAll permissionType = "all" - // permissionTypeNone is the "statuses" github permission type. - permissionTypeStatuses permissionType = "statuses" - // permissionTypeNone is the "checks" github permission type. - permissionTypeChecks permissionType = "checks" - // permissionTypeNone is the "security-events" github permission type. - permissionTypeSecurityEvents permissionType = "security-events" - // permissionTypeNone is the "deployments" github permission type. - permissionTypeDeployments permissionType = "deployments" - // permissionTypeNone is the "packages" github permission type. - permissionTypePackages permissionType = "packages" - // permissionTypeNone is the "actions" github permission type. - permissionTypeActions permissionType = "actions" -) - -// TokenPermissions applies the score policy for the Token-Permissions check. -func TokenPermissions(name string, c *checker.CheckRequest, r *checker.TokenPermissionsData) checker.CheckResult { - if r == nil { - e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data") - return checker.CreateRuntimeErrorResult(name, e) - } - - if r.NumTokens == 0 { - return checker.CreateInconclusiveResult(name, "no tokens found") - } - - // This is a temporary step that should be replaced by probes in ./probes - findings, err := rawToFindings(r) - if err != nil { - e := sce.WithMessage(sce.ErrScorecardInternal, "could not convert raw data to findings") - return checker.CreateRuntimeErrorResult(name, e) - } - - score, err := applyScorePolicy(findings, c) - if err != nil { - return checker.CreateRuntimeErrorResult(name, err) - } - - if score != checker.MaxResultScore { - return checker.CreateResultWithScore(name, - "detected GitHub workflow tokens with excessive permissions", score) - } - - return checker.CreateMaxScoreResult(name, - "GitHub workflow tokens follow principle of least privilege") -} - -// rawToFindings is a temporary step for converting the raw results -// to findings. This should be replaced by probes in ./probes. -func rawToFindings(results *checker.TokenPermissionsData) ([]finding.Finding, error) { - var findings []finding.Finding - - for _, r := range results.TokenPermissions { - var loc *finding.Location - if r.File != nil { - loc = &finding.Location{ - Type: r.File.Type, - Path: r.File.Path, - LineStart: newUint(r.File.Offset), - } - if r.File.Snippet != "" { - loc.Snippet = newStr(r.File.Snippet) - } - } - text, err := createText(r) - if err != nil { - return nil, err - } - - f, err := createFinding(r.LocationType, text, loc) - if err != nil { - return nil, err - } - - switch r.Type { - case checker.PermissionLevelNone: - f = f.WithOutcome(finding.OutcomePositive) - f = f.WithValue("PermissionLevel", string(permissionLevelNone)) - case checker.PermissionLevelRead: - f = f.WithOutcome(finding.OutcomePositive) - f = f.WithValue("PermissionLevel", string(permissionLevelRead)) - case checker.PermissionLevelUnknown: - f = f.WithValue("PermissionLevel", string(permissionLevelUnknown)) - f = f.WithOutcome(finding.OutcomeError) - case checker.PermissionLevelUndeclared: - var locationType permissionLocationType - //nolint:gocritic - if r.LocationType == nil { - locationType = permissionLocationNil - } else if *r.LocationType == checker.PermissionLocationTop { - locationType = permissionLocationTop - } else { - locationType = permissionLocationNotDeclared - } - permType := permTypeToEnum(r.Name) - f = f.WithValues(map[string]string{ - "PermissionLevel": string(permissionLevelUndeclared), - "LocationType": string(locationType), - "PermissionType": string(permType), - }) - case checker.PermissionLevelWrite: - var locationType permissionLocationType - switch *r.LocationType { - case checker.PermissionLocationTop: - locationType = permissionLocationTop - case checker.PermissionLocationJob: - locationType = permissionLocationJob - default: - locationType = permissionLocationNotDeclared - } - permType := permTypeToEnum(r.Name) - f = f.WithValues(map[string]string{ - "PermissionLevel": string(permissionLevelWrite), - "LocationType": string(locationType), - "PermissionType": string(permType), - }) - f = f.WithOutcome(finding.OutcomeNegative) - } - findings = append(findings, *f) - } - return findings, nil -} - -func permTypeToEnum(tokenName *string) permissionType { - if tokenName == nil { - return permissionTypeNone - } - switch *tokenName { - //nolint:goconst - case "all": - return permissionTypeAll - case "statuses": - return permissionTypeStatuses - case "checks": - return permissionTypeChecks - case "security-events": - return permissionTypeSecurityEvents - case "deployments": - return permissionTypeDeployments - case "contents": - return permissionTypePackages - case "actions": - return permissionTypeActions - default: - return permissionTypeNone - } -} - -func permTypeToName(permType string) *string { - var permName string - switch permissionType(permType) { - case permissionTypeAll: - permName = "all" - case permissionTypeStatuses: - permName = "statuses" - case permissionTypeChecks: - permName = "checks" - case permissionTypeSecurityEvents: - permName = "security-events" - case permissionTypeDeployments: - permName = "deployments" - case permissionTypePackages: - permName = "contents" - case permissionTypeActions: - permName = "actions" - default: - permName = "" - } - return &permName -} - -func createFinding(loct *checker.PermissionLocation, text string, loc *finding.Location) (*finding.Finding, error) { - probe := stepsNoWriteID - if loct == nil || *loct == checker.PermissionLocationTop { - probe = topNoWriteID - } - content, err := probes.ReadFile(probe + ".yml") - if err != nil { - return nil, fmt.Errorf("reading %v.yml: %w", probe, err) - } - f, err := finding.FromBytes(content, probe) - if err != nil { - return nil, - sce.WithMessage(sce.ErrScorecardInternal, err.Error()) - } - f = f.WithMessage(text) - if loc != nil { - f = f.WithLocation(loc) - } - return f, nil -} - -// avoid memory aliasing by returning a new copy. -func newUint(u uint) *uint { - return &u -} - -// avoid memory aliasing by returning a new copy. -func newStr(s string) *string { - return &s -} - -func applyScorePolicy(findings []finding.Finding, c *checker.CheckRequest) (int, error) { - // See list https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/. - // Note: there are legitimate reasons to use some of the permissions like checks, deployments, etc. - // in CI/CD systems https://docs.travis-ci.com/user/github-oauth-scopes/. - - hm := make(map[string]permissions) - dl := c.Dlogger - //nolint:errcheck - remediationMetadata, _ := remediation.New(c) - negativeProbeResults := map[string]bool{ - stepsNoWriteID: false, - topNoWriteID: false, - } - - for i := range findings { - f := &findings[i] - pLevel := permissionLevel(f.Values["PermissionLevel"]) - switch pLevel { - case permissionLevelNone, permissionLevelRead: - dl.Info(&checker.LogMessage{ - Finding: f, - }) - case permissionLevelUnknown: - dl.Debug(&checker.LogMessage{ - Finding: f, - }) - - case permissionLevelUndeclared: - switch permissionLocationType(f.Values["LocationType"]) { - case permissionLocationNil: - return checker.InconclusiveResultScore, - sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil") - case permissionLocationTop: - warnWithRemediation(dl, remediationMetadata, f, negativeProbeResults) - default: - // We warn only for top-level. - dl.Debug(&checker.LogMessage{ - Finding: f, - }) - } - - // Group results by workflow name for score computation. - if err := updateWorkflowHashMap(hm, f); err != nil { - return checker.InconclusiveResultScore, err - } - - case permissionLevelWrite: - warnWithRemediation(dl, remediationMetadata, f, negativeProbeResults) - - // Group results by workflow name for score computation. - if err := updateWorkflowHashMap(hm, f); err != nil { - return checker.InconclusiveResultScore, err - } - } - } - - if err := reportDefaultFindings(findings, c.Dlogger, negativeProbeResults); err != nil { - return checker.InconclusiveResultScore, err - } - return calculateScore(hm), nil -} - -func reportDefaultFindings(results []finding.Finding, - dl checker.DetailLogger, negativeProbeResults map[string]bool, -) error { - // Workflow files found, report positive findings if no - // negative findings were found. - // NOTE: we don't consider probe `topNoWriteID` - // because positive results are already reported. - found := negativeProbeResults[stepsNoWriteID] - if !found { - text := fmt.Sprintf("no %s write permissions found", checker.PermissionLocationJob) - if err := reportFinding(stepsNoWriteID, - text, finding.OutcomePositive, dl); err != nil { - return err - } - } - - return nil -} - -func reportFinding(probe, text string, o finding.Outcome, dl checker.DetailLogger) error { - content, err := probes.ReadFile(probe + ".yml") - if err != nil { - return fmt.Errorf("%w", err) - } - f, err := finding.FromBytes(content, probe) - if err != nil { - return sce.WithMessage(sce.ErrScorecardInternal, err.Error()) - } - f = f.WithMessage(text).WithOutcome(o) - dl.Info(&checker.LogMessage{ - Finding: f, - }) - return nil -} - -func warnWithRemediation(logger checker.DetailLogger, - rem *remediation.RemediationMetadata, - f *finding.Finding, - negativeProbeResults map[string]bool, -) { - if f.Location != nil && f.Location.Path != "" { - f = f.WithRemediationMetadata(map[string]string{ - "repo": rem.Repo, - "branch": rem.Branch, - "workflow": strings.TrimPrefix(f.Location.Path, ".github/workflows/"), - }) - } - logger.Warn(&checker.LogMessage{ - Finding: f, - }) - - // Record that we found a negative result. - negativeProbeResults[f.Probe] = true -} - -func recordPermissionWrite(hm map[string]permissions, path string, - locType permissionLocationType, permType string, -) { - if _, exists := hm[path]; !exists { - hm[path] = permissions{ - topLevelWritePermissions: make(map[string]bool), - jobLevelWritePermissions: make(map[string]bool), - } - } - - // Select the hash map to update. - m := hm[path].jobLevelWritePermissions - if locType == permissionLocationTop { - m = hm[path].topLevelWritePermissions - } - - // Set the permission name to record. - permName := permTypeToName(permType) - name := "all" - if permName != nil && *permName != "" { - name = *permName - } - m[name] = true -} - -func updateWorkflowHashMap(hm map[string]permissions, f *finding.Finding) error { - if _, ok := f.Values["LocationType"]; !ok { - return sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil") - } - - if f.Location == nil || f.Location.Path == "" { - return sce.WithMessage(sce.ErrScorecardInternal, "path is not set") - } - - if permissionLevel(f.Values["PermissionLevel"]) != permissionLevelWrite && - permissionLevel(f.Values["PermissionLevel"]) != permissionLevelUndeclared { - return nil - } - plt := permissionLocationType(f.Values["LocationType"]) - recordPermissionWrite(hm, f.Location.Path, plt, f.Values["PermissionType"]) - - return nil -} - -func createText(t checker.TokenPermission) (string, error) { - // By default, use the message already present. - if t.Msg != nil { - return *t.Msg, nil - } - - // Ensure there's no implementation bug. - if t.LocationType == nil { - return "", sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil") - } - - // Use a different text depending on the type. - if t.Type == checker.PermissionLevelUndeclared { - return fmt.Sprintf("no %s permission defined", *t.LocationType), nil - } - - if t.Value == nil { - return "", sce.WithMessage(sce.ErrScorecardInternal, "Value fields is nil") - } - - if t.Name == nil { - return fmt.Sprintf("%s permissions set to '%v'", *t.LocationType, - *t.Value), nil - } - - return fmt.Sprintf("%s '%v' permission set to '%v'", *t.LocationType, - *t.Name, *t.Value), nil -} - -// Calculate the score. -func calculateScore(result map[string]permissions) int { - // See list https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/. - // Note: there are legitimate reasons to use some of the permissions like checks, deployments, etc. - // in CI/CD systems https://docs.travis-ci.com/user/github-oauth-scopes/. - - // Start with a perfect score. - score := float32(checker.MaxResultScore) - - // Retrieve the overall results. - for _, perms := range result { - // If no top level permissions are defined, all the permissions - // are enabled by default. In this case, - if permissionIsPresentInTopLevel(perms, "all") { - if permissionIsPresentInRunLevel(perms, "all") { - // ... give lowest score if no run level permissions are defined either. - return checker.MinResultScore - } - // ... reduce score if run level permissions are defined. - score -= 0.5 - } - - // status: https://docs.github.com/en/rest/reference/repos#statuses. - // May allow an attacker to change the result of pre-submit and get a PR merged. - // Low risk: -0.5. - if permissionIsPresentInTopLevel(perms, "statuses") { - score -= 0.5 - } - - // checks. - // May allow an attacker to edit checks to remove pre-submit and introduce a bug. - // Low risk: -0.5. - if permissionIsPresentInTopLevel(perms, "checks") { - score -= 0.5 - } - - // secEvents. - // May allow attacker to read vuln reports before patch available. - // Low risk: -1 - if permissionIsPresentInTopLevel(perms, "security-events") { - score-- - } - - // deployments: https://docs.github.com/en/rest/reference/repos#deployments. - // May allow attacker to charge repo owner by triggering VM runs, - // and tiny chance an attacker can trigger a remote - // service with code they own if server accepts code/location var unsanitized. - // Low risk: -1 - if permissionIsPresentInTopLevel(perms, "deployments") { - score-- - } - - // contents. - // Allows attacker to commit unreviewed code. - // High risk: -10 - if permissionIsPresentInTopLevel(perms, "contents") { - score -= checker.MaxResultScore - } - - // packages: https://docs.github.com/en/packages/learn-github-packages/about-permissions-for-github-packages. - // Allows attacker to publish packages. - // High risk: -10 - if permissionIsPresentInTopLevel(perms, "packages") { - score -= checker.MaxResultScore - } - - // actions. - // May allow an attacker to steal GitHub secrets by approving to run an action that needs approval. - // High risk: -10 - if permissionIsPresentInTopLevel(perms, "actions") { - score -= checker.MaxResultScore - } - - if score < checker.MinResultScore { - break - } - } - - // We're done, calculate the final score. - if score < checker.MinResultScore { - return checker.MinResultScore - } - - return int(score) -} - -func permissionIsPresentInTopLevel(perms permissions, name string) bool { - _, ok := perms.topLevelWritePermissions[name] - return ok -} - -func permissionIsPresentInRunLevel(perms permissions, name string) bool { - _, ok := perms.jobLevelWritePermissions[name] - return ok -} diff --git a/checks/permissions.go b/checks/permissions.go index abb3f902558..8d7fbe73fdf 100644 --- a/checks/permissions.go +++ b/checks/permissions.go @@ -16,9 +16,11 @@ package checks import ( "github.com/ossf/scorecard/v4/checker" - evaluation "github.com/ossf/scorecard/v4/checks/evaluation/permissions" + "github.com/ossf/scorecard/v4/checks/evaluation" "github.com/ossf/scorecard/v4/checks/raw" sce "github.com/ossf/scorecard/v4/errors" + "github.com/ossf/scorecard/v4/probes" + "github.com/ossf/scorecard/v4/probes/zrunner" ) // CheckTokenPermissions is the exported name for Token-Permissions check. @@ -44,11 +46,17 @@ func TokenPermissions(c *checker.CheckRequest) checker.CheckResult { return checker.CreateRuntimeErrorResult(CheckTokenPermissions, e) } - // Return raw results. - if c.RawResults != nil { - c.RawResults.TokenPermissionsResults = rawData + // Set the raw results. + pRawResults := getRawResults(c) + pRawResults.TokenPermissionsResults = rawData + + // Evaluate the probes. + findings, err := zrunner.Run(pRawResults, probes.TokenPermissions) + if err != nil { + e := sce.WithMessage(sce.ErrScorecardInternal, err.Error()) + return checker.CreateRuntimeErrorResult(CheckTokenPermissions, e) } // Return the score evaluation. - return evaluation.TokenPermissions(CheckTokenPermissions, c, &rawData) + return evaluation.TokenPermissions(CheckTokenPermissions, findings, c.Dlogger) } diff --git a/checks/permissions_test.go b/checks/permissions_test.go index 16c7b9bc4ab..5fa7a98c64d 100644 --- a/checks/permissions_test.go +++ b/checks/permissions_test.go @@ -109,7 +109,7 @@ func TestGithubTokenPermissions(t *testing.T) { Error: nil, Score: checker.MinResultScore, NumberOfWarn: 1, - NumberOfInfo: 1, + NumberOfInfo: 0, NumberOfDebug: 5, }, }, diff --git a/checks/raw/permissions.go b/checks/raw/permissions.go index c3c7132db44..7c1d8c46507 100644 --- a/checks/raw/permissions.go +++ b/checks/raw/permissions.go @@ -104,6 +104,7 @@ var validateGitHubActionTokenPermissions fileparser.DoWhileTrueOnFileContent = f // 2. Run-level permission definitions, // see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idpermissions. ignoredPermissions := createIgnoredPermissions(workflow, path, pdata) + if err := validatejobLevelPermissions(workflow, path, pdata, ignoredPermissions); err != nil { return false, err } diff --git a/probes/entries.go b/probes/entries.go index 8c355cbeb3e..d18db1cb61c 100644 --- a/probes/entries.go +++ b/probes/entries.go @@ -33,10 +33,12 @@ import ( "github.com/ossf/scorecard/v4/probes/hasFSFOrOSIApprovedLicense" "github.com/ossf/scorecard/v4/probes/hasLicenseFile" "github.com/ossf/scorecard/v4/probes/hasLicenseFileAtTopDir" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionUnknown" "github.com/ossf/scorecard/v4/probes/hasOSVVulnerabilities" "github.com/ossf/scorecard/v4/probes/hasOpenSSFBadge" "github.com/ossf/scorecard/v4/probes/hasRecentCommits" "github.com/ossf/scorecard/v4/probes/issueActivityByProjectMember" + "github.com/ossf/scorecard/v4/probes/jobLevelPermissions" "github.com/ossf/scorecard/v4/probes/notArchived" "github.com/ossf/scorecard/v4/probes/notCreatedRecently" "github.com/ossf/scorecard/v4/probes/packagedWithAutomatedWorkflow" @@ -59,6 +61,7 @@ import ( "github.com/ossf/scorecard/v4/probes/toolDependabotInstalled" "github.com/ossf/scorecard/v4/probes/toolPyUpInstalled" "github.com/ossf/scorecard/v4/probes/toolRenovateInstalled" + "github.com/ossf/scorecard/v4/probes/topLevelPermissions" "github.com/ossf/scorecard/v4/probes/webhooksUseSecrets" ) @@ -150,6 +153,11 @@ var ( PinnedDependencies = []ProbeImpl{ pinsDependencies.Run, } + TokenPermissions = []ProbeImpl{ + hasNoGitHubWorkflowPermissionUnknown.Run, + jobLevelPermissions.Run, + topLevelPermissions.Run, + } // Probes which aren't included by any checks. // These still need to be listed so they can be called with --probes. diff --git a/checks/evaluation/permissions/gitHubWorkflowPermissionsTopNoWrite.yml b/probes/hasNoGitHubWorkflowPermissionUnknown/def.yml similarity index 58% rename from checks/evaluation/permissions/gitHubWorkflowPermissionsTopNoWrite.yml rename to probes/hasNoGitHubWorkflowPermissionUnknown/def.yml index 91b2f117c93..5f2b8593942 100644 --- a/checks/evaluation/permissions/gitHubWorkflowPermissionsTopNoWrite.yml +++ b/probes/hasNoGitHubWorkflowPermissionUnknown/def.yml @@ -1,4 +1,4 @@ -# Copyright 2023 OpenSSF Scorecard Authors +# 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. @@ -12,24 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -id: gitHubWorkflowPermissionsTopNoWrite -short: Checks that GitHub workflows do not have default write permissions +id: hasNoGitHubWorkflowPermissionUnknown +short: Checks that GitHub workflows have workflows with unknown permissions motivation: > - If no permissions are declared, a workflow's GitHub token's permissions default to write for all scopes. - This include write permissions to push to the repository, to read encrypted secrets, etc. - For more information, see https://docs.github.com/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token. + Unknown permissions may be a result of a bug or another error from fetching the permission levels. implementation: > - The rule is implemented by checking whether the `permissions` keyword is defined at the top of the workflow, - and that no write permissions are given. + The probe checks the permission levels of a projects workflows and collects the workflows that have unknown permissions. +outcome: + - The probe returns 1 negative outcome per workflow without unknown permission level(s). + - The probe returns 1 positive outcome if the project has no workflows with unknown permission levels. remediation: effort: Low text: - - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?enable=permissions + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions - Tick the 'Restrict permissions for GITHUB_TOKEN' - Untick other options - "NOTE: If you want to resolve multiple issues at once, you can visit https://app.stepsecurity.io/securerepo instead." markdown: - - Visit [https://app.stepsecurity.io/secureworkflow](https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?enable=permissions). + - Visit [https://app.stepsecurity.io/secureworkflow](https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions). - Tick the 'Restrict permissions for GITHUB_TOKEN' - Untick other options - "NOTE: If you want to resolve multiple issues at once, you can visit [https://app.stepsecurity.io/securerepo](https://app.stepsecurity.io/securerepo) instead." diff --git a/probes/hasNoGitHubWorkflowPermissionUnknown/impl.go b/probes/hasNoGitHubWorkflowPermissionUnknown/impl.go new file mode 100644 index 00000000000..0c6aad3e1d0 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionUnknown/impl.go @@ -0,0 +1,75 @@ +// 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. + +//nolint:stylecheck +package hasNoGitHubWorkflowPermissionUnknown + +import ( + "embed" + "fmt" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/uerror" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionUnknown" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + if raw == nil { + return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil) + } + + results := raw.TokenPermissionsResults + var findings []finding.Finding + + if results.NumTokens == 0 { + f, err := finding.NewWith(fs, Probe, + "No token permissions found", + nil, finding.OutcomeNotApplicable) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + return findings, Probe, nil + } + + for _, r := range results.TokenPermissions { + if r.Type != checker.PermissionLevelUnknown { + continue + } + + // Create finding + f, err := permissions.CreateNegativeFinding(r, Probe, fs, raw.Metadata.Metadata) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + } + + if len(findings) == 0 { + f, err := finding.NewWith(fs, Probe, + "no workflows with unknown permissions", + nil, finding.OutcomePositive) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + } + return findings, Probe, nil +} diff --git a/probes/hasNoGitHubWorkflowPermissionUnknown/impl_test.go b/probes/hasNoGitHubWorkflowPermissionUnknown/impl_test.go new file mode 100644 index 00000000000..97603167579 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionUnknown/impl_test.go @@ -0,0 +1,98 @@ +// 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. + +//nolint:stylecheck +package hasNoGitHubWorkflowPermissionUnknown + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + permLoc := checker.PermissionLocationTop + value := "value" + tests := []test.TestData{ + { + Name: "No Tokens", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 0, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNotApplicable, + }, + }, + { + Name: "Correct permission level", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + Type: checker.PermissionLevelUnknown, + LocationType: &permLoc, + Value: &value, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNegative, + }, + }, + { + Name: "Incorrect permission level", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + Type: checker.PermissionLevelRead, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomePositive, + }, + }, + } + 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() + + findings, s, err := Run(tt.Raw) + if !cmp.Equal(tt.Err, err, cmpopts.EquateErrors()) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.Err, err, cmpopts.EquateErrors())) + } + if err != nil { + return + } + if diff := cmp.Diff(Probe, s); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + test.AssertOutcomes(t, findings, tt.Outcomes) + }) + } +} diff --git a/probes/internal/utils/permissions/permissions.go b/probes/internal/utils/permissions/permissions.go new file mode 100644 index 00000000000..bc22f904b0f --- /dev/null +++ b/probes/internal/utils/permissions/permissions.go @@ -0,0 +1,169 @@ +// 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 permissions + +import ( + "embed" + "fmt" + "strings" + + "github.com/ossf/scorecard/v4/checker" + sce "github.com/ossf/scorecard/v4/errors" + "github.com/ossf/scorecard/v4/finding" +) + +func createText(t checker.TokenPermission) (string, error) { + // By default, use the message already present. + if t.Msg != nil { + return *t.Msg, nil + } + + // Ensure there's no implementation bug. + if t.LocationType == nil { + return "", sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil") + } + + // Use a different text depending on the type. + if t.Type == checker.PermissionLevelUndeclared { + return fmt.Sprintf("no %s permission defined", *t.LocationType), nil + } + + if t.Value == nil { + return "", sce.WithMessage(sce.ErrScorecardInternal, "Value fields is nil") + } + + if t.Name == nil { + return fmt.Sprintf("%s permissions set to '%v'", *t.LocationType, + *t.Value), nil + } + + return fmt.Sprintf("%s '%v' permission set to '%v'", *t.LocationType, + *t.Name, *t.Value), nil +} + +func CreateNegativeFinding(r checker.TokenPermission, + probe string, + fs embed.FS, + metadata map[string]string, +) (*finding.Finding, error) { + // Create finding + text, err := createText(r) + if err != nil { + return nil, fmt.Errorf("create finding: %w", err) + } + f, err := finding.NewWith(fs, probe, + text, nil, finding.OutcomeNegative) + if err != nil { + return nil, fmt.Errorf("create finding: %w", err) + } + + if r.File != nil { + f = f.WithLocation(r.File.Location()) + workflowPath := strings.TrimPrefix(f.Location.Path, ".github/workflows/") + f = f.WithRemediationMetadata(map[string]string{"workflow": workflowPath}) + } + if metadata != nil { + f = f.WithRemediationMetadata(metadata) + } + + if r.Name != nil { + f = f.WithValue("tokenName", *r.Name) + } + f = f.WithValue("permissionLevel", string(r.Type)) + return f, nil +} + +func ReadPositiveLevelFinding(probe string, + fs embed.FS, + r checker.TokenPermission, + metadata map[string]string, +) (*finding.Finding, error) { + f, err := finding.NewWith(fs, probe, + "found token with 'read' permissions", + nil, finding.OutcomePositive) + if err != nil { + return nil, fmt.Errorf("%w", err) + } + if r.File != nil { + f = f.WithLocation(r.File.Location()) + workflowPath := strings.TrimPrefix(f.Location.Path, ".github/workflows/") + f = f.WithRemediationMetadata(map[string]string{"workflow": workflowPath}) + } + if metadata != nil { + f = f.WithRemediationMetadata(metadata) + } + + f = f.WithValue("permissionLevel", "read") + return f, nil +} + +func CreateNoneFinding(probe string, + fs embed.FS, + r checker.TokenPermission, + metadata map[string]string, +) (*finding.Finding, error) { + // Create finding + f, err := finding.NewWith(fs, probe, + "found token with 'none' permissions", + nil, finding.OutcomeNegative) + if err != nil { + return nil, fmt.Errorf("create finding: %w", err) + } + if r.File != nil { + f = f.WithLocation(r.File.Location()) + workflowPath := strings.TrimPrefix(f.Location.Path, ".github/workflows/") + f = f.WithRemediationMetadata(map[string]string{"workflow": workflowPath}) + } + if metadata != nil { + f = f.WithRemediationMetadata(metadata) + } + + f = f.WithValue("permissionLevel", string(r.Type)) + return f, nil +} + +func CreateUndeclaredFinding(probe string, + fs embed.FS, + r checker.TokenPermission, + metadata map[string]string, +) (*finding.Finding, error) { + var f *finding.Finding + var err error + switch { + case r.LocationType == nil: + f, err = finding.NewWith(fs, probe, + "could not determine the location type", + nil, finding.OutcomeNotApplicable) + if err != nil { + return nil, fmt.Errorf("create finding: %w", err) + } + case *r.LocationType == checker.PermissionLocationTop, + *r.LocationType == checker.PermissionLocationJob: + // Create finding + f, err = CreateNegativeFinding(r, probe, fs, metadata) + if err != nil { + return nil, fmt.Errorf("create finding: %w", err) + } + default: + f, err = finding.NewWith(fs, probe, + "could not determine the location type", + nil, finding.OutcomeError) + if err != nil { + return nil, fmt.Errorf("create finding: %w", err) + } + } + f = f.WithValue("permissionLevel", string(r.Type)) + return f, nil +} diff --git a/probes/internal/utils/test/test.go b/probes/internal/utils/test/test.go index 484a4949bc3..9d1d150bc91 100644 --- a/probes/internal/utils/test/test.go +++ b/probes/internal/utils/test/test.go @@ -17,6 +17,8 @@ package test import ( "testing" + "github.com/ossf/scorecard/v4/checker" + sce "github.com/ossf/scorecard/v4/errors" "github.com/ossf/scorecard/v4/finding" ) @@ -32,3 +34,147 @@ func AssertOutcomes(t *testing.T, got []finding.Finding, want []finding.Outcome) } } } + +// Tests for permissions-probes. +type TestData struct { + Name string + Err error + Raw *checker.RawResults + Outcomes []finding.Outcome +} + +func GetTests(locationType checker.PermissionLocation, + permissionType checker.PermissionLevel, + tokenName string, +) []TestData { + name := tokenName // Should come from each probe test. + value := "value" + var wrongPermissionLocation checker.PermissionLocation + if locationType == checker.PermissionLocationTop { + wrongPermissionLocation = checker.PermissionLocationJob + } else { + wrongPermissionLocation = checker.PermissionLocationTop + } + + return []TestData{ + { + Name: "No Tokens", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 0, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNotApplicable, + }, + }, + { + Name: "Correct name", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + LocationType: &locationType, + Name: &name, + Value: &value, + Msg: nil, + Type: permissionType, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNegative, + }, + }, + { + Name: "Two tokens", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 2, + TokenPermissions: []checker.TokenPermission{ + { + LocationType: &locationType, + Name: &name, + Value: &value, + Msg: nil, + Type: permissionType, + }, + { + LocationType: &locationType, + Name: &name, + Value: &value, + Msg: nil, + Type: permissionType, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNegative, finding.OutcomeNegative, + }, + }, + { + Name: "Value is nil - Everything else correct", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + LocationType: &locationType, + Name: &name, + Value: nil, + Msg: nil, + Type: permissionType, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNegative, + }, + Err: sce.ErrScorecardInternal, + }, + { + Name: "Wrong locationType wrong type", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + LocationType: &wrongPermissionLocation, + Name: &name, + Value: nil, + Msg: nil, + Type: checker.PermissionLevel("999"), + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomePositive, + }, + }, + { + Name: "Wrong locationType correct type", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + LocationType: &wrongPermissionLocation, + Name: &name, + Value: nil, + Msg: nil, + Type: permissionType, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomePositive, + }, + }, + } +} diff --git a/probes/jobLevelPermissions/def.yml b/probes/jobLevelPermissions/def.yml new file mode 100644 index 00000000000..706c183ccf0 --- /dev/null +++ b/probes/jobLevelPermissions/def.yml @@ -0,0 +1,35 @@ +# 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. + +id: jobLevelPermissions +short: Checks that GitHub workflows do not have "write" permissions at the "job" level. +motivation: > + In some circumstances, having "write" permissions at the "job" level may enable attackers to escalate privileges. +implementation: > + The probe checks the permission level, the workflow type and the permission type of each workflow in the project. +outcome: + - The probe returns 1 negative outcome per workflow with "write" permissions at the "job" level. + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "job" level. +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions + - Tick the 'Restrict permissions for GITHUB_TOKEN' + - Untick other options + - "NOTE: If you want to resolve multiple issues at once, you can visit https://app.stepsecurity.io/securerepo instead." + markdown: + - Visit [https://app.stepsecurity.io/secureworkflow](https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions). + - Tick the 'Restrict permissions for GITHUB_TOKEN' + - Untick other options + - "NOTE: If you want to resolve multiple issues at once, you can visit [https://app.stepsecurity.io/securerepo](https://app.stepsecurity.io/securerepo) instead." diff --git a/probes/jobLevelPermissions/impl.go b/probes/jobLevelPermissions/impl.go new file mode 100644 index 00000000000..e4a5b030ae6 --- /dev/null +++ b/probes/jobLevelPermissions/impl.go @@ -0,0 +1,109 @@ +// 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. + +//nolint:stylecheck +package jobLevelPermissions + +import ( + "embed" + "fmt" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/uerror" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "jobLevelPermissions" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + if raw == nil { + return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil) + } + + results := raw.TokenPermissionsResults + var findings []finding.Finding + + if results.NumTokens == 0 { + f, err := finding.NewWith(fs, Probe, + "No token permissions found", + nil, finding.OutcomeNotApplicable) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + return findings, Probe, nil + } + + for _, r := range results.TokenPermissions { + if r.LocationType == nil { + continue + } + if *r.LocationType != checker.PermissionLocationJob { + continue + } + + switch r.Type { + case checker.PermissionLevelNone: + f, err := permissions.CreateNoneFinding(Probe, fs, r, raw.Metadata.Metadata) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + continue + case checker.PermissionLevelUndeclared: + f, err := permissions.CreateUndeclaredFinding(Probe, fs, r, raw.Metadata.Metadata) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + continue + case checker.PermissionLevelRead: + f, err := permissions.ReadPositiveLevelFinding(Probe, fs, r, raw.Metadata.Metadata) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + continue + default: + // to satisfy linter + } + + if r.Name == nil { + continue + } + + f, err := permissions.CreateNegativeFinding(r, Probe, fs, raw.Metadata.Metadata) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + f = f.WithValue("permissionLevel", string(r.Type)) + f = f.WithValue("tokenName", *r.Name) + findings = append(findings, *f) + } + + if len(findings) == 0 { + f, err := finding.NewWith(fs, Probe, + "no job-level permissions found", + nil, finding.OutcomePositive) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + } + return findings, Probe, nil +} diff --git a/probes/jobLevelPermissions/impl_test.go b/probes/jobLevelPermissions/impl_test.go new file mode 100644 index 00000000000..6909f700c2e --- /dev/null +++ b/probes/jobLevelPermissions/impl_test.go @@ -0,0 +1,57 @@ +// 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. + +//nolint:stylecheck +package jobLevelPermissions + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "actions") + + tests = append(tests, test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "checks")...) + tests = append(tests, test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "contents")...) + tests = append(tests, test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "deployments")...) + tests = append(tests, test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "packages")...) + tests = append(tests, test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "security-events")...) + + 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() + + findings, s, err := Run(tt.Raw) + if !cmp.Equal(tt.Err, err, cmpopts.EquateErrors()) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.Err, err, cmpopts.EquateErrors())) + } + if err != nil { + return + } + if diff := cmp.Diff(Probe, s); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + test.AssertOutcomes(t, findings, tt.Outcomes) + }) + } +} diff --git a/probes/topLevelPermissions/def.yml b/probes/topLevelPermissions/def.yml new file mode 100644 index 00000000000..44c122f8d73 --- /dev/null +++ b/probes/topLevelPermissions/def.yml @@ -0,0 +1,35 @@ +# 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. + +id: topLevelPermissions +short: Checks that the project does not have any top-level write permissions in its workflows. +motivation: > + In some circumstances, having "write" permissions at the "top" level may enable attackers to escalate privileges. +implementation: > + The probe checks the permission level, the workflow type and the permission type of each workflow in the project. +outcome: + - The probe returns 1 negative outcome per workflow with "write" permissions at the "top" level. + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "top" level. +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions + - Tick the 'Restrict permissions for GITHUB_TOKEN' + - Untick other options + - "NOTE: If you want to resolve multiple issues at once, you can visit https://app.stepsecurity.io/securerepo instead." + markdown: + - Visit [https://app.stepsecurity.io/secureworkflow](https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions). + - Tick the 'Restrict permissions for GITHUB_TOKEN' + - Untick other options + - "NOTE: If you want to resolve multiple issues at once, you can visit [https://app.stepsecurity.io/securerepo](https://app.stepsecurity.io/securerepo) instead." diff --git a/probes/topLevelPermissions/impl.go b/probes/topLevelPermissions/impl.go new file mode 100644 index 00000000000..41425f28b1c --- /dev/null +++ b/probes/topLevelPermissions/impl.go @@ -0,0 +1,118 @@ +// 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. + +//nolint:stylecheck +package topLevelPermissions + +import ( + "embed" + "fmt" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/uerror" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "topLevelPermissions" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + if raw == nil { + return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil) + } + + results := raw.TokenPermissionsResults + var findings []finding.Finding + + if results.NumTokens == 0 { + f, err := finding.NewWith(fs, Probe, + "No token permissions found", + nil, finding.OutcomeNotApplicable) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + return findings, Probe, nil + } + + for _, r := range results.TokenPermissions { + if r.LocationType == nil { + continue + } + if *r.LocationType != checker.PermissionLocationTop { + continue + } + + switch r.Type { + case checker.PermissionLevelNone: + f, err := permissions.CreateNoneFinding(Probe, fs, r, raw.Metadata.Metadata) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + continue + case checker.PermissionLevelUndeclared: + f, err := permissions.CreateUndeclaredFinding(Probe, fs, r, raw.Metadata.Metadata) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + continue + case checker.PermissionLevelRead: + f, err := permissions.ReadPositiveLevelFinding(Probe, fs, r, raw.Metadata.Metadata) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + continue + default: + // to satisfy linter + } + + tokenName := "" + switch { + case r.Name == nil && r.Value == nil: + continue + case r.Value != nil && *r.Value == "write-all": + tokenName = *r.Value + case r.Name != nil: + tokenName = *r.Name + default: + continue + } + + // Create finding + f, err := permissions.CreateNegativeFinding(r, Probe, fs, raw.Metadata.Metadata) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + f = f.WithValue("permissionLevel", string(r.Type)) + f = f.WithValue("tokenName", tokenName) + findings = append(findings, *f) + } + + if len(findings) == 0 { + f, err := finding.NewWith(fs, Probe, + "no job-level permissions found", + nil, finding.OutcomePositive) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + } + return findings, Probe, nil +} diff --git a/probes/topLevelPermissions/impl_test.go b/probes/topLevelPermissions/impl_test.go new file mode 100644 index 00000000000..e58ff21ed5c --- /dev/null +++ b/probes/topLevelPermissions/impl_test.go @@ -0,0 +1,57 @@ +// 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. + +//nolint:stylecheck +package topLevelPermissions + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "actions") + + tests = append(tests, test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "checks")...) + tests = append(tests, test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "contents")...) + tests = append(tests, test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "deployments")...) + tests = append(tests, test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "packages")...) + tests = append(tests, test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "security-events")...) + + 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() + + findings, s, err := Run(tt.Raw) + if !cmp.Equal(tt.Err, err, cmpopts.EquateErrors()) { + t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.Err, err, cmpopts.EquateErrors())) + } + if err != nil { + return + } + if diff := cmp.Diff(Probe, s); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + test.AssertOutcomes(t, findings, tt.Outcomes) + }) + } +} From e780e089f512f12cd1fc3d090a2424f186ac1a78 Mon Sep 17 00:00:00 2001 From: Spencer Schrock Date: Fri, 22 Mar 2024 11:14:57 -0700 Subject: [PATCH 2/2] :seedling: polish scorecard workflow for use as example workflow (#3969) This updates the version comments, adds some explanatory comments, and generally makes it better. The intent is to use this file as an example for the Scorecard Action repo so it remains up-to-date. Signed-off-by: Spencer Schrock --- .github/workflows/scorecard-analysis.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml index 093312b10c4..dbddc000cd4 100644 --- a/.github/workflows/scorecard-analysis.yml +++ b/.github/workflows/scorecard-analysis.yml @@ -7,8 +7,6 @@ on: schedule: # Weekly on Saturdays. - cron: '30 1 * * 6' -# pull_request: -# branches: [main] permissions: read-all @@ -17,19 +15,22 @@ jobs: name: Scorecard analysis runs-on: ubuntu-latest permissions: + # Needed for Code scanning upload security-events: write + # Needed for GitHub OIDC token if publish_results is true id-token: write steps: - name: "Checkout code" uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 with: results_file: results.sarif results_format: sarif - repo_token: ${{ secrets.GITHUB_TOKEN }} # Scorecard team runs a weekly scan of public GitHub repos, # see https://github.com/ossf/scorecard#public-data. # Setting `publish_results: true` helps us scale by leveraging your workflow to @@ -37,16 +38,19 @@ jobs: # And it's free for you! publish_results: true + # Upload the results as artifacts (optional). Commenting out will disable + # uploads of run results in SARIF format to the repository Actions tab. # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts - # Optional. - name: "Upload artifact" - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v3 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: SARIF file path: results.sarif retention-days: 5 - - name: "Upload SARIF results" - uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v1 + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@83a02f7883b12e0e4e1a146174f5e2292a01e601 # v2.16.4 with: sarif_file: results.sarif