diff --git a/checker/raw_result.go b/checker/raw_result.go index afeb848b7f63..874f3dd86cfd 100644 --- a/checker/raw_result.go +++ b/checker/raw_result.go @@ -420,6 +420,7 @@ const ( type TokenPermission struct { Job *WorkflowJob LocationType *PermissionLocation + Remediation *rule.Remediation Name *string Value *string File *File diff --git a/checks/evaluation/permissions.go b/checks/evaluation/permissions.go new file mode 100644 index 000000000000..d2c8dca4808d --- /dev/null +++ b/checks/evaluation/permissions.go @@ -0,0 +1,315 @@ +// 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/hasGitHubWorkflowPermissionNone" + "github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionRead" + "github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionUndeclared" + "github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionUnknown" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteActionsJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteActionsTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteAllJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteAllTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteChecksJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteChecksTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteContentsJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteContentsTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWritePackagesJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWritePackagesTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteStatusesTop" +) + +// TokenPermissions applies the score policy for the Token-Permissions check. +func TokenPermissions(name string, + findings []finding.Finding, + dl checker.DetailLogger, +) checker.CheckResult { + expectedProbes := []string{ + hasNoGitHubWorkflowPermissionWriteActionsTop.Probe, + hasNoGitHubWorkflowPermissionWriteAllTop.Probe, + hasNoGitHubWorkflowPermissionWriteChecksTop.Probe, + hasNoGitHubWorkflowPermissionWriteContentsTop.Probe, + hasNoGitHubWorkflowPermissionWriteDeploymentsTop.Probe, + hasNoGitHubWorkflowPermissionWritePackagesTop.Probe, + hasNoGitHubWorkflowPermissionWriteSecurityEventsTop.Probe, + hasNoGitHubWorkflowPermissionWriteStatusesTop.Probe, + hasGitHubWorkflowPermissionUnknown.Probe, + hasGitHubWorkflowPermissionNone.Probe, + hasGitHubWorkflowPermissionRead.Probe, + hasGitHubWorkflowPermissionUndeclared.Probe, + hasNoGitHubWorkflowPermissionWriteAllJob.Probe, + hasNoGitHubWorkflowPermissionWriteSecurityEventsJob.Probe, + hasNoGitHubWorkflowPermissionWriteActionsJob.Probe, + hasNoGitHubWorkflowPermissionWriteContentsJob.Probe, + hasNoGitHubWorkflowPermissionWritePackagesJob.Probe, + hasNoGitHubWorkflowPermissionWriteChecksJob.Probe, + hasNoGitHubWorkflowPermissionWriteDeploymentsJob.Probe, + hasNoGitHubWorkflowPermissionWriteStatusesJob.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" and "read" permissions. + if f.Outcome == finding.OutcomePositive && + (f.Probe == hasGitHubWorkflowPermissionNone.Probe || + f.Probe == hasGitHubWorkflowPermissionRead.Probe) { + dl.Info(&checker.LogMessage{ + Finding: f, + }) + } + + if notAvailableOrNotApplicable(f, dl) { + return checker.CreateInconclusiveResult(name, "Token permissions are not available") + } + + // If there are no TokenPermissions + if f.Outcome == finding.OutcomeNotAvailable { + return checker.CreateInconclusiveResult(name, "No tokens found") + } + + if f.Outcome != finding.OutcomeNegative { + continue + } + fPath := f.Location.Path + + addProbeToMaps(fPath, undeclaredPermissions, hasWritePermissions) + + switch f.Probe { + case hasGitHubWorkflowPermissionUndeclared.Probe: + score = updateScoreAndMapFromUndeclared(undeclaredPermissions, + hasWritePermissions, f, score, dl) + case hasNoGitHubWorkflowPermissionWriteAllTop.Probe: + // If no top level permissions are defined, all the permissions + // are enabled by default. + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + hasWritePermissions["topLevel"][fPath] = true + + if hasWritePermissions["jobLevel"][fPath] || + undeclaredPermissions["jobLevel"][fPath] { + return checker.CreateMinScoreResult(name, "detected GitHub workflow tokens with excessive permissions") + } + score -= 0.5 + case hasNoGitHubWorkflowPermissionWriteAllJob.Probe: + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + hasWritePermissions["jobLevel"][fPath] = true + if hasWritePermissions["topLevel"][fPath] { + score = checker.MinResultScore + break + } + if undeclaredPermissions["topLevel"][fPath] { + score -= 0.5 + } + case hasNoGitHubWorkflowPermissionWriteStatusesJob.Probe, + hasNoGitHubWorkflowPermissionWriteDeploymentsJob.Probe, + hasNoGitHubWorkflowPermissionWriteSecurityEventsJob.Probe, + hasNoGitHubWorkflowPermissionWriteActionsJob.Probe, + hasNoGitHubWorkflowPermissionWriteContentsJob.Probe, + hasNoGitHubWorkflowPermissionWritePackagesJob.Probe, + hasNoGitHubWorkflowPermissionWriteChecksJob.Probe: + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + hasWritePermissions["jobLevel"][fPath] = true + default: + score = logAndReduceScore(f, dl, score) + } + } + 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 notAvailableOrNotApplicable(f *finding.Finding, dl checker.DetailLogger) bool { + if f.Probe == hasGitHubWorkflowPermissionUndeclared.Probe { + 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.Values["jobLevel"] == 1 { + dl.Debug(&checker.LogMessage{ + Finding: f, + }) + undeclaredPermissions["jobLevel"][fPath] = true + score = updateScoreFromUndeclaredJob(undeclaredPermissions, + hasWritePermissions, + fPath, + score) + } else if f.Values["topLevel"] == 1 { + 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 + } +} + +// Some probes with negative outcomes triggers logging and simple reduce in score. +// logAndReduceScore represents these cases. +func logAndReduceScore(f *finding.Finding, dl checker.DetailLogger, score float32) float32 { + switch f.Probe { + case hasGitHubWorkflowPermissionUnknown.Probe: + dl.Debug(&checker.LogMessage{ + Finding: f, + }) + case hasNoGitHubWorkflowPermissionWriteChecksTop.Probe, + hasNoGitHubWorkflowPermissionWriteStatusesTop.Probe: + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + score -= 0.5 + case hasNoGitHubWorkflowPermissionWriteContentsTop.Probe, + hasNoGitHubWorkflowPermissionWritePackagesTop.Probe, + hasNoGitHubWorkflowPermissionWriteActionsTop.Probe: + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + score -= checker.MaxResultScore + case hasNoGitHubWorkflowPermissionWriteDeploymentsTop.Probe, + hasNoGitHubWorkflowPermissionWriteSecurityEventsTop.Probe: + dl.Warn(&checker.LogMessage{ + Finding: f, + }) + score-- + } + return score +} diff --git a/checks/evaluation/permissions/gitHubWorkflowPermissionsStepsNoWrite.yml b/checks/evaluation/permissions/gitHubWorkflowPermissionsStepsNoWrite.yml deleted file mode 100644 index 171f8503fb11..000000000000 --- 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 3aebd06c0473..000000000000 --- a/checks/evaluation/permissions/permissions.go +++ /dev/null @@ -1,570 +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 int - -const ( - // permissionLevelNone is a permission set to `none`. - permissionLevelNone permissionLevel = iota - // permissionLevelRead is a permission set to `read`. - permissionLevelRead - // permissionLevelUnknown is for other kinds of alerts, mostly to support debug messages. - // TODO: remove it once we have implemented severity (#1874). - permissionLevelUnknown - // permissionLevelUndeclared is an undeclared permission. - permissionLevelUndeclared - // permissionLevelWrite is a permission set to `write` for a permission we consider potentially dangerous. - permissionLevelWrite -) - -// permissionLocation represents a declaration type. -type permissionLocationType int - -const ( - // permissionLocationNil is in case the permission is nil. - permissionLocationNil permissionLocationType = iota - // permissionLocationNotDeclared is for undeclared permission. - permissionLocationNotDeclared - // permissionLocationTop is top-level workflow permission. - permissionLocationTop - // permissionLocationJob is job-level workflow permission. - permissionLocationJob -) - -// permissionType represents a permission type. -type permissionType int - -const ( - // permissionTypeNone represents none permission type. - permissionTypeNone permissionType = iota - // permissionTypeNone is the "all" github permission type. - permissionTypeAll - // permissionTypeNone is the "statuses" github permission type. - permissionTypeStatuses - // permissionTypeNone is the "checks" github permission type. - permissionTypeChecks - // permissionTypeNone is the "security-events" github permission type. - permissionTypeSecurityEvents - // permissionTypeNone is the "deployments" github permission type. - permissionTypeDeployments - // permissionTypeNone is the "packages" github permission type. - permissionTypePackages - // permissionTypeNone is the "actions" github permission type. - permissionTypeActions -) - -// 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.WithValues(map[string]int{ - "PermissionLevel": int(permissionLevelNone), - }) - case checker.PermissionLevelRead: - f = f.WithOutcome(finding.OutcomePositive) - f = f.WithValues(map[string]int{ - "PermissionLevel": int(permissionLevelRead), - }) - - case checker.PermissionLevelUnknown: - f = f.WithValues(map[string]int{ - "PermissionLevel": int(permissionLevelUnknown), - }).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]int{ - "PermissionLevel": int(permissionLevelUndeclared), - "LocationType": int(locationType), - "PermissionType": int(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]int{ - "PermissionLevel": int(permissionLevelWrite), - "LocationType": int(locationType), - "PermissionType": int(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 int) *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 int, -) { - 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 abb3f902558d..f701b5b2606a 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(CheckMaintained, 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 9261fb2792d4..0a6739e84b93 100644 --- a/checks/permissions_test.go +++ b/checks/permissions_test.go @@ -543,3 +543,4 @@ func TestGithubTokenPermissionsLineNumber(t *testing.T) { }) } } + diff --git a/checks/raw/permissions.go b/checks/raw/permissions.go index c3c7132db440..abc5e855b629 100644 --- a/checks/raw/permissions.go +++ b/checks/raw/permissions.go @@ -25,6 +25,8 @@ import ( "github.com/ossf/scorecard/v4/checks/raw/github" sce "github.com/ossf/scorecard/v4/errors" "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/remediation" + "github.com/ossf/scorecard/v4/rule" ) type permission string @@ -59,6 +61,17 @@ func TokenPermissions(c *checker.CheckRequest) (checker.TokenPermissionsData, er CaseSensitive: false, }, validateGitHubActionTokenPermissions, &data) + for i := range data.results.TokenPermissions { + rr := &data.results.TokenPermissions[i] + //nolint:errcheck + remdtion, _ := remediation.New(c) + ruleRemdtion := &rule.Remediation{ + Branch: remdtion.Branch, + Repo: remdtion.Repo, + } + rr.Remediation = ruleRemdtion + } + return data.results, err } @@ -104,6 +117,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 1f95324a34b8..49411698db76 100644 --- a/probes/entries.go +++ b/probes/entries.go @@ -36,8 +36,28 @@ import ( "github.com/ossf/scorecard/v4/probes/hasDangerousWorkflowScriptInjection" "github.com/ossf/scorecard/v4/probes/hasDangerousWorkflowUntrustedCheckout" "github.com/ossf/scorecard/v4/probes/hasFSFOrOSIApprovedLicense" + "github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionNone" + "github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionRead" + "github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionUndeclared" + "github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionUnknown" "github.com/ossf/scorecard/v4/probes/hasLicenseFile" "github.com/ossf/scorecard/v4/probes/hasLicenseFileAtTopDir" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteActionsJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteActionsTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteAllJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteAllTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteChecksJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteChecksTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteContentsJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteContentsTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWritePackagesJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWritePackagesTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob" + "github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteStatusesTop" "github.com/ossf/scorecard/v4/probes/hasOSVVulnerabilities" "github.com/ossf/scorecard/v4/probes/hasOpenSSFBadge" "github.com/ossf/scorecard/v4/probes/hasRecentCommits" @@ -148,6 +168,28 @@ var ( releasesAreSigned.Run, releasesHaveProvenance.Run, } + TokenPermissions = []ProbeImpl{ + hasNoGitHubWorkflowPermissionWriteActionsTop.Run, + hasNoGitHubWorkflowPermissionWriteAllTop.Run, + hasNoGitHubWorkflowPermissionWriteChecksTop.Run, + hasNoGitHubWorkflowPermissionWriteContentsTop.Run, + hasNoGitHubWorkflowPermissionWriteDeploymentsTop.Run, + hasNoGitHubWorkflowPermissionWritePackagesTop.Run, + hasNoGitHubWorkflowPermissionWriteSecurityEventsTop.Run, + hasNoGitHubWorkflowPermissionWriteStatusesTop.Run, + hasGitHubWorkflowPermissionUnknown.Run, + hasGitHubWorkflowPermissionNone.Run, + hasGitHubWorkflowPermissionRead.Run, + hasGitHubWorkflowPermissionUndeclared.Run, + hasNoGitHubWorkflowPermissionWriteAllJob.Run, + hasNoGitHubWorkflowPermissionWriteSecurityEventsJob.Run, + hasNoGitHubWorkflowPermissionWriteContentsJob.Run, + hasNoGitHubWorkflowPermissionWritePackagesJob.Run, + hasNoGitHubWorkflowPermissionWriteActionsJob.Run, + hasNoGitHubWorkflowPermissionWriteChecksJob.Run, + hasNoGitHubWorkflowPermissionWriteDeploymentsJob.Run, + hasNoGitHubWorkflowPermissionWriteStatusesJob.Run, + } probeRunners = map[string]func(*checker.RawResults) ([]finding.Finding, string, error){ securityPolicyPresent.Probe: securityPolicyPresent.Run, diff --git a/probes/hasGitHubWorkflowPermissionNone/def.yml b/probes/hasGitHubWorkflowPermissionNone/def.yml new file mode 100644 index 000000000000..16d91670e515 --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionNone/def.yml @@ -0,0 +1,36 @@ +# 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: hasGitHubWorkflowPermissionNone +short: Checks that the project has any Workflows with permission level "none". +motivation: > + Setting permission levels to none if a positive setting if the project does not need any permissions. + It prevents read and write actions against the given resource type. +implementation: > + The probe checks for workflows with "none" set as the permission level. +outcome: + - The probe returns 1 positive outcome per workflow with "none" set as its permission level. + - The probe returns 1 OutcomeNotApplicable if no workflows have permission level set to "none" +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasGitHubWorkflowPermissionNone/impl.go b/probes/hasGitHubWorkflowPermissionNone/impl.go new file mode 100644 index 000000000000..b214cf2ded57 --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionNone/impl.go @@ -0,0 +1,104 @@ +// 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 hasGitHubWorkflowPermissionNone + +import ( + "embed" + "fmt" + "strings" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/uerror" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasGitHubWorkflowPermissionNone" + +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.OutcomeNotAvailable) + 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.PermissionLevelNone { + continue + } + + // Create finding + f, err := finding.NewWith(fs, Probe, + "no workflows with 'none' permissions", + nil, finding.OutcomePositive) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + 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) + } + f = f.WithLocation(loc) + f = f.WithRemediationMetadata(map[string]string{ + "repo": r.Remediation.Repo, + "branch": r.Remediation.Branch, + "workflow": strings.TrimPrefix(f.Location.Path, ".github/workflows/"), + }) + } + findings = append(findings, *f) + } + + if len(findings) == 0 { + f, err := finding.NewWith(fs, Probe, + "no workflows with 'none' permissions", + nil, finding.OutcomeNotApplicable) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + } + return findings, Probe, 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 +} diff --git a/probes/hasGitHubWorkflowPermissionNone/impl_test.go b/probes/hasGitHubWorkflowPermissionNone/impl_test.go new file mode 100644 index 000000000000..7c87a08c079f --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionNone/impl_test.go @@ -0,0 +1,96 @@ +// 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 hasGitHubWorkflowPermissionNone + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := []permissions.TestData{ + { + Name: "No Tokens", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 0, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNotAvailable, + }, + }, + { + Name: "Correct permission level", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + Type: checker.PermissionLevelNone, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomePositive, + }, + }, + { + Name: "Incorrect permission level", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + Type: checker.PermissionLevelUnknown, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNotApplicable, + }, + }, + } + 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/hasGitHubWorkflowPermissionRead/def.yml b/probes/hasGitHubWorkflowPermissionRead/def.yml new file mode 100644 index 000000000000..e44d34c8d26e --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionRead/def.yml @@ -0,0 +1,36 @@ +# 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: hasGitHubWorkflowPermissionRead +short: Checks that the project has any Workflows with permission level "read". +motivation: > + Setting permission levels to none is positive if the project does not need write permisions for the given workflow. + It prevents write actions against the given resource type. +implementation: > + The probe checks for workflows with "read" set as the permission level. +outcome: + - The probe returns 1 positive outcome per workflow with "read" set as its permission level. + - The probe returns 1 OutcomeNotApplicable if no workflows have permission level set to "read" +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasGitHubWorkflowPermissionRead/impl.go b/probes/hasGitHubWorkflowPermissionRead/impl.go new file mode 100644 index 000000000000..e0bf5f4fbf14 --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionRead/impl.go @@ -0,0 +1,104 @@ +// 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 hasGitHubWorkflowPermissionRead + +import ( + "embed" + "fmt" + "strings" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/uerror" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasGitHubWorkflowPermissionRead" + +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.OutcomeNotAvailable) + 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.PermissionLevelRead { + continue + } + + // Create finding + f, err := finding.NewWith(fs, Probe, + "no workflows with 'read' permissions", + nil, finding.OutcomePositive) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + 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) + } + f = f.WithLocation(loc) + f = f.WithRemediationMetadata(map[string]string{ + "repo": r.Remediation.Repo, + "branch": r.Remediation.Branch, + "workflow": strings.TrimPrefix(f.Location.Path, ".github/workflows/"), + }) + } + findings = append(findings, *f) + } + + if len(findings) == 0 { + f, err := finding.NewWith(fs, Probe, + "no workflows with 'read' permissions", + nil, finding.OutcomeNotApplicable) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + } + return findings, Probe, 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 +} diff --git a/probes/hasGitHubWorkflowPermissionRead/impl_test.go b/probes/hasGitHubWorkflowPermissionRead/impl_test.go new file mode 100644 index 000000000000..55ef0bdd2715 --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionRead/impl_test.go @@ -0,0 +1,95 @@ +// 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 hasGitHubWorkflowPermissionRead + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + tests := []permissions.TestData{ + { + Name: "No Tokens", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 0, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNotAvailable, + }, + }, + { + Name: "Correct permission level", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + Type: checker.PermissionLevelRead, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomePositive, + }, + }, + { + Name: "Incorrect permission level", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + Type: checker.PermissionLevelUnknown, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNotApplicable, + }, + }, + } + 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/checks/evaluation/permissions/gitHubWorkflowPermissionsTopNoWrite.yml b/probes/hasGitHubWorkflowPermissionUndeclared/def.yml similarity index 79% rename from checks/evaluation/permissions/gitHubWorkflowPermissionsTopNoWrite.yml rename to probes/hasGitHubWorkflowPermissionUndeclared/def.yml index 91b2f117c932..5dd90744fc6a 100644 --- a/checks/evaluation/permissions/gitHubWorkflowPermissionsTopNoWrite.yml +++ b/probes/hasGitHubWorkflowPermissionUndeclared/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,8 +12,8 @@ # 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: hasGitHubWorkflowPermissionUndeclared +short: Checks that GitHub workflows have workflows without permissions declared 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. @@ -21,6 +21,10 @@ motivation: > 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. +outcome: + - The probe returns 1 negative outcome per workflow without a declared permission level. + - The probe returns 1 OutcomeNotAvailable if if the location of the permission in the workflow is not available. + - The probe returns 1 OutcomeNotApplicable if the project has no workflows without declared permissions. remediation: effort: Low text: diff --git a/probes/hasGitHubWorkflowPermissionUndeclared/impl.go b/probes/hasGitHubWorkflowPermissionUndeclared/impl.go new file mode 100644 index 000000000000..8cb8c2ab38c7 --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionUndeclared/impl.go @@ -0,0 +1,106 @@ +// 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 hasGitHubWorkflowPermissionUndeclared + +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 = "hasGitHubWorkflowPermissionUndeclared" + +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.OutcomeNotAvailable) + 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.PermissionLevelUndeclared { + continue + } + topLevel := 0 + jobLevel := 0 + if *r.LocationType == checker.PermissionLocationTop { + topLevel = 1 + } + if *r.LocationType == checker.PermissionLocationJob { + jobLevel = 1 + } + switch { + case r.LocationType == nil: + f, err := finding.NewWith(fs, Probe, + "could not determine the location type", + nil, finding.OutcomeNotAvailable) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + case *r.LocationType == checker.PermissionLocationTop, + *r.LocationType == checker.PermissionLocationJob: + // Create finding + f, err := permissions.CreateNegativeFinding(r, Probe, fs) + if err != nil { + return nil, Probe, fmt.Errorf("create finding: %w", err) + } + f = f.WithValues(map[string]int{ + "topLevel": topLevel, + "jobLevel": jobLevel, + }) + findings = append(findings, *f) + default: + f, err := finding.NewWith(fs, Probe, + "could not determine the location type", + nil, finding.OutcomeNotApplicable) + 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, + "project has no workflows with undeclared 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/hasGitHubWorkflowPermissionUndeclared/impl_test.go b/probes/hasGitHubWorkflowPermissionUndeclared/impl_test.go new file mode 100644 index 000000000000..fb1608441e1a --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionUndeclared/impl_test.go @@ -0,0 +1,97 @@ +// 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 hasGitHubWorkflowPermissionUndeclared + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + permLoc := checker.PermissionLocationTop + tests := []permissions.TestData{ + { + Name: "No Tokens", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 0, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNotAvailable, + }, + }, + { + Name: "Correct permission level", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + Type: checker.PermissionLevelUndeclared, + LocationType: &permLoc, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNegative, + }, + }, + { + Name: "Incorrect permission level", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + Type: checker.PermissionLevelUnknown, + }, + }, + }, + }, + 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/hasGitHubWorkflowPermissionUnknown/def.yml b/probes/hasGitHubWorkflowPermissionUnknown/def.yml new file mode 100644 index 000000000000..90d307e62b69 --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionUnknown/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: hasGitHubWorkflowPermissionUnknown +short: Checks that GitHub workflows have workflows with unknown permissions +motivation: > + Unknown permissions may be a result of a bug or another error from fetching the permission levels. +implementation: > + 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 + - 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). + - 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/hasGitHubWorkflowPermissionUnknown/impl.go b/probes/hasGitHubWorkflowPermissionUnknown/impl.go new file mode 100644 index 000000000000..b4110f7f05bb --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionUnknown/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 hasGitHubWorkflowPermissionUnknown + +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 = "hasGitHubWorkflowPermissionUnknown" + +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.OutcomeNotAvailable) + 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) + 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/hasGitHubWorkflowPermissionUnknown/impl_test.go b/probes/hasGitHubWorkflowPermissionUnknown/impl_test.go new file mode 100644 index 000000000000..0c0a98bcfd00 --- /dev/null +++ b/probes/hasGitHubWorkflowPermissionUnknown/impl_test.go @@ -0,0 +1,99 @@ +// Copyright 2024 OpenSSF Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//nolint:stylecheck +package hasGitHubWorkflowPermissionUnknown + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + permLoc := checker.PermissionLocationTop + value := "value" + tests := []permissions.TestData{ + { + Name: "No Tokens", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 0, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomeNotAvailable, + }, + }, + { + 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/hasNoGitHubWorkflowPermissionWriteActionsJob/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteActionsJob/def.yml new file mode 100644 index 000000000000..9afbb91d6f74 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteActionsJob/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: hasNoGitHubWorkflowPermissionWriteActionsJob +short: Checks that GitHub workflows do not have "write" permissions at the "job" level for "actions" +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 for "actions". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "job" level for "actions". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteActionsJob/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteActionsJob/impl.go new file mode 100644 index 000000000000..d4958b70ef94 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteActionsJob/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteActionsJob + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteActionsJob" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationJob, + checker.PermissionLevelWrite, Probe, + "actions") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteActionsJob/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteActionsJob/impl_test.go new file mode 100644 index 000000000000..453b19764ce5 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteActionsJob/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteActionsJob + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "actions") + + 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/hasNoGitHubWorkflowPermissionWriteActionsTop/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteActionsTop/def.yml new file mode 100644 index 000000000000..b8b5c4adf41f --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteActionsTop/def.yml @@ -0,0 +1,36 @@ +# 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: hasNoGitHubWorkflowPermissionWriteActionsTop + +short: Checks that GitHub workflows do not have "write" permissions at the "top" level for "actions" +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 for "actions". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "top" level for "actions". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteActionsTop/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteActionsTop/impl.go new file mode 100644 index 000000000000..132a4e78cc0a --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteActionsTop/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteActionsTop + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteActionsTop" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationTop, + checker.PermissionLevelWrite, Probe, + "actions") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteActionsTop/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteActionsTop/impl_test.go new file mode 100644 index 000000000000..aea21e2f95cc --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteActionsTop/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteActionsTop + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "actions") + + 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/hasNoGitHubWorkflowPermissionWriteAllJob/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteAllJob/def.yml new file mode 100644 index 000000000000..9851910de3fd --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteAllJob/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: hasNoGitHubWorkflowPermissionWriteAllJob +short: Checks that GitHub workflows do not have "write" permissions at the "job" level for "all" +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 for "all". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "job" level for "all". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteAllJob/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteAllJob/impl.go new file mode 100644 index 000000000000..7a9f0ddd6e7d --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteAllJob/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteAllJob + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteAllJob" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationJob, + checker.PermissionLevelWrite, Probe, + "all") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteAllJob/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteAllJob/impl_test.go new file mode 100644 index 000000000000..708c092f7fd2 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteAllJob/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteAllJob + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "all") + + 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/hasNoGitHubWorkflowPermissionWriteAllTop/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteAllTop/def.yml new file mode 100644 index 000000000000..2fddebb761fe --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteAllTop/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: hasNoGitHubWorkflowPermissionWriteAllTop +short: Checks that GitHub workflows do not have "write" permissions at the "top" level for "all" +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 for "all". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "top" level for "all". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteAllTop/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteAllTop/impl.go new file mode 100644 index 000000000000..11b0eccca2d6 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteAllTop/impl.go @@ -0,0 +1,88 @@ +// 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 hasNoGitHubWorkflowPermissionWriteAllTop + +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 = "hasNoGitHubWorkflowPermissionWriteAllTop" + +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.OutcomeNotAvailable) + 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.Name == nil { + if r.Value == nil { + continue + } + } + if r.Name != nil && *r.Name != "all" { + if r.Value != nil && *r.Value != "write-all" { + continue + } + } + if r.Type != checker.PermissionLevelWrite { + continue + } + if *r.LocationType != checker.PermissionLocationTop { + continue + } + + // Create finding + f, err := permissions.CreateNegativeFinding(r, Probe, fs) + 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 write permissions for 'all' at top level", + 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/hasNoGitHubWorkflowPermissionWriteAllTop/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteAllTop/impl_test.go new file mode 100644 index 000000000000..dee780b24091 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteAllTop/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteAllTop + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "all") + + 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/hasNoGitHubWorkflowPermissionWriteChecksJob/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteChecksJob/def.yml new file mode 100644 index 000000000000..38a91ab9f280 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteChecksJob/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: hasNoGitHubWorkflowPermissionWriteChecksJob +short: Checks that GitHub workflows do not have "write" permissions at the "job" level for "checks" +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 for "checks". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "job" level for "checks". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteChecksJob/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteChecksJob/impl.go new file mode 100644 index 000000000000..280fed15c6bb --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteChecksJob/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteChecksJob + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteChecksJob" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationJob, + checker.PermissionLevelWrite, Probe, + "checks") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteChecksJob/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteChecksJob/impl_test.go new file mode 100644 index 000000000000..7fe19b52e494 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteChecksJob/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteChecksJob + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "checks") + + 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/hasNoGitHubWorkflowPermissionWriteChecksTop/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteChecksTop/def.yml new file mode 100644 index 000000000000..2910c7ff51c1 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteChecksTop/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: hasNoGitHubWorkflowPermissionWriteChecksTop +short: Checks that GitHub workflows do not have "write" permissions at the "top" level for "checks" +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 for "checks". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "top" level for "checks". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteChecksTop/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteChecksTop/impl.go new file mode 100644 index 000000000000..d7b0cc4a2ba8 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteChecksTop/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteChecksTop + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteChecksTop" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationTop, + checker.PermissionLevelWrite, Probe, + "checks") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteChecksTop/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteChecksTop/impl_test.go new file mode 100644 index 000000000000..517361a49d17 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteChecksTop/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteChecksTop + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "checks") + + 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/hasNoGitHubWorkflowPermissionWriteContentsJob/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteContentsJob/def.yml new file mode 100644 index 000000000000..bec7a580be71 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteContentsJob/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: hasNoGitHubWorkflowPermissionWriteContentsJob +short: Checks that GitHub workflows do not have "write" permissions at the "job" level for "contents" +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 for "contents". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "job" level for "contents". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteContentsJob/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteContentsJob/impl.go new file mode 100644 index 000000000000..61c5b205cc66 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteContentsJob/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteContentsJob + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteContentsJob" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationJob, + checker.PermissionLevelWrite, Probe, + "contents") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteContentsJob/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteContentsJob/impl_test.go new file mode 100644 index 000000000000..3efbd92003f5 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteContentsJob/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteContentsJob + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "contents") + + 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/hasNoGitHubWorkflowPermissionWriteContentsTop/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteContentsTop/def.yml new file mode 100644 index 000000000000..fd53224245dd --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteContentsTop/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: hasNoGitHubWorkflowPermissionWriteContentsTop +short: Checks that GitHub workflows do not have "write" permissions at the "top" level for "contents" +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 for "contents". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "top" level for "contents". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteContentsTop/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteContentsTop/impl.go new file mode 100644 index 000000000000..358e65042b3f --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteContentsTop/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteContentsTop + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteContentsTop" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationTop, + checker.PermissionLevelWrite, Probe, + "contents") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteContentsTop/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteContentsTop/impl_test.go new file mode 100644 index 000000000000..c3e1ab286d3a --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteContentsTop/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteContentsTop + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "contents") + + 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/hasNoGitHubWorkflowPermissionWriteDeploymentsJob/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsJob/def.yml new file mode 100644 index 000000000000..f717cdf80188 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsJob/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: hasNoGitHubWorkflowPermissionWriteDeploymentsJob +short: Checks that GitHub workflows do not have "write" permissions at the "job" level for "deployments" +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 for "deployments". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "job" level for "deployments". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteDeploymentsJob/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsJob/impl.go new file mode 100644 index 000000000000..c3360cf2a154 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsJob/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteDeploymentsJob + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteDeploymentsJob" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationJob, + checker.PermissionLevelWrite, Probe, + "deployments") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsJob/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsJob/impl_test.go new file mode 100644 index 000000000000..566105ef952a --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsJob/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteDeploymentsJob + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "deployments") + + 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/hasNoGitHubWorkflowPermissionWriteDeploymentsTop/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsTop/def.yml new file mode 100644 index 000000000000..3cf8a706262b --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsTop/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: hasNoGitHubWorkflowPermissionWriteDeploymentsTop +short: Checks that GitHub workflows do not have "write" permissions at the "top" level for "deployments" +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 for "deployments". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "top" level for "deployments". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteDeploymentsTop/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsTop/impl.go new file mode 100644 index 000000000000..99a9cc98f4bc --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsTop/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteDeploymentsTop + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteDeploymentsTop" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationTop, + checker.PermissionLevelWrite, Probe, + "deployments") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsTop/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsTop/impl_test.go new file mode 100644 index 000000000000..2c31ace8b5cd --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsTop/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteDeploymentsTop + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "deployments") + + 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/hasNoGitHubWorkflowPermissionWritePackagesJob/def.yml b/probes/hasNoGitHubWorkflowPermissionWritePackagesJob/def.yml new file mode 100644 index 000000000000..aa6f73f1c955 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWritePackagesJob/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: hasNoGitHubWorkflowPermissionWritePackagesJob +short: Checks that GitHub workflows do not have "write" permissions at the "job" level for "packages" +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 for "packages". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "job" level for "packages". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWritePackagesJob/impl.go b/probes/hasNoGitHubWorkflowPermissionWritePackagesJob/impl.go new file mode 100644 index 000000000000..f64412bd1fbc --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWritePackagesJob/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWritePackagesJob + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWritePackagesJob" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationJob, + checker.PermissionLevelWrite, Probe, + "packages") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWritePackagesJob/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWritePackagesJob/impl_test.go new file mode 100644 index 000000000000..eb7267ea0df7 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWritePackagesJob/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWritePackagesJob + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "packages") + + 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/hasNoGitHubWorkflowPermissionWritePackagesTop/def.yml b/probes/hasNoGitHubWorkflowPermissionWritePackagesTop/def.yml new file mode 100644 index 000000000000..c761c5ae12f2 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWritePackagesTop/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: hasNoGitHubWorkflowPermissionWritePackagesTop +short: Checks that GitHub workflows do not have "write" permissions at the "top" level for "packages" +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 for "packages". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "top" level for "packages". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWritePackagesTop/impl.go b/probes/hasNoGitHubWorkflowPermissionWritePackagesTop/impl.go new file mode 100644 index 000000000000..beca582cffb5 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWritePackagesTop/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWritePackagesTop + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWritePackagesTop" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationTop, + checker.PermissionLevelWrite, Probe, + "packages") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWritePackagesTop/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWritePackagesTop/impl_test.go new file mode 100644 index 000000000000..49b9829868ce --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWritePackagesTop/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWritePackagesTop + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "packages") + + 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/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob/def.yml new file mode 100644 index 000000000000..55d59ef175d1 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob/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: hasNoGitHubWorkflowPermissionWriteSecurityEventsJob +short: Checks that GitHub workflows do not have "write" permissions at the "job" level for "security-events" +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 for "security-events". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "job" level for "security-events". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob/impl.go new file mode 100644 index 000000000000..1c26861a0ed5 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteSecurityEventsJob + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteSecurityEventsJob" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationJob, + checker.PermissionLevelWrite, Probe, + "security-events") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob/impl_test.go new file mode 100644 index 000000000000..bf0d0e6e9e4e --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteSecurityEventsJob + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.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/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop/def.yml new file mode 100644 index 000000000000..2a78851ea9a4 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop/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: hasNoGitHubWorkflowPermissionWriteSecurityEventsTop +short: Checks that GitHub workflows do not have "write" permissions at the "top" level for "security-events" +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 for "security-events". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "top" level for "security-events". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop/impl.go new file mode 100644 index 000000000000..e4685baa1a0b --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteSecurityEventsTop + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteSecurityEventsTop" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationTop, + checker.PermissionLevelWrite, Probe, + "security-events") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop/impl_test.go new file mode 100644 index 000000000000..e85d5aafd5fa --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteSecurityEventsTop + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.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) + }) + } +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob/def.yml new file mode 100644 index 000000000000..9847adfea85a --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob/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: hasNoGitHubWorkflowPermissionWriteStatusesJob +short: Checks that GitHub workflows do not have "write" permissions at the "job" level for "statuses" +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 for "statuses". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "job" level for "statuses". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteStatusesJob/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob/impl.go new file mode 100644 index 000000000000..de8e3b666850 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteStatusesJob + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteStatusesJob" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationJob, + checker.PermissionLevelWrite, Probe, + "statuses") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob/impl_test.go new file mode 100644 index 000000000000..9736d0f344bf --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteStatusesJob + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "statuses") + + 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/hasNoGitHubWorkflowPermissionWriteStatusesTop/def.yml b/probes/hasNoGitHubWorkflowPermissionWriteStatusesTop/def.yml new file mode 100644 index 000000000000..3cc420881bcc --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteStatusesTop/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: hasNoGitHubWorkflowPermissionWriteStatusesTop +short: Checks that GitHub workflows do not have "write" permissions at the "top" level for "statuses" +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 for "statuses". + - The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "top" level for "statuses". +remediation: + effort: Low + text: + - Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?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). + - 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/hasNoGitHubWorkflowPermissionWriteStatusesTop/impl.go b/probes/hasNoGitHubWorkflowPermissionWriteStatusesTop/impl.go new file mode 100644 index 000000000000..9b09d1e75944 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteStatusesTop/impl.go @@ -0,0 +1,36 @@ +// 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 hasNoGitHubWorkflowPermissionWriteStatusesTop + +import ( + "embed" + + "github.com/ossf/scorecard/v4/checker" + "github.com/ossf/scorecard/v4/finding" + "github.com/ossf/scorecard/v4/probes/internal/utils/permissions" +) + +//go:embed *.yml +var fs embed.FS + +const Probe = "hasNoGitHubWorkflowPermissionWriteStatusesTop" + +func Run(raw *checker.RawResults) ([]finding.Finding, string, error) { + //nolint:wrapcheck + return permissions.CreateFindings(fs, raw, checker.PermissionLocationTop, + checker.PermissionLevelWrite, Probe, + "statuses") +} diff --git a/probes/hasNoGitHubWorkflowPermissionWriteStatusesTop/impl_test.go b/probes/hasNoGitHubWorkflowPermissionWriteStatusesTop/impl_test.go new file mode 100644 index 000000000000..db5836fe8e98 --- /dev/null +++ b/probes/hasNoGitHubWorkflowPermissionWriteStatusesTop/impl_test.go @@ -0,0 +1,52 @@ +// 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 hasNoGitHubWorkflowPermissionWriteStatusesTop + +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/permissions" + "github.com/ossf/scorecard/v4/probes/internal/utils/test" +) + +func Test_Run(t *testing.T) { + t.Parallel() + + tests := permissions.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "statuses") + + 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 000000000000..cfef583679b1 --- /dev/null +++ b/probes/internal/utils/permissions/permissions.go @@ -0,0 +1,345 @@ +// 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" + "github.com/ossf/scorecard/v4/probes/internal/utils/uerror" +) + +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, +) (*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) + } + + // Create Location + 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) + } + f = f.WithLocation(loc) + f = f.WithRemediationMetadata(map[string]string{ + "repo": r.Remediation.Repo, + "branch": r.Remediation.Branch, + "workflow": strings.TrimPrefix(f.Location.Path, ".github/workflows/"), + }) + } + return f, nil +} + +func CreateFindings(fs embed.FS, + raw *checker.RawResults, + locationType checker.PermissionLocation, + permissionLevel checker.PermissionLevel, + probe, tokenName string, +) ([]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.OutcomeNotAvailable) + 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.Name == nil { + continue + } + if *r.Name != tokenName { + continue + } + if r.Type != permissionLevel { + continue + } + if *r.LocationType != locationType { + continue + } + + // Create finding + f, err := CreateNegativeFinding(r, probe, fs) + 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, + fmt.Sprintf("no workflows with write permissions for '%v' at '%v'", + tokenName, + permissionLevel), + nil, finding.OutcomePositive) + if err != nil { + return nil, probe, fmt.Errorf("create finding: %w", err) + } + findings = append(findings, *f) + } + return findings, probe, 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 +} + +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. + wrongName := "wrongName" + 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.OutcomeNotAvailable, + }, + }, + { + Name: "Invalid name", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + LocationType: &locationType, + Name: &wrongName, + Value: &value, + Msg: nil, + Type: permissionType, + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomePositive, + }, + }, + { + 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: "Value is nil - Type is wrong", + Raw: &checker.RawResults{ + TokenPermissionsResults: checker.TokenPermissionsData{ + NumTokens: 1, + TokenPermissions: []checker.TokenPermission{ + { + LocationType: &locationType, + Name: &name, + Value: nil, + Msg: nil, + Type: checker.PermissionLevel("999"), + }, + }, + }, + }, + Outcomes: []finding.Outcome{ + finding.OutcomePositive, + }, + }, + { + 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/rule/rule.go b/rule/rule.go index e5aa7f6d43a6..a50d9080dc6e 100644 --- a/rule/rule.go +++ b/rule/rule.go @@ -46,6 +46,8 @@ type Remediation struct { // Text in markdown format for humans. Markdown string `json:"markdown"` // Effort to remediate. + Branch string `json:"branch"` + Repo string `json:"repo"` Effort RemediationEffort `json:"effort"` }