Skip to content

Commit

Permalink
🌱 migrate token permission check to probes
Browse files Browse the repository at this point in the history
Signed-off-by: Adam Korczynski <[email protected]>
  • Loading branch information
AdamKorcz committed Jan 22, 2024
1 parent e41a3fe commit 07539fd
Show file tree
Hide file tree
Showing 70 changed files with 3,644 additions and 610 deletions.
1 change: 1 addition & 0 deletions checker/raw_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ const (
type TokenPermission struct {
Job *WorkflowJob
LocationType *PermissionLocation
Remediation *rule.Remediation
Name *string
Value *string
File *File
Expand Down
315 changes: 315 additions & 0 deletions checks/evaluation/permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
// Copyright 2021 OpenSSF Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package evaluation

import (
"fmt"

"github.com/ossf/scorecard/v4/checker"
sce "github.com/ossf/scorecard/v4/errors"
"github.com/ossf/scorecard/v4/finding"
"github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionNone"
"github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionRead"
"github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionUndeclared"
"github.com/ossf/scorecard/v4/probes/hasGitHubWorkflowPermissionUnknown"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteActionsJob"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteActionsTop"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteAllJob"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteAllTop"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteChecksJob"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteChecksTop"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteContentsJob"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteContentsTop"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsJob"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteDeploymentsTop"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWritePackagesJob"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWritePackagesTop"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsJob"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteSecurityEventsTop"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteStatusesJob"
"github.com/ossf/scorecard/v4/probes/hasNoGitHubWorkflowPermissionWriteStatusesTop"
)

// TokenPermissions applies the score policy for the Token-Permissions check.
func TokenPermissions(name string,
findings []finding.Finding,
dl checker.DetailLogger,
) checker.CheckResult {
expectedProbes := []string{
hasNoGitHubWorkflowPermissionWriteActionsTop.Probe,
hasNoGitHubWorkflowPermissionWriteAllTop.Probe,
hasNoGitHubWorkflowPermissionWriteChecksTop.Probe,
hasNoGitHubWorkflowPermissionWriteContentsTop.Probe,
hasNoGitHubWorkflowPermissionWriteDeploymentsTop.Probe,
hasNoGitHubWorkflowPermissionWritePackagesTop.Probe,
hasNoGitHubWorkflowPermissionWriteSecurityEventsTop.Probe,
hasNoGitHubWorkflowPermissionWriteStatusesTop.Probe,
hasGitHubWorkflowPermissionUnknown.Probe,
hasGitHubWorkflowPermissionNone.Probe,
hasGitHubWorkflowPermissionRead.Probe,
hasGitHubWorkflowPermissionUndeclared.Probe,
hasNoGitHubWorkflowPermissionWriteAllJob.Probe,
hasNoGitHubWorkflowPermissionWriteSecurityEventsJob.Probe,
hasNoGitHubWorkflowPermissionWriteActionsJob.Probe,
hasNoGitHubWorkflowPermissionWriteContentsJob.Probe,
hasNoGitHubWorkflowPermissionWritePackagesJob.Probe,
hasNoGitHubWorkflowPermissionWriteChecksJob.Probe,
hasNoGitHubWorkflowPermissionWriteDeploymentsJob.Probe,
hasNoGitHubWorkflowPermissionWriteStatusesJob.Probe,
}
if !finding.UniqueProbesEqual(findings, expectedProbes) {
e := sce.WithMessage(sce.ErrScorecardInternal, "invalid probe results")
return checker.CreateRuntimeErrorResult(name, e)
}

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

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L73-L75

Added lines #L73 - L75 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" and "read" permissions.
if f.Outcome == finding.OutcomePositive &&
(f.Probe == hasGitHubWorkflowPermissionNone.Probe ||
f.Probe == hasGitHubWorkflowPermissionRead.Probe) {
dl.Info(&checker.LogMessage{
Finding: f,
})
}

if notAvailableOrNotApplicable(f, dl) {
return checker.CreateInconclusiveResult(name, "Token permissions are not available")
}

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

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L115-L116

Added lines #L115 - L116 were not covered by tests

// If there are no TokenPermissions
if f.Outcome == finding.OutcomeNotAvailable {
return checker.CreateInconclusiveResult(name, "No tokens found")
}

if f.Outcome != finding.OutcomeNegative {
continue
}
fPath := f.Location.Path

addProbeToMaps(fPath, undeclaredPermissions, hasWritePermissions)

switch f.Probe {
case hasGitHubWorkflowPermissionUndeclared.Probe:
score = updateScoreAndMapFromUndeclared(undeclaredPermissions,
hasWritePermissions, f, score, dl)
case hasNoGitHubWorkflowPermissionWriteAllTop.Probe:
// If no top level permissions are defined, all the permissions
// are enabled by default.
dl.Warn(&checker.LogMessage{
Finding: f,
})
hasWritePermissions["topLevel"][fPath] = true

if hasWritePermissions["jobLevel"][fPath] ||
undeclaredPermissions["jobLevel"][fPath] {
return checker.CreateMinScoreResult(name, "detected GitHub workflow tokens with excessive permissions")
}

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

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L144-L145

Added lines #L144 - L145 were not covered by tests
score -= 0.5
case hasNoGitHubWorkflowPermissionWriteAllJob.Probe:
dl.Warn(&checker.LogMessage{
Finding: f,
})
hasWritePermissions["jobLevel"][fPath] = true
if hasWritePermissions["topLevel"][fPath] {
score = checker.MinResultScore
break

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

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L147-L154

Added lines #L147 - L154 were not covered by tests
}
if undeclaredPermissions["topLevel"][fPath] {
score -= 0.5
}

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

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L156-L158

Added lines #L156 - L158 were not covered by tests
case hasNoGitHubWorkflowPermissionWriteStatusesJob.Probe,
hasNoGitHubWorkflowPermissionWriteDeploymentsJob.Probe,
hasNoGitHubWorkflowPermissionWriteSecurityEventsJob.Probe,
hasNoGitHubWorkflowPermissionWriteActionsJob.Probe,
hasNoGitHubWorkflowPermissionWriteContentsJob.Probe,
hasNoGitHubWorkflowPermissionWritePackagesJob.Probe,
hasNoGitHubWorkflowPermissionWriteChecksJob.Probe:
dl.Warn(&checker.LogMessage{
Finding: f,
})
hasWritePermissions["jobLevel"][fPath] = true
default:
score = logAndReduceScore(f, dl, score)
}
}
if score < checker.MinResultScore {
score = checker.MinResultScore
}

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

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L175-L176

Added lines #L175 - L176 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
}
return score
}

