Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🌱 migrate token permission check to probes #3816

Merged
merged 30 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0e9f356
:seedling: migrate token permission check to probes
AdamKorcz Jan 17, 2024
a73974d
combine seperate write-probes into two that combine them all
AdamKorcz Feb 18, 2024
4034c6d
change write probes to read and write
AdamKorcz Mar 2, 2024
d2e1466
minor nit
AdamKorcz Mar 2, 2024
698497e
remove WritaAll probes
AdamKorcz Mar 2, 2024
79486c2
Merge read-perm probe with job/top probes
AdamKorcz Mar 5, 2024
95d4bca
minor refactoring
AdamKorcz Mar 5, 2024
fa1e5a1
fix copy paste error
AdamKorcz Mar 5, 2024
436e65b
fix linter issues and restructure code
AdamKorcz Mar 5, 2024
071f0f8
remove hasGitHubWorkflowPermissionNone probe
AdamKorcz Mar 7, 2024
9aca6c2
Remove 'hasGitHubWorkflowPermissionUndeclared' probe
AdamKorcz Mar 7, 2024
368f459
bit of clean up
AdamKorcz Mar 7, 2024
2988326
reduce code complexity and remove comment
AdamKorcz Mar 7, 2024
e77d7d1
simplify file location
AdamKorcz Mar 7, 2024
2788198
change probe text
AdamKorcz Mar 7, 2024
ed55db8
invert name of probe
AdamKorcz Mar 7, 2024
889ac31
OutcomeNotApplicable -> OutcomeError
AdamKorcz Mar 7, 2024
bf12d7b
OutcomeNotAvailable -> OutcomeNotApplicable
AdamKorcz Mar 7, 2024
aec9fcd
more OutcomeNotAvailable -> OutcomeNotApplicable
AdamKorcz Mar 7, 2024
1cd85ea
change name of 'notAvailableOrNotApplicable'
AdamKorcz Mar 7, 2024
5836bbf
fix linter issues
AdamKorcz Mar 7, 2024
9899ea6
add comments to remediation fields
AdamKorcz Mar 7, 2024
ac57491
add check for nil-dereference
AdamKorcz Mar 7, 2024
d26a86a
remove the permissionLocation finding value
AdamKorcz Mar 11, 2024
a147631
rename checkAndLogNotAvailableOrNotApplicable to isBothUndeclaredAndN…
AdamKorcz Mar 11, 2024
91a4363
use raw metadata for remediation output
AdamKorcz Mar 21, 2024
c56806d
change 'branch' to 'defaultBranch'
AdamKorcz Mar 21, 2024
7138399
remove unused fields in rule Remediation
AdamKorcz Mar 21, 2024
c16e38d
fix remediation
AdamKorcz Mar 21, 2024
096a1b2
change 'metadata.defaultBranch' to 'metadata.repository.defaultBranch'
AdamKorcz Mar 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 303 additions & 0 deletions checks/evaluation/permissions.go
Original file line number Diff line number Diff line change
@@ -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)
}

Check warning on line 47 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L45-L47

Added lines #L45 - L47 were not covered by tests

// 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")
}

Check warning on line 94 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L93-L94

Added lines #L93 - L94 were not covered by tests

// 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

Check warning on line 105 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L105

Added line #L105 was not covered by tests
}
fPath := f.Location.Path
AdamKorcz marked this conversation as resolved.
Show resolved Hide resolved

addProbeToMaps(fPath, undeclaredPermissions, hasWritePermissions)

if f.Values["permissionLevel"] == string(checker.PermissionLevelUndeclared) {
score = updateScoreAndMapFromUndeclared(undeclaredPermissions,
hasWritePermissions, f, score, dl)
continue
}
spencerschrock marked this conversation as resolved.
Show resolved Hide resolved

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

Check warning on line 142 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L142

Added line #L142 was not covered by tests
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

Check warning on line 157 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L156-L157

Added lines #L156 - L157 were not covered by tests
}
// If project has not declared permissions at top level::
if undeclaredPermissions["topLevel"][fPath] {
score -= 0.5
}
default:
continue

Check warning on line 164 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L161-L164

Added lines #L161 - L164 were not covered by tests
}
}
if score < checker.MinResultScore {
score = checker.MinResultScore
}

Check warning on line 169 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L168-L169

Added lines #L168 - L169 were not covered by tests

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
}

Check warning on line 207 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L206-L207

Added lines #L206 - L207 were not covered by tests
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

Check warning on line 226 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L226

Added line #L226 was not covered by tests
} else if f.Outcome == finding.OutcomeNotApplicable {
dl.Debug(&checker.LogMessage{
Finding: f,
})
return false
}

Check warning on line 232 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L228-L232

Added lines #L228 - L232 were not covered by tests
}
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
}

Check warning on line 283 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L282-L283

Added lines #L282 - L283 were not covered by tests
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

Check warning on line 302 in checks/evaluation/permissions.go

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L302

Added line #L302 was not covered by tests
}

This file was deleted.

Loading
Loading