Skip to content

Commit

Permalink
Merge branch 'ossf:main' into singhsaurabh/contributing
Browse files Browse the repository at this point in the history
  • Loading branch information
singhsaurabh committed Jul 18, 2022
2 parents 57ba930 + f1b182a commit 29c0b09
Show file tree
Hide file tree
Showing 19 changed files with 1,218 additions and 954 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/scorecard-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b

- name: "Run analysis"
uses: ossf/scorecard-action@5c8bc69dc88b65c66584e07611df79d3579b0377
uses: ossf/scorecard-action@ce330fde6b1a5c9c75b417e7efc510b822a35564
with:
results_file: results.sarif
results_format: sarif
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ ARG TARGETOS
ARG TARGETARCH
RUN CGO_ENABLED=0 make build-scorecard

FROM gcr.io/distroless/base:nonroot@sha256:d65ac1a65a4d82a48ebd0a22aea2acdd95d7abeeda245dfee932ec0018c781f4
FROM gcr.io/distroless/base:nonroot@sha256:49d2923f35d66b8402487a7c01bc62a66d8279cd42e89c11b64cdce8d5826c03
COPY --from=build /src/scorecard /
ENTRYPOINT [ "/scorecard" ]
46 changes: 45 additions & 1 deletion checker/raw_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (

// RawResults contains results before a policy
// is applied.
//nolint
// nolint
type RawResults struct {
PackagingResults PackagingData
CIIBestPracticesResults CIIBestPracticesData
Expand All @@ -38,6 +38,7 @@ type RawResults struct {
SignedReleasesResults SignedReleasesData
FuzzingResults FuzzingData
LicenseResults LicenseData
TokenPermissionsResults TokenPermissionsData
}

// FuzzingData represents different fuzzing done.
Expand Down Expand Up @@ -243,3 +244,46 @@ type WorkflowJob struct {
Name *string
ID *string
}

// TokenPermissionsData represents data about a permission failure.
type TokenPermissionsData struct {
TokenPermissions []TokenPermission
}

// PermissionLocation represents a declaration type.
type PermissionLocation string

const (
// PermissionLocationTop is top-level workflow permission.
PermissionLocationTop PermissionLocation = "topLevel"
// PermissionLocationJob is job-level workflow permission.
PermissionLocationJob PermissionLocation = "jobLevel"
)

// PermissionLevel represents a permission type.
type PermissionLevel string

const (
// PermissionLevelUndeclared is an undecleared permission.
PermissionLevelUndeclared PermissionLevel = "undeclared"
// PermissionLevelWrite is a permission set to `write` for a permission we consider potentially dangerous.
PermissionLevelWrite PermissionLevel = "write"
// PermissionLevelRead is a permission set to `read`.
PermissionLevelRead PermissionLevel = "read"
// PermissionLevelNone is a permission set to `none`.
PermissionLevelNone PermissionLevel = "none"
// PermissionLevelUnknown is for other kinds of alerts, mostly to support debug messages.
// TODO: remove it once we have implemented severity (#1874).
PermissionLevelUnknown PermissionLevel = "unknown"
)

// TokenPermission defines a token permission result.
type TokenPermission struct {
Job *WorkflowJob
LocationType *PermissionLocation
Name *string
Value *string
File *File
Msg *string
Type PermissionLevel
}
1 change: 1 addition & 0 deletions checks/branch_protection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) {

rel1 := "release/v.1"
sha := "8fb3cb86082b17144a80402f5367ae65f06083bd"
//nolint:goconst
main := "main"
trueVal := true
falseVal := false
Expand Down
4 changes: 0 additions & 4 deletions checks/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ import (
)

var (
errInvalidGitHubWorkflow = errors.New("invalid GitHub workflow")
errInternalNameCannotBeEmpty = errors.New("name cannot be empty")
errInternalCheckFuncCannotBeNil = errors.New("checkFunc cannot be nil")
// TODO(#1245): these should be moved under `raw` package after migration.
errInvalidArgType = errors.New("invalid arg type")
errInvalidArgLength = errors.New("invalid arg length")
)
286 changes: 286 additions & 0 deletions checks/evaluation/permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
// Copyright 2021 Security 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/remediation"
)

type permissions struct {
topLevelWritePermissions map[string]bool
jobLevelWritePermissions map[string]bool
}

// TokenPermissions applies the score policy for the Token-Permissions check.
func TokenPermissions(name string, dl checker.DetailLogger, r *checker.TokenPermissionsData) checker.CheckResult {
if r == nil {
e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data")
return checker.CreateRuntimeErrorResult(name, e)
}

score, err := applyScorePolicy(r, dl)
if err != nil {
return checker.CreateRuntimeErrorResult(name, err)
}

if score != checker.MaxResultScore {
return checker.CreateResultWithScore(name,
"non read-only tokens detected in GitHub workflows", score)
}

return checker.CreateMaxScoreResult(name,
"tokens are read-only in GitHub workflows")
}

func applyScorePolicy(results *checker.TokenPermissionsData, dl checker.DetailLogger) (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)

for _, r := range results.TokenPermissions {
var msg checker.LogMessage

if r.File != nil {
msg.Path = r.File.Path
msg.Offset = r.File.Offset
msg.Type = r.File.Type
msg.Snippet = r.File.Snippet

if msg.Path != "" {
msg.Remediation = remediation.CreateWorkflowPermissionRemediation(r.File.Path)
}
}

text, err := createMessage(r)
if err != nil {
return checker.MinResultScore, err
}
msg.Text = text

switch r.Type {
case checker.PermissionLevelNone, checker.PermissionLevelRead:
dl.Info(&msg)
case checker.PermissionLevelUnknown:
dl.Debug(&msg)

case checker.PermissionLevelUndeclared:
if r.LocationType == nil {
return checker.InconclusiveResultScore,
sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil")
}

// We warn only for top-level.
if *r.LocationType == checker.PermissionLocationTop {
dl.Warn(&msg)
} else {
dl.Debug(&msg)
}

// Group results by workflow name for score computation.
if err := updateWorkflowHashMap(hm, r); err != nil {
return checker.InconclusiveResultScore, err
}

case checker.PermissionLevelWrite:
dl.Warn(&msg)

// Group results by workflow name for score computation.
if err := updateWorkflowHashMap(hm, r); err != nil {
return checker.InconclusiveResultScore, err
}
}
}

return calculateScore(hm), nil
}