func updateScoreFromUndeclaredTop(undeclaredPermissions map[string]map[string]bool,
fPath string,
score float32,
) float32 {
if undeclaredPermissions["jobLevel"][fPath] {
score = checker.MinResultScore

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

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L223

Added line #L223 was not covered by tests
} else {
score -= 0.5
}
return score
}

func notAvailableOrNotApplicable(f *finding.Finding, dl checker.DetailLogger) bool {
if f.Probe == hasGitHubWorkflowPermissionUndeclared.Probe {
if f.Outcome == finding.OutcomeNotAvailable {
return true

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

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L233

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

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

View check run for this annotation

Codecov / codecov/patch

checks/evaluation/permissions.go#L235-L239

Added lines #L235 - L239 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.Values["jobLevel"] == 1 {
dl.Debug(&checker.LogMessage{
Finding: f,
})
undeclaredPermissions["jobLevel"][fPath] = true
score = updateScoreFromUndeclaredJob(undeclaredPermissions,
hasWritePermissions,
fPath,
score)
} else if f.Values["topLevel"] == 1 {
dl.Warn(&checker.LogMessage{
Finding: f,
})
undeclaredPermissions["topLevel"][fPath] = true
score = updateScoreFromUndeclaredTop(undeclaredPermissions,
fPath,
score)
}
return score
}

func addProbeToMaps(fPath string, hasWritePermissions, undeclaredPermissions map[string]map[string]bool) {
if _, ok := undeclaredPermissions["jobLevel"][fPath]; !ok {
undeclaredPermissions["jobLevel"][fPath] = false
}
if _, ok := undeclaredPermissions["topLevel"][fPath]; !ok {
undeclaredPermissions["topLevel"][fPath] = false
}
if _, ok := hasWritePermissions["jobLevel"][fPath]; !ok {
hasWritePermissions["jobLevel"][fPath] = false
}
if _, ok := hasWritePermissions["topLevel"][fPath]; !ok {
hasWritePermissions["topLevel"][fPath] = false
}
}

// Some probes with negative outcomes triggers logging and simple reduce in score.
// logAndReduceScore represents these cases.
func logAndReduceScore(f *finding.Finding, dl checker.DetailLogger, score float32) float32 {
switch f.Probe {
case hasGitHubWorkflowPermissionUnknown.Probe:
dl.Debug(&checker.LogMessage{
Finding: f,
})
case hasNoGitHubWorkflowPermissionWriteChecksTop.Probe,
hasNoGitHubWorkflowPermissionWriteStatusesTop.Probe:
dl.Warn(&checker.LogMessage{
Finding: f,
})
score -= 0.5
case hasNoGitHubWorkflowPermissionWriteContentsTop.Probe,
hasNoGitHubWorkflowPermissionWritePackagesTop.Probe,
hasNoGitHubWorkflowPermissionWriteActionsTop.Probe:
dl.Warn(&checker.LogMessage{
Finding: f,
})
score -= checker.MaxResultScore
case hasNoGitHubWorkflowPermissionWriteDeploymentsTop.Probe,
hasNoGitHubWorkflowPermissionWriteSecurityEventsTop.Probe:
dl.Warn(&checker.LogMessage{
Finding: f,
})
score--
}
return score
}

This file was deleted.

Loading

0 comments on commit 07539fd

Please sign in to comment.