mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-17 11:57:12 +03:00
🌱 migrate token permission check to probes (#3816)
* 🌱 migrate token permission check to probes
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* combine seperate write-probes into two that combine them all
Signed-off-by: AdamKorcz <adam@adalogics.com>
* change write probes to read and write
Signed-off-by: AdamKorcz <adam@adalogics.com>
* minor nit
Signed-off-by: AdamKorcz <adam@adalogics.com>
* remove WritaAll probes
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* Merge read-perm probe with job/top probes
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* minor refactoring
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix copy paste error
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix linter issues and restructure code
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* remove hasGitHubWorkflowPermissionNone probe
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* Remove 'hasGitHubWorkflowPermissionUndeclared' probe
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* bit of clean up
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* reduce code complexity and remove comment
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* simplify file location
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* change probe text
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* invert name of probe
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* OutcomeNotApplicable -> OutcomeError
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* OutcomeNotAvailable -> OutcomeNotApplicable
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* more OutcomeNotAvailable -> OutcomeNotApplicable
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* change name of 'notAvailableOrNotApplicable'
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix linter issues
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* add comments to remediation fields
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* add check for nil-dereference
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* remove the permissionLocation finding value
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* rename checkAndLogNotAvailableOrNotApplicable to isBothUndeclaredAndNotAvailableOrNotApplicable
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* use raw metadata for remediation output
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* change 'branch' to 'defaultBranch'
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* remove unused fields in rule Remediation
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* fix remediation
Signed-off-by: Adam Korczynski <adam@adalogics.com>
* change 'metadata.defaultBranch' to 'metadata.repository.defaultBranch'
Signed-off-by: Adam Korczynski <adam@adalogics.com>
---------
Signed-off-by: Adam Korczynski <adam@adalogics.com>
Signed-off-by: AdamKorcz <adam@adalogics.com>
This commit is contained in:
parent
c1066d9ac2
commit
5b0ae81d49
303
checks/evaluation/permissions.go
Normal file
303
checks/evaluation/permissions.go
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
// Copyright 2021 OpenSSF Scorecard Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package evaluation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
sce "github.com/ossf/scorecard/v4/errors"
|
||||||
|
"github.com/ossf/scorecard/v4/finding"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionUnknown"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/jobLevelPermissions"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/topLevelPermissions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isWriteAll(f *finding.Finding) bool {
|
||||||
|
return (f.Values["tokenName"] == "all" || f.Values["tokenName"] == "write-all")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenPermissions applies the score policy for the Token-Permissions check.
|
||||||
|
//
|
||||||
|
//nolint:gocognit
|
||||||
|
func TokenPermissions(name string,
|
||||||
|
findings []finding.Finding,
|
||||||
|
dl checker.DetailLogger,
|
||||||
|
) checker.CheckResult {
|
||||||
|
expectedProbes := []string{
|
||||||
|
hasNoGitHubWorkflowPermissionUnknown.Probe,
|
||||||
|
jobLevelPermissions.Probe,
|
||||||
|
topLevelPermissions.Probe,
|
||||||
|
}
|
||||||
|
if !finding.UniqueProbesEqual(findings, expectedProbes) {
|
||||||
|
e := sce.WithMessage(sce.ErrScorecardInternal, "invalid probe results")
|
||||||
|
return checker.CreateRuntimeErrorResult(name, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with a perfect score.
|
||||||
|
score := float32(checker.MaxResultScore)
|
||||||
|
|
||||||
|
// hasWritePermissions is a map that holds information about the
|
||||||
|
// workflows in the project that have write permissions. It holds
|
||||||
|
// information about the write permissions of jobs and at the
|
||||||
|
// top-level too. The inner map (map[string]bool) has the
|
||||||
|
// workflow path as its key, and the value determines whether
|
||||||
|
// that workflow has write permissions at either "job" or "top"
|
||||||
|
// level.
|
||||||
|
hasWritePermissions := make(map[string]map[string]bool)
|
||||||
|
hasWritePermissions["jobLevel"] = make(map[string]bool)
|
||||||
|
hasWritePermissions["topLevel"] = make(map[string]bool)
|
||||||
|
|
||||||
|
// undeclaredPermissions is a map that holds information about the
|
||||||
|
// workflows in the project that have undeclared permissions. It holds
|
||||||
|
// information about the undeclared permissions of jobs and at the
|
||||||
|
// top-level too. The inner map (map[string]bool) has the
|
||||||
|
// workflow path as its key, and the value determines whether
|
||||||
|
// that workflow has undeclared permissions at either "job" or "top"
|
||||||
|
// level.
|
||||||
|
undeclaredPermissions := make(map[string]map[string]bool)
|
||||||
|
undeclaredPermissions["jobLevel"] = make(map[string]bool)
|
||||||
|
undeclaredPermissions["topLevel"] = make(map[string]bool)
|
||||||
|
|
||||||
|
for i := range findings {
|
||||||
|
f := &findings[i]
|
||||||
|
|
||||||
|
// Log workflows with "none" permissions
|
||||||
|
if f.Values["permissionLevel"] == string(checker.PermissionLevelNone) {
|
||||||
|
dl.Info(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log workflows with "read" permissions
|
||||||
|
if f.Values["permissionLevel"] == string(checker.PermissionLevelRead) {
|
||||||
|
dl.Info(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if isBothUndeclaredAndNotAvailableOrNotApplicable(f, dl) {
|
||||||
|
return checker.CreateInconclusiveResult(name, "Token permissions are not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no TokenPermissions
|
||||||
|
if f.Outcome == finding.OutcomeNotApplicable {
|
||||||
|
return checker.CreateInconclusiveResult(name, "No tokens found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Outcome != finding.OutcomeNegative {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f.Location == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fPath := f.Location.Path
|
||||||
|
|
||||||
|
addProbeToMaps(fPath, undeclaredPermissions, hasWritePermissions)
|
||||||
|
|
||||||
|
if f.Values["permissionLevel"] == string(checker.PermissionLevelUndeclared) {
|
||||||
|
score = updateScoreAndMapFromUndeclared(undeclaredPermissions,
|
||||||
|
hasWritePermissions, f, score, dl)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch f.Probe {
|
||||||
|
case hasNoGitHubWorkflowPermissionUnknown.Probe:
|
||||||
|
dl.Debug(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
case topLevelPermissions.Probe:
|
||||||
|
if f.Values["permissionLevel"] != string(checker.PermissionLevelWrite) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hasWritePermissions["topLevel"][fPath] = true
|
||||||
|
|
||||||
|
if !isWriteAll(f) {
|
||||||
|
score -= reduceBy(f, dl)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.Warn(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
// "all" is evaluated separately. If the project also has write permissions
|
||||||
|
// or undeclared permissions at the job level, this is particularly bad.
|
||||||
|
if hasWritePermissions["jobLevel"][fPath] ||
|
||||||
|
undeclaredPermissions["jobLevel"][fPath] {
|
||||||
|
return checker.CreateMinScoreResult(name, "detected GitHub workflow tokens with excessive permissions")
|
||||||
|
}
|
||||||
|
score -= 0.5
|
||||||
|
case jobLevelPermissions.Probe:
|
||||||
|
if f.Values["permissionLevel"] != string(checker.PermissionLevelWrite) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.Warn(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
hasWritePermissions["jobLevel"][fPath] = true
|
||||||
|
|
||||||
|
// If project has "all" writepermissions too at top level, this is
|
||||||
|
// particularly bad.
|
||||||
|
if hasWritePermissions["topLevel"][fPath] {
|
||||||
|
score = checker.MinResultScore
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// If project has not declared permissions at top level::
|
||||||
|
if undeclaredPermissions["topLevel"][fPath] {
|
||||||
|
score -= 0.5
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if score < checker.MinResultScore {
|
||||||
|
score = checker.MinResultScore
|
||||||
|
}
|
||||||
|
|
||||||
|
logIfNoWritePermissionsFound(hasWritePermissions, dl)
|
||||||
|
|
||||||
|
if score != checker.MaxResultScore {
|
||||||
|
return checker.CreateResultWithScore(name,
|
||||||
|
"detected GitHub workflow tokens with excessive permissions", int(score))
|
||||||
|
}
|
||||||
|
|
||||||
|
return checker.CreateMaxScoreResult(name,
|
||||||
|
"GitHub workflow tokens follow principle of least privilege")
|
||||||
|
}
|
||||||
|
|
||||||
|
func logIfNoWritePermissionsFound(hasWritePermissions map[string]map[string]bool,
|
||||||
|
dl checker.DetailLogger,
|
||||||
|
) {
|
||||||
|
foundWritePermissions := false
|
||||||
|
for _, isWritePermission := range hasWritePermissions["jobLevel"] {
|
||||||
|
if isWritePermission {
|
||||||
|
foundWritePermissions = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundWritePermissions {
|
||||||
|
text := fmt.Sprintf("no %s write permissions found", checker.PermissionLocationJob)
|
||||||
|
dl.Info(&checker.LogMessage{
|
||||||
|
Text: text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateScoreFromUndeclaredJob(undeclaredPermissions map[string]map[string]bool,
|
||||||
|
hasWritePermissions map[string]map[string]bool,
|
||||||
|
fPath string,
|
||||||
|
score float32,
|
||||||
|
) float32 {
|
||||||
|
if hasWritePermissions["topLevel"][fPath] ||
|
||||||
|
undeclaredPermissions["topLevel"][fPath] {
|
||||||
|
score = checker.MinResultScore
|
||||||
|
}
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateScoreFromUndeclaredTop(undeclaredPermissions map[string]map[string]bool,
|
||||||
|
fPath string,
|
||||||
|
score float32,
|
||||||
|
) float32 {
|
||||||
|
if undeclaredPermissions["jobLevel"][fPath] {
|
||||||
|
score = checker.MinResultScore
|
||||||
|
} else {
|
||||||
|
score -= 0.5
|
||||||
|
}
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBothUndeclaredAndNotAvailableOrNotApplicable(f *finding.Finding, dl checker.DetailLogger) bool {
|
||||||
|
if f.Values["permissionLevel"] == string(checker.PermissionLevelUndeclared) {
|
||||||
|
if f.Outcome == finding.OutcomeNotAvailable {
|
||||||
|
return true
|
||||||
|
} else if f.Outcome == finding.OutcomeNotApplicable {
|
||||||
|
dl.Debug(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateScoreAndMapFromUndeclared(undeclaredPermissions map[string]map[string]bool,
|
||||||
|
hasWritePermissions map[string]map[string]bool,
|
||||||
|
f *finding.Finding,
|
||||||
|
score float32, dl checker.DetailLogger,
|
||||||
|
) float32 {
|
||||||
|
fPath := f.Location.Path
|
||||||
|
if f.Probe == jobLevelPermissions.Probe {
|
||||||
|
dl.Debug(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
undeclaredPermissions["jobLevel"][fPath] = true
|
||||||
|
score = updateScoreFromUndeclaredJob(undeclaredPermissions,
|
||||||
|
hasWritePermissions,
|
||||||
|
fPath,
|
||||||
|
score)
|
||||||
|
} else if f.Probe == topLevelPermissions.Probe {
|
||||||
|
dl.Warn(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
undeclaredPermissions["topLevel"][fPath] = true
|
||||||
|
score = updateScoreFromUndeclaredTop(undeclaredPermissions,
|
||||||
|
fPath,
|
||||||
|
score)
|
||||||
|
}
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
func addProbeToMaps(fPath string, hasWritePermissions, undeclaredPermissions map[string]map[string]bool) {
|
||||||
|
if _, ok := undeclaredPermissions["jobLevel"][fPath]; !ok {
|
||||||
|
undeclaredPermissions["jobLevel"][fPath] = false
|
||||||
|
}
|
||||||
|
if _, ok := undeclaredPermissions["topLevel"][fPath]; !ok {
|
||||||
|
undeclaredPermissions["topLevel"][fPath] = false
|
||||||
|
}
|
||||||
|
if _, ok := hasWritePermissions["jobLevel"][fPath]; !ok {
|
||||||
|
hasWritePermissions["jobLevel"][fPath] = false
|
||||||
|
}
|
||||||
|
if _, ok := hasWritePermissions["topLevel"][fPath]; !ok {
|
||||||
|
hasWritePermissions["topLevel"][fPath] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reduceBy(f *finding.Finding, dl checker.DetailLogger) float32 {
|
||||||
|
if f.Values["permissionLevel"] != string(checker.PermissionLevelWrite) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
tokenName := f.Values["tokenName"]
|
||||||
|
switch tokenName {
|
||||||
|
case "checks", "statuses":
|
||||||
|
dl.Warn(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
return 0.5
|
||||||
|
case "contents", "packages", "actions":
|
||||||
|
dl.Warn(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
return checker.MaxResultScore
|
||||||
|
case "deployments", "security-events":
|
||||||
|
dl.Warn(&checker.LogMessage{
|
||||||
|
Finding: f,
|
||||||
|
})
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
@ -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.
|
|
@ -1,564 +0,0 @@
|
|||||||
// Copyright 2021 OpenSSF Scorecard Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package evaluation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ossf/scorecard/v4/checker"
|
|
||||||
sce "github.com/ossf/scorecard/v4/errors"
|
|
||||||
"github.com/ossf/scorecard/v4/finding"
|
|
||||||
"github.com/ossf/scorecard/v4/remediation"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed *.yml
|
|
||||||
var probes embed.FS
|
|
||||||
|
|
||||||
type permissions struct {
|
|
||||||
topLevelWritePermissions map[string]bool
|
|
||||||
jobLevelWritePermissions map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
stepsNoWriteID = "gitHubWorkflowPermissionsStepsNoWrite"
|
|
||||||
topNoWriteID = "gitHubWorkflowPermissionsTopNoWrite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type permissionLevel string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// permissionLevelNone is a permission set to `none`.
|
|
||||||
permissionLevelNone permissionLevel = "none"
|
|
||||||
// permissionLevelRead is a permission set to `read`.
|
|
||||||
permissionLevelRead permissionLevel = "read"
|
|
||||||
// permissionLevelUnknown is for other kinds of alerts, mostly to support debug messages.
|
|
||||||
// TODO: remove it once we have implemented severity (#1874).
|
|
||||||
permissionLevelUnknown permissionLevel = "unknown"
|
|
||||||
// permissionLevelUndeclared is an undeclared permission.
|
|
||||||
permissionLevelUndeclared permissionLevel = "undeclared"
|
|
||||||
// permissionLevelWrite is a permission set to `write` for a permission we consider potentially dangerous.
|
|
||||||
permissionLevelWrite permissionLevel = "write"
|
|
||||||
)
|
|
||||||
|
|
||||||
// permissionLocation represents a declaration type.
|
|
||||||
type permissionLocationType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// permissionLocationNil is in case the permission is nil.
|
|
||||||
permissionLocationNil permissionLocationType = "nil"
|
|
||||||
// permissionLocationNotDeclared is for undeclared permission.
|
|
||||||
permissionLocationNotDeclared permissionLocationType = "not declared"
|
|
||||||
// permissionLocationTop is top-level workflow permission.
|
|
||||||
permissionLocationTop permissionLocationType = "top"
|
|
||||||
// permissionLocationJob is job-level workflow permission.
|
|
||||||
permissionLocationJob permissionLocationType = "job"
|
|
||||||
)
|
|
||||||
|
|
||||||
// permissionType represents a permission type.
|
|
||||||
type permissionType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// permissionTypeNone represents none permission type.
|
|
||||||
permissionTypeNone permissionType = "none"
|
|
||||||
// permissionTypeNone is the "all" github permission type.
|
|
||||||
permissionTypeAll permissionType = "all"
|
|
||||||
// permissionTypeNone is the "statuses" github permission type.
|
|
||||||
permissionTypeStatuses permissionType = "statuses"
|
|
||||||
// permissionTypeNone is the "checks" github permission type.
|
|
||||||
permissionTypeChecks permissionType = "checks"
|
|
||||||
// permissionTypeNone is the "security-events" github permission type.
|
|
||||||
permissionTypeSecurityEvents permissionType = "security-events"
|
|
||||||
// permissionTypeNone is the "deployments" github permission type.
|
|
||||||
permissionTypeDeployments permissionType = "deployments"
|
|
||||||
// permissionTypeNone is the "packages" github permission type.
|
|
||||||
permissionTypePackages permissionType = "packages"
|
|
||||||
// permissionTypeNone is the "actions" github permission type.
|
|
||||||
permissionTypeActions permissionType = "actions"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TokenPermissions applies the score policy for the Token-Permissions check.
|
|
||||||
func TokenPermissions(name string, c *checker.CheckRequest, r *checker.TokenPermissionsData) checker.CheckResult {
|
|
||||||
if r == nil {
|
|
||||||
e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data")
|
|
||||||
return checker.CreateRuntimeErrorResult(name, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.NumTokens == 0 {
|
|
||||||
return checker.CreateInconclusiveResult(name, "no tokens found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a temporary step that should be replaced by probes in ./probes
|
|
||||||
findings, err := rawToFindings(r)
|
|
||||||
if err != nil {
|
|
||||||
e := sce.WithMessage(sce.ErrScorecardInternal, "could not convert raw data to findings")
|
|
||||||
return checker.CreateRuntimeErrorResult(name, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
score, err := applyScorePolicy(findings, c)
|
|
||||||
if err != nil {
|
|
||||||
return checker.CreateRuntimeErrorResult(name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if score != checker.MaxResultScore {
|
|
||||||
return checker.CreateResultWithScore(name,
|
|
||||||
"detected GitHub workflow tokens with excessive permissions", score)
|
|
||||||
}
|
|
||||||
|
|
||||||
return checker.CreateMaxScoreResult(name,
|
|
||||||
"GitHub workflow tokens follow principle of least privilege")
|
|
||||||
}
|
|
||||||
|
|
||||||
// rawToFindings is a temporary step for converting the raw results
|
|
||||||
// to findings. This should be replaced by probes in ./probes.
|
|
||||||
func rawToFindings(results *checker.TokenPermissionsData) ([]finding.Finding, error) {
|
|
||||||
var findings []finding.Finding
|
|
||||||
|
|
||||||
for _, r := range results.TokenPermissions {
|
|
||||||
var loc *finding.Location
|
|
||||||
if r.File != nil {
|
|
||||||
loc = &finding.Location{
|
|
||||||
Type: r.File.Type,
|
|
||||||
Path: r.File.Path,
|
|
||||||
LineStart: newUint(r.File.Offset),
|
|
||||||
}
|
|
||||||
if r.File.Snippet != "" {
|
|
||||||
loc.Snippet = newStr(r.File.Snippet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text, err := createText(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := createFinding(r.LocationType, text, loc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.Type {
|
|
||||||
case checker.PermissionLevelNone:
|
|
||||||
f = f.WithOutcome(finding.OutcomePositive)
|
|
||||||
f = f.WithValue("PermissionLevel", string(permissionLevelNone))
|
|
||||||
case checker.PermissionLevelRead:
|
|
||||||
f = f.WithOutcome(finding.OutcomePositive)
|
|
||||||
f = f.WithValue("PermissionLevel", string(permissionLevelRead))
|
|
||||||
case checker.PermissionLevelUnknown:
|
|
||||||
f = f.WithValue("PermissionLevel", string(permissionLevelUnknown))
|
|
||||||
f = f.WithOutcome(finding.OutcomeError)
|
|
||||||
case checker.PermissionLevelUndeclared:
|
|
||||||
var locationType permissionLocationType
|
|
||||||
//nolint:gocritic
|
|
||||||
if r.LocationType == nil {
|
|
||||||
locationType = permissionLocationNil
|
|
||||||
} else if *r.LocationType == checker.PermissionLocationTop {
|
|
||||||
locationType = permissionLocationTop
|
|
||||||
} else {
|
|
||||||
locationType = permissionLocationNotDeclared
|
|
||||||
}
|
|
||||||
permType := permTypeToEnum(r.Name)
|
|
||||||
f = f.WithValues(map[string]string{
|
|
||||||
"PermissionLevel": string(permissionLevelUndeclared),
|
|
||||||
"LocationType": string(locationType),
|
|
||||||
"PermissionType": string(permType),
|
|
||||||
})
|
|
||||||
case checker.PermissionLevelWrite:
|
|
||||||
var locationType permissionLocationType
|
|
||||||
switch *r.LocationType {
|
|
||||||
case checker.PermissionLocationTop:
|
|
||||||
locationType = permissionLocationTop
|
|
||||||
case checker.PermissionLocationJob:
|
|
||||||
locationType = permissionLocationJob
|
|
||||||
default:
|
|
||||||
locationType = permissionLocationNotDeclared
|
|
||||||
}
|
|
||||||
permType := permTypeToEnum(r.Name)
|
|
||||||
f = f.WithValues(map[string]string{
|
|
||||||
"PermissionLevel": string(permissionLevelWrite),
|
|
||||||
"LocationType": string(locationType),
|
|
||||||
"PermissionType": string(permType),
|
|
||||||
})
|
|
||||||
f = f.WithOutcome(finding.OutcomeNegative)
|
|
||||||
}
|
|
||||||
findings = append(findings, *f)
|
|
||||||
}
|
|
||||||
return findings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func permTypeToEnum(tokenName *string) permissionType {
|
|
||||||
if tokenName == nil {
|
|
||||||
return permissionTypeNone
|
|
||||||
}
|
|
||||||
switch *tokenName {
|
|
||||||
//nolint:goconst
|
|
||||||
case "all":
|
|
||||||
return permissionTypeAll
|
|
||||||
case "statuses":
|
|
||||||
return permissionTypeStatuses
|
|
||||||
case "checks":
|
|
||||||
return permissionTypeChecks
|
|
||||||
case "security-events":
|
|
||||||
return permissionTypeSecurityEvents
|
|
||||||
case "deployments":
|
|
||||||
return permissionTypeDeployments
|
|
||||||
case "contents":
|
|
||||||
return permissionTypePackages
|
|
||||||
case "actions":
|
|
||||||
return permissionTypeActions
|
|
||||||
default:
|
|
||||||
return permissionTypeNone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func permTypeToName(permType string) *string {
|
|
||||||
var permName string
|
|
||||||
switch permissionType(permType) {
|
|
||||||
case permissionTypeAll:
|
|
||||||
permName = "all"
|
|
||||||
case permissionTypeStatuses:
|
|
||||||
permName = "statuses"
|
|
||||||
case permissionTypeChecks:
|
|
||||||
permName = "checks"
|
|
||||||
case permissionTypeSecurityEvents:
|
|
||||||
permName = "security-events"
|
|
||||||
case permissionTypeDeployments:
|
|
||||||
permName = "deployments"
|
|
||||||
case permissionTypePackages:
|
|
||||||
permName = "contents"
|
|
||||||
case permissionTypeActions:
|
|
||||||
permName = "actions"
|
|
||||||
default:
|
|
||||||
permName = ""
|
|
||||||
}
|
|
||||||
return &permName
|
|
||||||
}
|
|
||||||
|
|
||||||
func createFinding(loct *checker.PermissionLocation, text string, loc *finding.Location) (*finding.Finding, error) {
|
|
||||||
probe := stepsNoWriteID
|
|
||||||
if loct == nil || *loct == checker.PermissionLocationTop {
|
|
||||||
probe = topNoWriteID
|
|
||||||
}
|
|
||||||
content, err := probes.ReadFile(probe + ".yml")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("reading %v.yml: %w", probe, err)
|
|
||||||
}
|
|
||||||
f, err := finding.FromBytes(content, probe)
|
|
||||||
if err != nil {
|
|
||||||
return nil,
|
|
||||||
sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
|
||||||
}
|
|
||||||
f = f.WithMessage(text)
|
|
||||||
if loc != nil {
|
|
||||||
f = f.WithLocation(loc)
|
|
||||||
}
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// avoid memory aliasing by returning a new copy.
|
|
||||||
func newUint(u uint) *uint {
|
|
||||||
return &u
|
|
||||||
}
|
|
||||||
|
|
||||||
// avoid memory aliasing by returning a new copy.
|
|
||||||
func newStr(s string) *string {
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyScorePolicy(findings []finding.Finding, c *checker.CheckRequest) (int, error) {
|
|
||||||
// See list https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/.
|
|
||||||
// Note: there are legitimate reasons to use some of the permissions like checks, deployments, etc.
|
|
||||||
// in CI/CD systems https://docs.travis-ci.com/user/github-oauth-scopes/.
|
|
||||||
|
|
||||||
hm := make(map[string]permissions)
|
|
||||||
dl := c.Dlogger
|
|
||||||
//nolint:errcheck
|
|
||||||
remediationMetadata, _ := remediation.New(c)
|
|
||||||
negativeProbeResults := map[string]bool{
|
|
||||||
stepsNoWriteID: false,
|
|
||||||
topNoWriteID: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range findings {
|
|
||||||
f := &findings[i]
|
|
||||||
pLevel := permissionLevel(f.Values["PermissionLevel"])
|
|
||||||
switch pLevel {
|
|
||||||
case permissionLevelNone, permissionLevelRead:
|
|
||||||
dl.Info(&checker.LogMessage{
|
|
||||||
Finding: f,
|
|
||||||
})
|
|
||||||
case permissionLevelUnknown:
|
|
||||||
dl.Debug(&checker.LogMessage{
|
|
||||||
Finding: f,
|
|
||||||
})
|
|
||||||
|
|
||||||
case permissionLevelUndeclared:
|
|
||||||
switch permissionLocationType(f.Values["LocationType"]) {
|
|
||||||
case permissionLocationNil:
|
|
||||||
return checker.InconclusiveResultScore,
|
|
||||||
sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil")
|
|
||||||
case permissionLocationTop:
|
|
||||||
warnWithRemediation(dl, remediationMetadata, f, negativeProbeResults)
|
|
||||||
default:
|
|
||||||
// We warn only for top-level.
|
|
||||||
dl.Debug(&checker.LogMessage{
|
|
||||||
Finding: f,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group results by workflow name for score computation.
|
|
||||||
if err := updateWorkflowHashMap(hm, f); err != nil {
|
|
||||||
return checker.InconclusiveResultScore, err
|
|
||||||
}
|
|
||||||
|
|
||||||
case permissionLevelWrite:
|
|
||||||
warnWithRemediation(dl, remediationMetadata, f, negativeProbeResults)
|
|
||||||
|
|
||||||
// Group results by workflow name for score computation.
|
|
||||||
if err := updateWorkflowHashMap(hm, f); err != nil {
|
|
||||||
return checker.InconclusiveResultScore, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := reportDefaultFindings(findings, c.Dlogger, negativeProbeResults); err != nil {
|
|
||||||
return checker.InconclusiveResultScore, err
|
|
||||||
}
|
|
||||||
return calculateScore(hm), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func reportDefaultFindings(results []finding.Finding,
|
|
||||||
dl checker.DetailLogger, negativeProbeResults map[string]bool,
|
|
||||||
) error {
|
|
||||||
// Workflow files found, report positive findings if no
|
|
||||||
// negative findings were found.
|
|
||||||
// NOTE: we don't consider probe `topNoWriteID`
|
|
||||||
// because positive results are already reported.
|
|
||||||
found := negativeProbeResults[stepsNoWriteID]
|
|
||||||
if !found {
|
|
||||||
text := fmt.Sprintf("no %s write permissions found", checker.PermissionLocationJob)
|
|
||||||
if err := reportFinding(stepsNoWriteID,
|
|
||||||
text, finding.OutcomePositive, dl); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func reportFinding(probe, text string, o finding.Outcome, dl checker.DetailLogger) error {
|
|
||||||
content, err := probes.ReadFile(probe + ".yml")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w", err)
|
|
||||||
}
|
|
||||||
f, err := finding.FromBytes(content, probe)
|
|
||||||
if err != nil {
|
|
||||||
return sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
|
||||||
}
|
|
||||||
f = f.WithMessage(text).WithOutcome(o)
|
|
||||||
dl.Info(&checker.LogMessage{
|
|
||||||
Finding: f,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func warnWithRemediation(logger checker.DetailLogger,
|
|
||||||
rem *remediation.RemediationMetadata,
|
|
||||||
f *finding.Finding,
|
|
||||||
negativeProbeResults map[string]bool,
|
|
||||||
) {
|
|
||||||
if f.Location != nil && f.Location.Path != "" {
|
|
||||||
f = f.WithRemediationMetadata(map[string]string{
|
|
||||||
"repo": rem.Repo,
|
|
||||||
"branch": rem.Branch,
|
|
||||||
"workflow": strings.TrimPrefix(f.Location.Path, ".github/workflows/"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
logger.Warn(&checker.LogMessage{
|
|
||||||
Finding: f,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Record that we found a negative result.
|
|
||||||
negativeProbeResults[f.Probe] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func recordPermissionWrite(hm map[string]permissions, path string,
|
|
||||||
locType permissionLocationType, permType string,
|
|
||||||
) {
|
|
||||||
if _, exists := hm[path]; !exists {
|
|
||||||
hm[path] = permissions{
|
|
||||||
topLevelWritePermissions: make(map[string]bool),
|
|
||||||
jobLevelWritePermissions: make(map[string]bool),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select the hash map to update.
|
|
||||||
m := hm[path].jobLevelWritePermissions
|
|
||||||
if locType == permissionLocationTop {
|
|
||||||
m = hm[path].topLevelWritePermissions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the permission name to record.
|
|
||||||
permName := permTypeToName(permType)
|
|
||||||
name := "all"
|
|
||||||
if permName != nil && *permName != "" {
|
|
||||||
name = *permName
|
|
||||||
}
|
|
||||||
m[name] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateWorkflowHashMap(hm map[string]permissions, f *finding.Finding) error {
|
|
||||||
if _, ok := f.Values["LocationType"]; !ok {
|
|
||||||
return sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.Location == nil || f.Location.Path == "" {
|
|
||||||
return sce.WithMessage(sce.ErrScorecardInternal, "path is not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
if permissionLevel(f.Values["PermissionLevel"]) != permissionLevelWrite &&
|
|
||||||
permissionLevel(f.Values["PermissionLevel"]) != permissionLevelUndeclared {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
plt := permissionLocationType(f.Values["LocationType"])
|
|
||||||
recordPermissionWrite(hm, f.Location.Path, plt, f.Values["PermissionType"])
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createText(t checker.TokenPermission) (string, error) {
|
|
||||||
// By default, use the message already present.
|
|
||||||
if t.Msg != nil {
|
|
||||||
return *t.Msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure there's no implementation bug.
|
|
||||||
if t.LocationType == nil {
|
|
||||||
return "", sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a different text depending on the type.
|
|
||||||
if t.Type == checker.PermissionLevelUndeclared {
|
|
||||||
return fmt.Sprintf("no %s permission defined", *t.LocationType), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.Value == nil {
|
|
||||||
return "", sce.WithMessage(sce.ErrScorecardInternal, "Value fields is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.Name == nil {
|
|
||||||
return fmt.Sprintf("%s permissions set to '%v'", *t.LocationType,
|
|
||||||
*t.Value), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s '%v' permission set to '%v'", *t.LocationType,
|
|
||||||
*t.Name, *t.Value), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the score.
|
|
||||||
func calculateScore(result map[string]permissions) int {
|
|
||||||
// See list https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/.
|
|
||||||
// Note: there are legitimate reasons to use some of the permissions like checks, deployments, etc.
|
|
||||||
// in CI/CD systems https://docs.travis-ci.com/user/github-oauth-scopes/.
|
|
||||||
|
|
||||||
// Start with a perfect score.
|
|
||||||
score := float32(checker.MaxResultScore)
|
|
||||||
|
|
||||||
// Retrieve the overall results.
|
|
||||||
for _, perms := range result {
|
|
||||||
// If no top level permissions are defined, all the permissions
|
|
||||||
// are enabled by default. In this case,
|
|
||||||
if permissionIsPresentInTopLevel(perms, "all") {
|
|
||||||
if permissionIsPresentInRunLevel(perms, "all") {
|
|
||||||
// ... give lowest score if no run level permissions are defined either.
|
|
||||||
return checker.MinResultScore
|
|
||||||
}
|
|
||||||
// ... reduce score if run level permissions are defined.
|
|
||||||
score -= 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
// status: https://docs.github.com/en/rest/reference/repos#statuses.
|
|
||||||
// May allow an attacker to change the result of pre-submit and get a PR merged.
|
|
||||||
// Low risk: -0.5.
|
|
||||||
if permissionIsPresentInTopLevel(perms, "statuses") {
|
|
||||||
score -= 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks.
|
|
||||||
// May allow an attacker to edit checks to remove pre-submit and introduce a bug.
|
|
||||||
// Low risk: -0.5.
|
|
||||||
if permissionIsPresentInTopLevel(perms, "checks") {
|
|
||||||
score -= 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
// secEvents.
|
|
||||||
// May allow attacker to read vuln reports before patch available.
|
|
||||||
// Low risk: -1
|
|
||||||
if permissionIsPresentInTopLevel(perms, "security-events") {
|
|
||||||
score--
|
|
||||||
}
|
|
||||||
|
|
||||||
// deployments: https://docs.github.com/en/rest/reference/repos#deployments.
|
|
||||||
// May allow attacker to charge repo owner by triggering VM runs,
|
|
||||||
// and tiny chance an attacker can trigger a remote
|
|
||||||
// service with code they own if server accepts code/location var unsanitized.
|
|
||||||
// Low risk: -1
|
|
||||||
if permissionIsPresentInTopLevel(perms, "deployments") {
|
|
||||||
score--
|
|
||||||
}
|
|
||||||
|
|
||||||
// contents.
|
|
||||||
// Allows attacker to commit unreviewed code.
|
|
||||||
// High risk: -10
|
|
||||||
if permissionIsPresentInTopLevel(perms, "contents") {
|
|
||||||
score -= checker.MaxResultScore
|
|
||||||
}
|
|
||||||
|
|
||||||
// packages: https://docs.github.com/en/packages/learn-github-packages/about-permissions-for-github-packages.
|
|
||||||
// Allows attacker to publish packages.
|
|
||||||
// High risk: -10
|
|
||||||
if permissionIsPresentInTopLevel(perms, "packages") {
|
|
||||||
score -= checker.MaxResultScore
|
|
||||||
}
|
|
||||||
|
|
||||||
// actions.
|
|
||||||
// May allow an attacker to steal GitHub secrets by approving to run an action that needs approval.
|
|
||||||
// High risk: -10
|
|
||||||
if permissionIsPresentInTopLevel(perms, "actions") {
|
|
||||||
score -= checker.MaxResultScore
|
|
||||||
}
|
|
||||||
|
|
||||||
if score < checker.MinResultScore {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're done, calculate the final score.
|
|
||||||
if score < checker.MinResultScore {
|
|
||||||
return checker.MinResultScore
|
|
||||||
}
|
|
||||||
|
|
||||||
return int(score)
|
|
||||||
}
|
|
||||||
|
|
||||||
func permissionIsPresentInTopLevel(perms permissions, name string) bool {
|
|
||||||
_, ok := perms.topLevelWritePermissions[name]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func permissionIsPresentInRunLevel(perms permissions, name string) bool {
|
|
||||||
_, ok := perms.jobLevelWritePermissions[name]
|
|
||||||
return ok
|
|
||||||
}
|
|
@ -16,9 +16,11 @@ package checks
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ossf/scorecard/v4/checker"
|
"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"
|
"github.com/ossf/scorecard/v4/checks/raw"
|
||||||
sce "github.com/ossf/scorecard/v4/errors"
|
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.
|
// 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 checker.CreateRuntimeErrorResult(CheckTokenPermissions, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return raw results.
|
// Set the raw results.
|
||||||
if c.RawResults != nil {
|
pRawResults := getRawResults(c)
|
||||||
c.RawResults.TokenPermissionsResults = rawData
|
pRawResults.TokenPermissionsResults = rawData
|
||||||
|
|
||||||
|
// Evaluate the probes.
|
||||||
|
findings, err := zrunner.Run(pRawResults, probes.TokenPermissions)
|
||||||
|
if err != nil {
|
||||||
|
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
|
||||||
|
return checker.CreateRuntimeErrorResult(CheckTokenPermissions, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the score evaluation.
|
// Return the score evaluation.
|
||||||
return evaluation.TokenPermissions(CheckTokenPermissions, c, &rawData)
|
return evaluation.TokenPermissions(CheckTokenPermissions, findings, c.Dlogger)
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ func TestGithubTokenPermissions(t *testing.T) {
|
|||||||
Error: nil,
|
Error: nil,
|
||||||
Score: checker.MinResultScore,
|
Score: checker.MinResultScore,
|
||||||
NumberOfWarn: 1,
|
NumberOfWarn: 1,
|
||||||
NumberOfInfo: 1,
|
NumberOfInfo: 0,
|
||||||
NumberOfDebug: 5,
|
NumberOfDebug: 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -104,6 +104,7 @@ var validateGitHubActionTokenPermissions fileparser.DoWhileTrueOnFileContent = f
|
|||||||
// 2. Run-level permission definitions,
|
// 2. Run-level permission definitions,
|
||||||
// see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idpermissions.
|
// see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idpermissions.
|
||||||
ignoredPermissions := createIgnoredPermissions(workflow, path, pdata)
|
ignoredPermissions := createIgnoredPermissions(workflow, path, pdata)
|
||||||
|
|
||||||
if err := validatejobLevelPermissions(workflow, path, pdata, ignoredPermissions); err != nil {
|
if err := validatejobLevelPermissions(workflow, path, pdata, ignoredPermissions); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -33,10 +33,12 @@ import (
|
|||||||
"github.com/ossf/scorecard/v4/probes/hasFSFOrOSIApprovedLicense"
|
"github.com/ossf/scorecard/v4/probes/hasFSFOrOSIApprovedLicense"
|
||||||
"github.com/ossf/scorecard/v4/probes/hasLicenseFile"
|
"github.com/ossf/scorecard/v4/probes/hasLicenseFile"
|
||||||
"github.com/ossf/scorecard/v4/probes/hasLicenseFileAtTopDir"
|
"github.com/ossf/scorecard/v4/probes/hasLicenseFileAtTopDir"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionUnknown"
|
||||||
"github.com/ossf/scorecard/v4/probes/hasOSVVulnerabilities"
|
"github.com/ossf/scorecard/v4/probes/hasOSVVulnerabilities"
|
||||||
"github.com/ossf/scorecard/v4/probes/hasOpenSSFBadge"
|
"github.com/ossf/scorecard/v4/probes/hasOpenSSFBadge"
|
||||||
"github.com/ossf/scorecard/v4/probes/hasRecentCommits"
|
"github.com/ossf/scorecard/v4/probes/hasRecentCommits"
|
||||||
"github.com/ossf/scorecard/v4/probes/issueActivityByProjectMember"
|
"github.com/ossf/scorecard/v4/probes/issueActivityByProjectMember"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/jobLevelPermissions"
|
||||||
"github.com/ossf/scorecard/v4/probes/notArchived"
|
"github.com/ossf/scorecard/v4/probes/notArchived"
|
||||||
"github.com/ossf/scorecard/v4/probes/notCreatedRecently"
|
"github.com/ossf/scorecard/v4/probes/notCreatedRecently"
|
||||||
"github.com/ossf/scorecard/v4/probes/packagedWithAutomatedWorkflow"
|
"github.com/ossf/scorecard/v4/probes/packagedWithAutomatedWorkflow"
|
||||||
@ -59,6 +61,7 @@ import (
|
|||||||
"github.com/ossf/scorecard/v4/probes/toolDependabotInstalled"
|
"github.com/ossf/scorecard/v4/probes/toolDependabotInstalled"
|
||||||
"github.com/ossf/scorecard/v4/probes/toolPyUpInstalled"
|
"github.com/ossf/scorecard/v4/probes/toolPyUpInstalled"
|
||||||
"github.com/ossf/scorecard/v4/probes/toolRenovateInstalled"
|
"github.com/ossf/scorecard/v4/probes/toolRenovateInstalled"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/topLevelPermissions"
|
||||||
"github.com/ossf/scorecard/v4/probes/webhooksUseSecrets"
|
"github.com/ossf/scorecard/v4/probes/webhooksUseSecrets"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -150,6 +153,11 @@ var (
|
|||||||
PinnedDependencies = []ProbeImpl{
|
PinnedDependencies = []ProbeImpl{
|
||||||
pinsDependencies.Run,
|
pinsDependencies.Run,
|
||||||
}
|
}
|
||||||
|
TokenPermissions = []ProbeImpl{
|
||||||
|
hasNoGitHubWorkflowPermissionUnknown.Run,
|
||||||
|
jobLevelPermissions.Run,
|
||||||
|
topLevelPermissions.Run,
|
||||||
|
}
|
||||||
|
|
||||||
// Probes which aren't included by any checks.
|
// Probes which aren't included by any checks.
|
||||||
// These still need to be listed so they can be called with --probes.
|
// These still need to be listed so they can be called with --probes.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright 2023 OpenSSF Scorecard Authors
|
# Copyright 2024 OpenSSF Scorecard Authors
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -12,24 +12,24 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
id: gitHubWorkflowPermissionsTopNoWrite
|
id: hasNoGitHubWorkflowPermissionUnknown
|
||||||
short: Checks that GitHub workflows do not have default write permissions
|
short: Checks that GitHub workflows have workflows with unknown permissions
|
||||||
motivation: >
|
motivation: >
|
||||||
If no permissions are declared, a workflow's GitHub token's permissions default to write for all scopes.
|
Unknown permissions may be a result of a bug or another error from fetching the permission levels.
|
||||||
This include write permissions to push to the repository, to read encrypted secrets, etc.
|
|
||||||
For more information, see https://docs.github.com/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token.
|
|
||||||
implementation: >
|
implementation: >
|
||||||
The rule is implemented by checking whether the `permissions` keyword is defined at the top of the workflow,
|
The probe checks the permission levels of a projects workflows and collects the workflows that have unknown permissions.
|
||||||
and that no write permissions are given.
|
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:
|
remediation:
|
||||||
effort: Low
|
effort: Low
|
||||||
text:
|
text:
|
||||||
- Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?enable=permissions
|
- Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions
|
||||||
- Tick the 'Restrict permissions for GITHUB_TOKEN'
|
- Tick the 'Restrict permissions for GITHUB_TOKEN'
|
||||||
- Untick other options
|
- Untick other options
|
||||||
- "NOTE: If you want to resolve multiple issues at once, you can visit https://app.stepsecurity.io/securerepo instead."
|
- "NOTE: If you want to resolve multiple issues at once, you can visit https://app.stepsecurity.io/securerepo instead."
|
||||||
markdown:
|
markdown:
|
||||||
- Visit [https://app.stepsecurity.io/secureworkflow](https://app.stepsecurity.io/secureworkflow/${{ metadata.repo }}/${{ metadata.workflow }}/${{ metadata.branch }}?enable=permissions).
|
- Visit [https://app.stepsecurity.io/secureworkflow](https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions).
|
||||||
- Tick the 'Restrict permissions for GITHUB_TOKEN'
|
- Tick the 'Restrict permissions for GITHUB_TOKEN'
|
||||||
- Untick other options
|
- 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."
|
- "NOTE: If you want to resolve multiple issues at once, you can visit [https://app.stepsecurity.io/securerepo](https://app.stepsecurity.io/securerepo) instead."
|
75
probes/hasNoGitHubWorkflowPermissionUnknown/impl.go
Normal file
75
probes/hasNoGitHubWorkflowPermissionUnknown/impl.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 2024 OpenSSF Scorecard Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//nolint:stylecheck
|
||||||
|
package hasNoGitHubWorkflowPermissionUnknown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
"github.com/ossf/scorecard/v4/finding"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/internal/utils/permissions"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.yml
|
||||||
|
var fs embed.FS
|
||||||
|
|
||||||
|
const Probe = "hasNoGitHubWorkflowPermissionUnknown"
|
||||||
|
|
||||||
|
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||||
|
if raw == nil {
|
||||||
|
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := raw.TokenPermissionsResults
|
||||||
|
var findings []finding.Finding
|
||||||
|
|
||||||
|
if results.NumTokens == 0 {
|
||||||
|
f, err := finding.NewWith(fs, Probe,
|
||||||
|
"No token permissions found",
|
||||||
|
nil, finding.OutcomeNotApplicable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
return findings, Probe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range results.TokenPermissions {
|
||||||
|
if r.Type != checker.PermissionLevelUnknown {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create finding
|
||||||
|
f, err := permissions.CreateNegativeFinding(r, Probe, fs, raw.Metadata.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(findings) == 0 {
|
||||||
|
f, err := finding.NewWith(fs, Probe,
|
||||||
|
"no workflows with unknown permissions",
|
||||||
|
nil, finding.OutcomePositive)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
}
|
||||||
|
return findings, Probe, nil
|
||||||
|
}
|
98
probes/hasNoGitHubWorkflowPermissionUnknown/impl_test.go
Normal file
98
probes/hasNoGitHubWorkflowPermissionUnknown/impl_test.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// Copyright 2024 OpenSSF Scorecard Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//nolint:stylecheck
|
||||||
|
package hasNoGitHubWorkflowPermissionUnknown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
"github.com/ossf/scorecard/v4/finding"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/internal/utils/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Run(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
permLoc := checker.PermissionLocationTop
|
||||||
|
value := "value"
|
||||||
|
tests := []test.TestData{
|
||||||
|
{
|
||||||
|
Name: "No Tokens",
|
||||||
|
Raw: &checker.RawResults{
|
||||||
|
TokenPermissionsResults: checker.TokenPermissionsData{
|
||||||
|
NumTokens: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outcomes: []finding.Outcome{
|
||||||
|
finding.OutcomeNotApplicable,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Correct permission level",
|
||||||
|
Raw: &checker.RawResults{
|
||||||
|
TokenPermissionsResults: checker.TokenPermissionsData{
|
||||||
|
NumTokens: 1,
|
||||||
|
TokenPermissions: []checker.TokenPermission{
|
||||||
|
{
|
||||||
|
Type: checker.PermissionLevelUnknown,
|
||||||
|
LocationType: &permLoc,
|
||||||
|
Value: &value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outcomes: []finding.Outcome{
|
||||||
|
finding.OutcomeNegative,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Incorrect permission level",
|
||||||
|
Raw: &checker.RawResults{
|
||||||
|
TokenPermissionsResults: checker.TokenPermissionsData{
|
||||||
|
NumTokens: 1,
|
||||||
|
TokenPermissions: []checker.TokenPermission{
|
||||||
|
{
|
||||||
|
Type: checker.PermissionLevelRead,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outcomes: []finding.Outcome{
|
||||||
|
finding.OutcomePositive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||||
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
findings, s, err := Run(tt.Raw)
|
||||||
|
if !cmp.Equal(tt.Err, err, cmpopts.EquateErrors()) {
|
||||||
|
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.Err, err, cmpopts.EquateErrors()))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||||
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
test.AssertOutcomes(t, findings, tt.Outcomes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
169
probes/internal/utils/permissions/permissions.go
Normal file
169
probes/internal/utils/permissions/permissions.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// Copyright 2024 OpenSSF Scorecard Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
sce "github.com/ossf/scorecard/v4/errors"
|
||||||
|
"github.com/ossf/scorecard/v4/finding"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createText(t checker.TokenPermission) (string, error) {
|
||||||
|
// By default, use the message already present.
|
||||||
|
if t.Msg != nil {
|
||||||
|
return *t.Msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure there's no implementation bug.
|
||||||
|
if t.LocationType == nil {
|
||||||
|
return "", sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a different text depending on the type.
|
||||||
|
if t.Type == checker.PermissionLevelUndeclared {
|
||||||
|
return fmt.Sprintf("no %s permission defined", *t.LocationType), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Value == nil {
|
||||||
|
return "", sce.WithMessage(sce.ErrScorecardInternal, "Value fields is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Name == nil {
|
||||||
|
return fmt.Sprintf("%s permissions set to '%v'", *t.LocationType,
|
||||||
|
*t.Value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s '%v' permission set to '%v'", *t.LocationType,
|
||||||
|
*t.Name, *t.Value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateNegativeFinding(r checker.TokenPermission,
|
||||||
|
probe string,
|
||||||
|
fs embed.FS,
|
||||||
|
metadata map[string]string,
|
||||||
|
) (*finding.Finding, error) {
|
||||||
|
// Create finding
|
||||||
|
text, err := createText(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
f, err := finding.NewWith(fs, probe,
|
||||||
|
text, nil, finding.OutcomeNegative)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.File != nil {
|
||||||
|
f = f.WithLocation(r.File.Location())
|
||||||
|
workflowPath := strings.TrimPrefix(f.Location.Path, ".github/workflows/")
|
||||||
|
f = f.WithRemediationMetadata(map[string]string{"workflow": workflowPath})
|
||||||
|
}
|
||||||
|
if metadata != nil {
|
||||||
|
f = f.WithRemediationMetadata(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Name != nil {
|
||||||
|
f = f.WithValue("tokenName", *r.Name)
|
||||||
|
}
|
||||||
|
f = f.WithValue("permissionLevel", string(r.Type))
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadPositiveLevelFinding(probe string,
|
||||||
|
fs embed.FS,
|
||||||
|
r checker.TokenPermission,
|
||||||
|
metadata map[string]string,
|
||||||
|
) (*finding.Finding, error) {
|
||||||
|
f, err := finding.NewWith(fs, probe,
|
||||||
|
"found token with 'read' permissions",
|
||||||
|
nil, finding.OutcomePositive)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w", err)
|
||||||
|
}
|
||||||
|
if r.File != nil {
|
||||||
|
f = f.WithLocation(r.File.Location())
|
||||||
|
workflowPath := strings.TrimPrefix(f.Location.Path, ".github/workflows/")
|
||||||
|
f = f.WithRemediationMetadata(map[string]string{"workflow": workflowPath})
|
||||||
|
}
|
||||||
|
if metadata != nil {
|
||||||
|
f = f.WithRemediationMetadata(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
f = f.WithValue("permissionLevel", "read")
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateNoneFinding(probe string,
|
||||||
|
fs embed.FS,
|
||||||
|
r checker.TokenPermission,
|
||||||
|
metadata map[string]string,
|
||||||
|
) (*finding.Finding, error) {
|
||||||
|
// Create finding
|
||||||
|
f, err := finding.NewWith(fs, probe,
|
||||||
|
"found token with 'none' permissions",
|
||||||
|
nil, finding.OutcomeNegative)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
if r.File != nil {
|
||||||
|
f = f.WithLocation(r.File.Location())
|
||||||
|
workflowPath := strings.TrimPrefix(f.Location.Path, ".github/workflows/")
|
||||||
|
f = f.WithRemediationMetadata(map[string]string{"workflow": workflowPath})
|
||||||
|
}
|
||||||
|
if metadata != nil {
|
||||||
|
f = f.WithRemediationMetadata(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
f = f.WithValue("permissionLevel", string(r.Type))
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateUndeclaredFinding(probe string,
|
||||||
|
fs embed.FS,
|
||||||
|
r checker.TokenPermission,
|
||||||
|
metadata map[string]string,
|
||||||
|
) (*finding.Finding, error) {
|
||||||
|
var f *finding.Finding
|
||||||
|
var err error
|
||||||
|
switch {
|
||||||
|
case r.LocationType == nil:
|
||||||
|
f, err = finding.NewWith(fs, probe,
|
||||||
|
"could not determine the location type",
|
||||||
|
nil, finding.OutcomeNotApplicable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
case *r.LocationType == checker.PermissionLocationTop,
|
||||||
|
*r.LocationType == checker.PermissionLocationJob:
|
||||||
|
// Create finding
|
||||||
|
f, err = CreateNegativeFinding(r, probe, fs, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
f, err = finding.NewWith(fs, probe,
|
||||||
|
"could not determine the location type",
|
||||||
|
nil, finding.OutcomeError)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f = f.WithValue("permissionLevel", string(r.Type))
|
||||||
|
return f, nil
|
||||||
|
}
|
@ -17,6 +17,8 @@ package test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
sce "github.com/ossf/scorecard/v4/errors"
|
||||||
"github.com/ossf/scorecard/v4/finding"
|
"github.com/ossf/scorecard/v4/finding"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,3 +34,147 @@ func AssertOutcomes(t *testing.T, got []finding.Finding, want []finding.Outcome)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests for permissions-probes.
|
||||||
|
type TestData struct {
|
||||||
|
Name string
|
||||||
|
Err error
|
||||||
|
Raw *checker.RawResults
|
||||||
|
Outcomes []finding.Outcome
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTests(locationType checker.PermissionLocation,
|
||||||
|
permissionType checker.PermissionLevel,
|
||||||
|
tokenName string,
|
||||||
|
) []TestData {
|
||||||
|
name := tokenName // Should come from each probe test.
|
||||||
|
value := "value"
|
||||||
|
var wrongPermissionLocation checker.PermissionLocation
|
||||||
|
if locationType == checker.PermissionLocationTop {
|
||||||
|
wrongPermissionLocation = checker.PermissionLocationJob
|
||||||
|
} else {
|
||||||
|
wrongPermissionLocation = checker.PermissionLocationTop
|
||||||
|
}
|
||||||
|
|
||||||
|
return []TestData{
|
||||||
|
{
|
||||||
|
Name: "No Tokens",
|
||||||
|
Raw: &checker.RawResults{
|
||||||
|
TokenPermissionsResults: checker.TokenPermissionsData{
|
||||||
|
NumTokens: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outcomes: []finding.Outcome{
|
||||||
|
finding.OutcomeNotApplicable,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Correct name",
|
||||||
|
Raw: &checker.RawResults{
|
||||||
|
TokenPermissionsResults: checker.TokenPermissionsData{
|
||||||
|
NumTokens: 1,
|
||||||
|
TokenPermissions: []checker.TokenPermission{
|
||||||
|
{
|
||||||
|
LocationType: &locationType,
|
||||||
|
Name: &name,
|
||||||
|
Value: &value,
|
||||||
|
Msg: nil,
|
||||||
|
Type: permissionType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outcomes: []finding.Outcome{
|
||||||
|
finding.OutcomeNegative,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Two tokens",
|
||||||
|
Raw: &checker.RawResults{
|
||||||
|
TokenPermissionsResults: checker.TokenPermissionsData{
|
||||||
|
NumTokens: 2,
|
||||||
|
TokenPermissions: []checker.TokenPermission{
|
||||||
|
{
|
||||||
|
LocationType: &locationType,
|
||||||
|
Name: &name,
|
||||||
|
Value: &value,
|
||||||
|
Msg: nil,
|
||||||
|
Type: permissionType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
LocationType: &locationType,
|
||||||
|
Name: &name,
|
||||||
|
Value: &value,
|
||||||
|
Msg: nil,
|
||||||
|
Type: permissionType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outcomes: []finding.Outcome{
|
||||||
|
finding.OutcomeNegative, finding.OutcomeNegative,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Value is nil - Everything else correct",
|
||||||
|
Raw: &checker.RawResults{
|
||||||
|
TokenPermissionsResults: checker.TokenPermissionsData{
|
||||||
|
NumTokens: 1,
|
||||||
|
TokenPermissions: []checker.TokenPermission{
|
||||||
|
{
|
||||||
|
LocationType: &locationType,
|
||||||
|
Name: &name,
|
||||||
|
Value: nil,
|
||||||
|
Msg: nil,
|
||||||
|
Type: permissionType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outcomes: []finding.Outcome{
|
||||||
|
finding.OutcomeNegative,
|
||||||
|
},
|
||||||
|
Err: sce.ErrScorecardInternal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Wrong locationType wrong type",
|
||||||
|
Raw: &checker.RawResults{
|
||||||
|
TokenPermissionsResults: checker.TokenPermissionsData{
|
||||||
|
NumTokens: 1,
|
||||||
|
TokenPermissions: []checker.TokenPermission{
|
||||||
|
{
|
||||||
|
LocationType: &wrongPermissionLocation,
|
||||||
|
Name: &name,
|
||||||
|
Value: nil,
|
||||||
|
Msg: nil,
|
||||||
|
Type: checker.PermissionLevel("999"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outcomes: []finding.Outcome{
|
||||||
|
finding.OutcomePositive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Wrong locationType correct type",
|
||||||
|
Raw: &checker.RawResults{
|
||||||
|
TokenPermissionsResults: checker.TokenPermissionsData{
|
||||||
|
NumTokens: 1,
|
||||||
|
TokenPermissions: []checker.TokenPermission{
|
||||||
|
{
|
||||||
|
LocationType: &wrongPermissionLocation,
|
||||||
|
Name: &name,
|
||||||
|
Value: nil,
|
||||||
|
Msg: nil,
|
||||||
|
Type: permissionType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outcomes: []finding.Outcome{
|
||||||
|
finding.OutcomePositive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
35
probes/jobLevelPermissions/def.yml
Normal file
35
probes/jobLevelPermissions/def.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Copyright 2024 OpenSSF Scorecard Authors
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
id: jobLevelPermissions
|
||||||
|
short: Checks that GitHub workflows do not have "write" permissions at the "job" level.
|
||||||
|
motivation: >
|
||||||
|
In some circumstances, having "write" permissions at the "job" level may enable attackers to escalate privileges.
|
||||||
|
implementation: >
|
||||||
|
The probe checks the permission level, the workflow type and the permission type of each workflow in the project.
|
||||||
|
outcome:
|
||||||
|
- The probe returns 1 negative outcome per workflow with "write" permissions at the "job" level.
|
||||||
|
- The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "job" level.
|
||||||
|
remediation:
|
||||||
|
effort: Low
|
||||||
|
text:
|
||||||
|
- Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions
|
||||||
|
- Tick the 'Restrict permissions for GITHUB_TOKEN'
|
||||||
|
- Untick other options
|
||||||
|
- "NOTE: If you want to resolve multiple issues at once, you can visit https://app.stepsecurity.io/securerepo instead."
|
||||||
|
markdown:
|
||||||
|
- Visit [https://app.stepsecurity.io/secureworkflow](https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions).
|
||||||
|
- Tick the 'Restrict permissions for GITHUB_TOKEN'
|
||||||
|
- Untick other options
|
||||||
|
- "NOTE: If you want to resolve multiple issues at once, you can visit [https://app.stepsecurity.io/securerepo](https://app.stepsecurity.io/securerepo) instead."
|
109
probes/jobLevelPermissions/impl.go
Normal file
109
probes/jobLevelPermissions/impl.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// Copyright 2024 OpenSSF Scorecard Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//nolint:stylecheck
|
||||||
|
package jobLevelPermissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
"github.com/ossf/scorecard/v4/finding"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/internal/utils/permissions"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.yml
|
||||||
|
var fs embed.FS
|
||||||
|
|
||||||
|
const Probe = "jobLevelPermissions"
|
||||||
|
|
||||||
|
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||||
|
if raw == nil {
|
||||||
|
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := raw.TokenPermissionsResults
|
||||||
|
var findings []finding.Finding
|
||||||
|
|
||||||
|
if results.NumTokens == 0 {
|
||||||
|
f, err := finding.NewWith(fs, Probe,
|
||||||
|
"No token permissions found",
|
||||||
|
nil, finding.OutcomeNotApplicable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
return findings, Probe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range results.TokenPermissions {
|
||||||
|
if r.LocationType == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if *r.LocationType != checker.PermissionLocationJob {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Type {
|
||||||
|
case checker.PermissionLevelNone:
|
||||||
|
f, err := permissions.CreateNoneFinding(Probe, fs, r, raw.Metadata.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
continue
|
||||||
|
case checker.PermissionLevelUndeclared:
|
||||||
|
f, err := permissions.CreateUndeclaredFinding(Probe, fs, r, raw.Metadata.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
continue
|
||||||
|
case checker.PermissionLevelRead:
|
||||||
|
f, err := permissions.ReadPositiveLevelFinding(Probe, fs, r, raw.Metadata.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
// to satisfy linter
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Name == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := permissions.CreateNegativeFinding(r, Probe, fs, raw.Metadata.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
f = f.WithValue("permissionLevel", string(r.Type))
|
||||||
|
f = f.WithValue("tokenName", *r.Name)
|
||||||
|
findings = append(findings, *f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(findings) == 0 {
|
||||||
|
f, err := finding.NewWith(fs, Probe,
|
||||||
|
"no job-level permissions found",
|
||||||
|
nil, finding.OutcomePositive)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
}
|
||||||
|
return findings, Probe, nil
|
||||||
|
}
|
57
probes/jobLevelPermissions/impl_test.go
Normal file
57
probes/jobLevelPermissions/impl_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2024 OpenSSF Scorecard Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//nolint:stylecheck
|
||||||
|
package jobLevelPermissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/internal/utils/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Run(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "actions")
|
||||||
|
|
||||||
|
tests = append(tests, test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "checks")...)
|
||||||
|
tests = append(tests, test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "contents")...)
|
||||||
|
tests = append(tests, test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "deployments")...)
|
||||||
|
tests = append(tests, test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "packages")...)
|
||||||
|
tests = append(tests, test.GetTests(checker.PermissionLocationJob, checker.PermissionLevelWrite, "security-events")...)
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||||
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
findings, s, err := Run(tt.Raw)
|
||||||
|
if !cmp.Equal(tt.Err, err, cmpopts.EquateErrors()) {
|
||||||
|
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.Err, err, cmpopts.EquateErrors()))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||||
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
test.AssertOutcomes(t, findings, tt.Outcomes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
35
probes/topLevelPermissions/def.yml
Normal file
35
probes/topLevelPermissions/def.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Copyright 2024 OpenSSF Scorecard Authors
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
id: topLevelPermissions
|
||||||
|
short: Checks that the project does not have any top-level write permissions in its workflows.
|
||||||
|
motivation: >
|
||||||
|
In some circumstances, having "write" permissions at the "top" level may enable attackers to escalate privileges.
|
||||||
|
implementation: >
|
||||||
|
The probe checks the permission level, the workflow type and the permission type of each workflow in the project.
|
||||||
|
outcome:
|
||||||
|
- The probe returns 1 negative outcome per workflow with "write" permissions at the "top" level.
|
||||||
|
- The probe returns 1 positive outcome if the project has no workflows "write" permissions a the "top" level.
|
||||||
|
remediation:
|
||||||
|
effort: Low
|
||||||
|
text:
|
||||||
|
- Visit https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions
|
||||||
|
- Tick the 'Restrict permissions for GITHUB_TOKEN'
|
||||||
|
- Untick other options
|
||||||
|
- "NOTE: If you want to resolve multiple issues at once, you can visit https://app.stepsecurity.io/securerepo instead."
|
||||||
|
markdown:
|
||||||
|
- Visit [https://app.stepsecurity.io/secureworkflow](https://app.stepsecurity.io/secureworkflow/${{ metadata.repository.uri }}/${{ metadata.workflow }}/${{ metadata.repository.defaultBranch }}?enable=permissions).
|
||||||
|
- Tick the 'Restrict permissions for GITHUB_TOKEN'
|
||||||
|
- Untick other options
|
||||||
|
- "NOTE: If you want to resolve multiple issues at once, you can visit [https://app.stepsecurity.io/securerepo](https://app.stepsecurity.io/securerepo) instead."
|
118
probes/topLevelPermissions/impl.go
Normal file
118
probes/topLevelPermissions/impl.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
// Copyright 2024 OpenSSF Scorecard Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//nolint:stylecheck
|
||||||
|
package topLevelPermissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
"github.com/ossf/scorecard/v4/finding"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/internal/utils/permissions"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/internal/utils/uerror"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.yml
|
||||||
|
var fs embed.FS
|
||||||
|
|
||||||
|
const Probe = "topLevelPermissions"
|
||||||
|
|
||||||
|
func Run(raw *checker.RawResults) ([]finding.Finding, string, error) {
|
||||||
|
if raw == nil {
|
||||||
|
return nil, "", fmt.Errorf("%w: raw", uerror.ErrNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := raw.TokenPermissionsResults
|
||||||
|
var findings []finding.Finding
|
||||||
|
|
||||||
|
if results.NumTokens == 0 {
|
||||||
|
f, err := finding.NewWith(fs, Probe,
|
||||||
|
"No token permissions found",
|
||||||
|
nil, finding.OutcomeNotApplicable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
return findings, Probe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range results.TokenPermissions {
|
||||||
|
if r.LocationType == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if *r.LocationType != checker.PermissionLocationTop {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Type {
|
||||||
|
case checker.PermissionLevelNone:
|
||||||
|
f, err := permissions.CreateNoneFinding(Probe, fs, r, raw.Metadata.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
continue
|
||||||
|
case checker.PermissionLevelUndeclared:
|
||||||
|
f, err := permissions.CreateUndeclaredFinding(Probe, fs, r, raw.Metadata.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
continue
|
||||||
|
case checker.PermissionLevelRead:
|
||||||
|
f, err := permissions.ReadPositiveLevelFinding(Probe, fs, r, raw.Metadata.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
// to satisfy linter
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenName := ""
|
||||||
|
switch {
|
||||||
|
case r.Name == nil && r.Value == nil:
|
||||||
|
continue
|
||||||
|
case r.Value != nil && *r.Value == "write-all":
|
||||||
|
tokenName = *r.Value
|
||||||
|
case r.Name != nil:
|
||||||
|
tokenName = *r.Name
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create finding
|
||||||
|
f, err := permissions.CreateNegativeFinding(r, Probe, fs, raw.Metadata.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
f = f.WithValue("permissionLevel", string(r.Type))
|
||||||
|
f = f.WithValue("tokenName", tokenName)
|
||||||
|
findings = append(findings, *f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(findings) == 0 {
|
||||||
|
f, err := finding.NewWith(fs, Probe,
|
||||||
|
"no job-level permissions found",
|
||||||
|
nil, finding.OutcomePositive)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Probe, fmt.Errorf("create finding: %w", err)
|
||||||
|
}
|
||||||
|
findings = append(findings, *f)
|
||||||
|
}
|
||||||
|
return findings, Probe, nil
|
||||||
|
}
|
57
probes/topLevelPermissions/impl_test.go
Normal file
57
probes/topLevelPermissions/impl_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2024 OpenSSF Scorecard Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//nolint:stylecheck
|
||||||
|
package topLevelPermissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
|
||||||
|
"github.com/ossf/scorecard/v4/checker"
|
||||||
|
"github.com/ossf/scorecard/v4/probes/internal/utils/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Run(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "actions")
|
||||||
|
|
||||||
|
tests = append(tests, test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "checks")...)
|
||||||
|
tests = append(tests, test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "contents")...)
|
||||||
|
tests = append(tests, test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "deployments")...)
|
||||||
|
tests = append(tests, test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "packages")...)
|
||||||
|
tests = append(tests, test.GetTests(checker.PermissionLocationTop, checker.PermissionLevelWrite, "security-events")...)
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||||
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
findings, s, err := Run(tt.Raw)
|
||||||
|
if !cmp.Equal(tt.Err, err, cmpopts.EquateErrors()) {
|
||||||
|
t.Errorf("mismatch (-want +got):\n%s", cmp.Diff(tt.Err, err, cmpopts.EquateErrors()))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(Probe, s); diff != "" {
|
||||||
|
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
test.AssertOutcomes(t, findings, tt.Outcomes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user