func recordPermissionWrite(hm map[string]permissions, path string,
locType checker.PermissionLocation, permName *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 == checker.PermissionLocationTop {
m = hm[path].topLevelWritePermissions
}

// Set the permission name to record.
name := "all"
if permName != nil && *permName != "" {
name = *permName
}
m[name] = true
}

func updateWorkflowHashMap(hm map[string]permissions, t checker.TokenPermission) error {
if t.LocationType == nil {
return sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil")
}

if t.File == nil || t.File.Path == "" {
return sce.WithMessage(sce.ErrScorecardInternal, "path is not set")
}

if t.Type != checker.PermissionLevelWrite &&
t.Type != checker.PermissionLevelUndeclared {
return nil
}

recordPermissionWrite(hm, t.File.Path, *t.LocationType, t.Name)

return nil
}

func createMessage(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 message 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 permissionIsPresent(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 permissionIsPresent(perms, "checks") {
score -= 0.5
}

// secEvents.
// May allow attacker to read vuln reports before patch available.
// Low risk: -1
if permissionIsPresent(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 permissionIsPresent(perms, "deployments") {
score--
}

// contents.
// Allows attacker to commit unreviewed code.
// High risk: -10
if permissionIsPresent(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 permissionIsPresent(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 permissionIsPresent(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 permissionIsPresent(perms permissions, name string) bool {
return permissionIsPresentInTopLevel(perms, name) ||
permissionIsPresentInRunLevel(perms, name)
}

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
}

0 comments on commit 29c0b09

Please sign in to comment.