check for read-only permissions of github token (#534)

* check for read-only permissions of github token

* linter

* linter

* doc

* comments

* commments

* fix

* generate checks.mg

* update license

* linter

* comments

* license

* linter

* missing file

* linter

* license

* cleanup
This commit is contained in:
laurentsimon 2021-06-03 16:30:37 -07:00 committed by GitHub
parent 93a47e678d
commit 37d979f79b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 533 additions and 5 deletions

View File

@ -57,7 +57,7 @@ This check tries to determine if a project has a set of contributors from multip
## Frozen-Deps
This check tries to determine if a project has declared and pinned its dependencies. It works by (1) looking for the following files in the root directory: go.mod, go.sum (Golang), package-lock.json, npm-shrinkwrap.json (Javascript), requirements.txt, pipfile.lock (Python), gemfile.lock (Ruby), cargo.lock (Rust), yarn.lock (package manager), composer.lock (PHP), vendor/, third_party/, third-party/; (2) look for github actions under .github/workflows/ and verifies they are pinned by hash. If one of the files in (1) AND all the dependencies in (2) are pinned, the check succeds. This check does not currently look for docker image pinning and shell script pinning.
This check tries to determine if a project has declared and pinned its dependencies. It works by (1) looking for the following files in the root directory: go.mod, go.sum (Golang), package-lock.json, npm-shrinkwrap.json (Javascript), requirements.txt, pipfile.lock (Python), gemfile.lock (Ruby), cargo.lock (Rust), yarn.lock (package manager), composer.lock (PHP), vendor/, third_party/, third-party/; (2) look for github actions under .github/workflows/ and verifies they are pinned by hash; (3) looks for Dockerfiles and verifies FROM dependencies are pinned by hash. If one of the files in (1) AND all the dependencies in (2),(3) are pinned, the check succeds. This check does not currently look for shell script pinning (curl | bash) in scripts, Dockerfiles, Makefiles or system()-like code.
**Remediation steps**
- Declare all your dependencies with specific versions in your package format file (e.g. `package.json` for npm, `requirements.txt` for python). For C/C++, check in the code from a trusted source and add a `README` on the specific version used (and the archive SHA hashes).
@ -117,3 +117,17 @@ This check looks for cryptographically signed tags in the last 5 tags. The check
- Publish the tag and then sign it with this key.
- For GitHub, check out the steps [here](https://docs.github.com/en/github/authenticating-to-github/signing-tags#further-reading).
## Token-Permissions
This check tries to determine if a project's GitHub workflows follow the principle of least privilege, i.e. if the GitHub tokens are set read-only by default. The check currently checks that the 'permission' keyword is used and set to read/none for the 'contents' permission for every workflow yaml file. If other permissions are set globally for the entire file, this check fails. Otherwise it succeeds.
**Remediation steps**
- Use: ``` permissions:
contents: read
``` in all your .yaml files.
- If you need more permissions, declare them in the job itself, e.g. ``` jobs: create_commit:
runs-on: ubuntu-latest
permissions:
issues: write
```

View File

@ -15,6 +15,31 @@
# This is the source of truth for all check descriptions and remediation steps.
# Run `cd checks/main && go run /main` to generate `checks.json` and `checks.md`.
checks:
Token-Permissions:
description: >-
This check tries to determine if a project's GitHub workflows
follow the principle of least privilege, i.e. if the GitHub tokens
are set read-only by default. The check currently checks that the 'permission'
keyword is used and set to read/none for the 'contents' permission for every workflow
yaml file. If other permissions are set globally for the entire file, this check fails.
Otherwise it succeeds.
remediation:
- >-
Use:
```
permissions:
contents: read
```
in all your .yaml files.
- >-
If you need more permissions, declare them in the job itself, e.g.
```
jobs:
create_commit:
runs-on: ubuntu-latest
permissions:
issues: write
```
Security-Policy:
description: >-
This check tries to determine if a project has published a security
@ -50,10 +75,11 @@ checks:
(Javascript), requirements.txt, pipfile.lock (Python), gemfile.lock
(Ruby), cargo.lock (Rust), yarn.lock (package manager), composer.lock
(PHP), vendor/, third_party/, third-party/; (2) look for github actions
under .github/workflows/ and verifies they are pinned by hash. If one of
the files in (1) AND all the dependencies in (2) are pinned, the check
succeds. This check does not currently look for docker image pinning and
shell script pinning.
under .github/workflows/ and verifies they are pinned by hash; (3) looks
for Dockerfiles and verifies FROM dependencies are pinned by hash. If one of
the files in (1) AND all the dependencies in (2),(3) are pinned, the check
succeds. This check does not currently look for shell script pinning (curl | bash)
in scripts, Dockerfiles, Makefiles or system()-like code.
remediation:
- >-
Declare all your dependencies with specific versions in your package

View File

@ -73,6 +73,7 @@ func TestGithubWorkflowPinning(t *testing.T) {
},
},
}
//nolint
for _, tt := range tests {
tt := tt // Re-initializing variable so it is not changed while executing the closure below
l.messages = []string{}

166
checks/permissions.go Normal file
View File

@ -0,0 +1,166 @@
// 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 checks
import (
"errors"
"fmt"
"gopkg.in/yaml.v2"
"github.com/ossf/scorecard/checker"
)
const CheckPermissions = "Token-Permissions"
// ErrInvalidGitHubWorkflowFile : Invalid GitHub workflow file.
var ErrInvalidGitHubWorkflowFile = errors.New("invalid GitHub workflow file")
//nolint:gochecknoinits
func init() {
registerCheck(CheckPermissions, leastPrivilegedTokens)
}
func leastPrivilegedTokens(c *checker.CheckRequest) checker.CheckResult {
return CheckFilesContent(CheckPermissions, ".github/workflows/*", true, c, validateGitHubActionTokenPermissions)
}
func validatePermission(key string, value interface{}, path string,
logf func(s string, f ...interface{})) (bool, error) {
switch val := value.(type) {
case string:
if val == "write" {
logf("!! token-permissions/github-token - %v permission set to '%v' in %v", key, val, path)
return false, nil
}
default:
return false, ErrInvalidGitHubWorkflowFile
}
return true, nil
}
func validateMapPermissions(values map[interface{}]interface{}, path string,
logf func(s string, f ...interface{})) (bool, error) {
permissionRead := true
var r bool
var err error
// Iterate over the permission, verify keys and values are strings.
for k, v := range values {
switch key := k.(type) {
// String type.
case string:
if r, err = validatePermission(key, v, path, logf); err != nil {
return false, err
}
if !r {
permissionRead = false
}
// Invalid type.
default:
return false, ErrInvalidGitHubWorkflowFile
}
}
return permissionRead, nil
}
func validateReadPermissions(config map[interface{}]interface{}, path string,
logf func(s string, f ...interface{})) (bool, error) {
permissionFound := false
permissionRead := true
var err error
// Iterate over the values.
for key, value := range config {
if key != "permissions" {
continue
}
// We have found the permissions keyword.
permissionFound = true
// Check the type of our values.
switch val := value.(type) {
// Empty string is nil type.
// It defaults to 'none'
case nil:
// String type.
case string:
if val != "read-all" && val != "" {
logf("!! token-permissions/github-token - permission set to '%v' in %v", val, path)
return false, nil
}
// Map type.
case map[interface{}]interface{}:
var res bool
if res, err = validateMapPermissions(val, path, logf); err != nil {
return false, err
}
if !res {
permissionRead = false
}
// Invalid type.
default:
return false, ErrInvalidGitHubWorkflowFile
}
}
// Did we find a permission at all?
if !permissionFound {
logf("!! token-permissions/github-token - no permission defined in %v", path)
return false, nil
}
return permissionRead, nil
}
// Check file content.
func validateGitHubActionTokenPermissions(path string, content []byte,
logf func(s string, f ...interface{})) (bool, error) {
if len(content) == 0 {
return false, ErrEmptyFile
}
var workflow map[interface{}]interface{}
var r bool
var err error
err = yaml.Unmarshal(content, &workflow)
if err != nil {
return false, fmt.Errorf("!! token-permissions - cannot unmarshal file %v\n%v\n%v: %w",
path, content, string(content), err)
}
// 1. Check that each file uses 'content: read' only or 'none'.
//nolint
// https://docs.github.com/en/actions/reference/authentication-in-a-workflow#example-1-passing-the-github_token-as-an-input,
// https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/,
// https://docs.github.com/en/actions/reference/authentication-in-a-workflow#modifying-the-permissions-for-the-github_token.
if r, err = validateReadPermissions(workflow, path, logf); err != nil {
return false, nil
}
if !r {
return r, nil
}
// TODO(laurent): 2. Identify github actions that require write and add checks.
// TODO(laurent): 3. Read a few runs and ensures they have the same permissions.
return true, nil
}

145
checks/permissions_test.go Normal file
View File

@ -0,0 +1,145 @@
// 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 checks
import (
"errors"
"fmt"
"io/ioutil"
"testing"
)
func TestGithubTokenPermissions(t *testing.T) {
t.Parallel()
type args struct {
Logf func(s string, f ...interface{})
Filename string
}
type returnValue struct {
Error error
Result bool
}
l := log{}
tests := []struct {
args args
want returnValue
name string
}{
{
name: "Write all test",
args: args{
Filename: "./testdata/github-workflow-permissions-writeall.yaml",
Logf: l.Logf,
},
want: returnValue{
Error: nil,
Result: false,
},
},
{
name: "Read all test",
args: args{
Filename: "./testdata/github-workflow-permissions-readall.yaml",
Logf: l.Logf,
},
want: returnValue{
Error: nil,
Result: true,
},
},
{
name: "No permission test",
args: args{
Filename: "./testdata/github-workflow-permissions-absent.yaml",
Logf: l.Logf,
},
want: returnValue{
Error: nil,
Result: false,
},
},
{
name: "Writes test",
args: args{
Filename: "./testdata/github-workflow-permissions-writes.yaml",
Logf: l.Logf,
},
want: returnValue{
Error: nil,
Result: false,
},
},
{
name: "Reads test",
args: args{
Filename: "./testdata/github-workflow-permissions-reads.yaml",
Logf: l.Logf,
},
want: returnValue{
Error: nil,
Result: true,
},
},
{
name: "Nones test",
args: args{
Filename: "./testdata/github-workflow-permissions-nones.yaml",
Logf: l.Logf,
},
want: returnValue{
Error: nil,
Result: true,
},
},
{
name: "None test",
args: args{
Filename: "./testdata/github-workflow-permissions-none.yaml",
Logf: l.Logf,
},
want: returnValue{
Error: nil,
Result: true,
},
},
}
//nolint
for _, tt := range tests {
tt := tt // Re-initializing variable so it is not changed while executing the closure below
l.messages = []string{}
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var content []byte
var err error
if tt.args.Filename == "" {
content = make([]byte, 0)
} else {
content, err = ioutil.ReadFile(tt.args.Filename)
if err != nil {
panic(fmt.Errorf("cannot read file: %w", err))
}
}
r, err := validateGitHubActionTokenPermissions(tt.args.Filename, content, tt.args.Logf)
if !errors.Is(err, tt.want.Error) ||
r != tt.want.Result {
t.Errorf("TestGithubTokenPermissions:\"%v\": %v (%v,%v) want (%v, %v)",
tt.name, tt.args.Filename, r, err, tt.want.Result, tt.want.Error)
}
})
}
}

View File

@ -0,0 +1,21 @@
# 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.
name: absent workflow
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "absent workflow"

View File

@ -0,0 +1,23 @@
# 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.
name: none workflow
on: [push]
permissions:
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "none workflow"

View File

@ -0,0 +1,32 @@
# 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.
name: writes workflow
on: [push]
permissions:
actions: none
checks: none
contents: none
deployments: none
issues: none
packages: none
pull-requests: none
repository-projects: none
security-events: none
statuses: none
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "none workflow"

View File

@ -0,0 +1,22 @@
# 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.
name: read all workflow
on: [push]
permissions: read-all
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "read all workflow"

View File

@ -0,0 +1,32 @@
# 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.
name: reads workflow
on: [push]
permissions:
actions: read
checks: read
contents: read
deployments: read
issues: read
packages: read
pull-requests: read
repository-projects: read
security-events: read
statuses: read
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "reads workflow"

View File

@ -0,0 +1,22 @@
# 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.
name: write all workflow
on: [push]
permissions: write-all
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "write all workflow"

View File

@ -0,0 +1,24 @@
# 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.
name: write-and-read workflow
on: [push]
permissions:
contents: read
pull-requests: write
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "write-and-read workflow"