Skip to content

Commit 6c92a63

Browse files
authored
Merge branch 'main' into binary-artifact-gradle-exception
2 parents d96378f + f1b182a commit 6c92a63

19 files changed

+1218
-954
lines changed

.github/workflows/scorecard-analysis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
3131

3232
- name: "Run analysis"
33-
uses: ossf/scorecard-action@5c8bc69dc88b65c66584e07611df79d3579b0377
33+
uses: ossf/scorecard-action@ce330fde6b1a5c9c75b417e7efc510b822a35564
3434
with:
3535
results_file: results.sarif
3636
results_format: sarif

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ ARG TARGETOS
2424
ARG TARGETARCH
2525
RUN CGO_ENABLED=0 make build-scorecard
2626

27-
FROM gcr.io/distroless/base:nonroot@sha256:d65ac1a65a4d82a48ebd0a22aea2acdd95d7abeeda245dfee932ec0018c781f4
27+
FROM gcr.io/distroless/base:nonroot@sha256:49d2923f35d66b8402487a7c01bc62a66d8279cd42e89c11b64cdce8d5826c03
2828
COPY --from=build /src/scorecard /
2929
ENTRYPOINT [ "/scorecard" ]

checker/raw_result.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020

2121
// RawResults contains results before a policy
2222
// is applied.
23-
//nolint
23+
// nolint
2424
type RawResults struct {
2525
PackagingResults PackagingData
2626
CIIBestPracticesResults CIIBestPracticesData
@@ -38,6 +38,7 @@ type RawResults struct {
3838
SignedReleasesResults SignedReleasesData
3939
FuzzingResults FuzzingData
4040
LicenseResults LicenseData
41+
TokenPermissionsResults TokenPermissionsData
4142
}
4243

4344
// FuzzingData represents different fuzzing done.
@@ -243,3 +244,46 @@ type WorkflowJob struct {
243244
Name *string
244245
ID *string
245246
}
247+
248+
// TokenPermissionsData represents data about a permission failure.
249+
type TokenPermissionsData struct {
250+
TokenPermissions []TokenPermission
251+
}
252+
253+
// PermissionLocation represents a declaration type.
254+
type PermissionLocation string
255+
256+
const (
257+
// PermissionLocationTop is top-level workflow permission.
258+
PermissionLocationTop PermissionLocation = "topLevel"
259+
// PermissionLocationJob is job-level workflow permission.
260+
PermissionLocationJob PermissionLocation = "jobLevel"
261+
)
262+
263+
// PermissionLevel represents a permission type.
264+
type PermissionLevel string
265+
266+
const (
267+
// PermissionLevelUndeclared is an undecleared permission.
268+
PermissionLevelUndeclared PermissionLevel = "undeclared"
269+
// PermissionLevelWrite is a permission set to `write` for a permission we consider potentially dangerous.
270+
PermissionLevelWrite PermissionLevel = "write"
271+
// PermissionLevelRead is a permission set to `read`.
272+
PermissionLevelRead PermissionLevel = "read"
273+
// PermissionLevelNone is a permission set to `none`.
274+
PermissionLevelNone PermissionLevel = "none"
275+
// PermissionLevelUnknown is for other kinds of alerts, mostly to support debug messages.
276+
// TODO: remove it once we have implemented severity (#1874).
277+
PermissionLevelUnknown PermissionLevel = "unknown"
278+
)
279+
280+
// TokenPermission defines a token permission result.
281+
type TokenPermission struct {
282+
Job *WorkflowJob
283+
LocationType *PermissionLocation
284+
Name *string
285+
Value *string
286+
File *File
287+
Msg *string
288+
Type PermissionLevel
289+
}

checks/branch_protection_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func TestReleaseAndDevBranchProtected(t *testing.T) {
5757

5858
rel1 := "release/v.1"
5959
sha := "8fb3cb86082b17144a80402f5367ae65f06083bd"
60+
//nolint:goconst
6061
main := "main"
6162
trueVal := true
6263
falseVal := false

checks/errors.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ import (
1919
)
2020

2121
var (
22-
errInvalidGitHubWorkflow = errors.New("invalid GitHub workflow")
2322
errInternalNameCannotBeEmpty = errors.New("name cannot be empty")
2423
errInternalCheckFuncCannotBeNil = errors.New("checkFunc cannot be nil")
25-
// TODO(#1245): these should be moved under `raw` package after migration.
26-
errInvalidArgType = errors.New("invalid arg type")
27-
errInvalidArgLength = errors.New("invalid arg length")
2824
)

checks/evaluation/permissions.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
// Copyright 2021 Security Scorecard Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package evaluation
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/ossf/scorecard/v4/checker"
21+
sce "github.com/ossf/scorecard/v4/errors"
22+
"github.com/ossf/scorecard/v4/remediation"
23+
)
24+
25+
type permissions struct {
26+
topLevelWritePermissions map[string]bool
27+
jobLevelWritePermissions map[string]bool
28+
}
29+
30+
// TokenPermissions applies the score policy for the Token-Permissions check.
31+
func TokenPermissions(name string, dl checker.DetailLogger, r *checker.TokenPermissionsData) checker.CheckResult {
32+
if r == nil {
33+
e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data")
34+
return checker.CreateRuntimeErrorResult(name, e)
35+
}
36+
37+
score, err := applyScorePolicy(r, dl)
38+
if err != nil {
39+
return checker.CreateRuntimeErrorResult(name, err)
40+
}
41+
42+
if score != checker.MaxResultScore {
43+
return checker.CreateResultWithScore(name,
44+
"non read-only tokens detected in GitHub workflows", score)
45+
}
46+
47+
return checker.CreateMaxScoreResult(name,
48+
"tokens are read-only in GitHub workflows")
49+
}
50+
51+
func applyScorePolicy(results *checker.TokenPermissionsData, dl checker.DetailLogger) (int, error) {
52+
// See list https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/.
53+
// Note: there are legitimate reasons to use some of the permissions like checks, deployments, etc.
54+
// in CI/CD systems https://docs.travis-ci.com/user/github-oauth-scopes/.
55+
56+
hm := make(map[string]permissions)
57+
58+
for _, r := range results.TokenPermissions {
59+
var msg checker.LogMessage
60+
61+
if r.File != nil {
62+
msg.Path = r.File.Path
63+
msg.Offset = r.File.Offset
64+
msg.Type = r.File.Type
65+
msg.Snippet = r.File.Snippet
66+
67+
if msg.Path != "" {
68+
msg.Remediation = remediation.CreateWorkflowPermissionRemediation(r.File.Path)
69+
}
70+
}
71+
72+
text, err := createMessage(r)
73+
if err != nil {
74+
return checker.MinResultScore, err
75+
}
76+
msg.Text = text
77+
78+
switch r.Type {
79+
case checker.PermissionLevelNone, checker.PermissionLevelRead:
80+
dl.Info(&msg)
81+
case checker.PermissionLevelUnknown:
82+
dl.Debug(&msg)
83+
84+
case checker.PermissionLevelUndeclared:
85+
if r.LocationType == nil {
86+
return checker.InconclusiveResultScore,
87+
sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil")
88+
}
89+
90+
// We warn only for top-level.
91+
if *r.LocationType == checker.PermissionLocationTop {
92+
dl.Warn(&msg)
93+
} else {
94+
dl.Debug(&msg)
95+
}
96+
97+
// Group results by workflow name for score computation.
98+
if err := updateWorkflowHashMap(hm, r); err != nil {
99+
return checker.InconclusiveResultScore, err
100+
}
101+
102+
case checker.PermissionLevelWrite:
103+
dl.Warn(&msg)
104+
105+
// Group results by workflow name for score computation.
106+
if err := updateWorkflowHashMap(hm, r); err != nil {
107+
return checker.InconclusiveResultScore, err
108+
}
109+
}
110+
}
111+
112+
return calculateScore(hm), nil
113+
}
114+
115+
func recordPermissionWrite(hm map[string]permissions, path string,
116+
locType checker.PermissionLocation, permName *string,
117+
) {
118+
if _, exists := hm[path]; !exists {
119+
hm[path] = permissions{
120+
topLevelWritePermissions: make(map[string]bool),
121+
jobLevelWritePermissions: make(map[string]bool),
122+
}
123+
}
124+
125+
// Select the hash map to update.
126+
m := hm[path].jobLevelWritePermissions
127+
if locType == checker.PermissionLocationTop {
128+
m = hm[path].topLevelWritePermissions
129+
}
130+
131+
// Set the permission name to record.
132+
name := "all"
133+
if permName != nil && *permName != "" {
134+
name = *permName
135+
}
136+
m[name] = true
137+
}
138+
139+
func updateWorkflowHashMap(hm map[string]permissions, t checker.TokenPermission) error {
140+
if t.LocationType == nil {
141+
return sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil")
142+
}
143+
144+
if t.File == nil || t.File.Path == "" {
145+
return sce.WithMessage(sce.ErrScorecardInternal, "path is not set")
146+
}
147+
148+
if t.Type != checker.PermissionLevelWrite &&
149+
t.Type != checker.PermissionLevelUndeclared {
150+
return nil
151+
}
152+
153+
recordPermissionWrite(hm, t.File.Path, *t.LocationType, t.Name)
154+
155+
return nil
156+
}
157+
158+
func createMessage(t checker.TokenPermission) (string, error) {
159+
// By default, use the message already present.
160+
if t.Msg != nil {
161+
return *t.Msg, nil
162+
}
163+
164+
// Ensure there's no implementation bug.
165+
if t.LocationType == nil {
166+
return "", sce.WithMessage(sce.ErrScorecardInternal, "locationType is nil")
167+
}
168+
169+
// Use a different message depending on the type.
170+
if t.Type == checker.PermissionLevelUndeclared {
171+
return fmt.Sprintf("no %s permission defined", *t.LocationType), nil
172+
}
173+
174+
if t.Value == nil {
175+
return "", sce.WithMessage(sce.ErrScorecardInternal, "Value fields is nil")
176+
}
177+
178+
if t.Name == nil {
179+
return fmt.Sprintf("%s permissions set to '%v'", *t.LocationType,
180+
*t.Value), nil
181+
}
182+
183+
return fmt.Sprintf("%s '%v' permission set to '%v'", *t.LocationType,
184+
*t.Name, *t.Value), nil
185+
}
186+
187+
// Calculate the score.
188+
func calculateScore(result map[string]permissions) int {
189+
// See list https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/.
190+
// Note: there are legitimate reasons to use some of the permissions like checks, deployments, etc.
191+
// in CI/CD systems https://docs.travis-ci.com/user/github-oauth-scopes/.
192+
193+
// Start with a perfect score.
194+
score := float32(checker.MaxResultScore)
195+
196+
// Retrieve the overall results.
197+
for _, perms := range result {
198+
// If no top level permissions are defined, all the permissions
199+
// are enabled by default. In this case,
200+
if permissionIsPresentInTopLevel(perms, "all") {
201+
if permissionIsPresentInRunLevel(perms, "all") {
202+
// ... give lowest score if no run level permissions are defined either.
203+
return checker.MinResultScore
204+
}
205+
// ... reduce score if run level permissions are defined.
206+
score -= 0.5
207+
}
208+
209+
// status: https://docs.github.com/en/rest/reference/repos#statuses.
210+
// May allow an attacker to change the result of pre-submit and get a PR merged.
211+
// Low risk: -0.5.
212+
if permissionIsPresent(perms, "statuses") {
213+
score -= 0.5
214+
}
215+
216+
// checks.
217+
// May allow an attacker to edit checks to remove pre-submit and introduce a bug.
218+
// Low risk: -0.5.
219+
if permissionIsPresent(perms, "checks") {
220+
score -= 0.5
221+
}
222+
223+
// secEvents.
224+
// May allow attacker to read vuln reports before patch available.
225+
// Low risk: -1
226+
if permissionIsPresent(perms, "security-events") {
227+
score--
228+
}
229+
230+
// deployments: https://docs.github.com/en/rest/reference/repos#deployments.
231+
// May allow attacker to charge repo owner by triggering VM runs,
232+
// and tiny chance an attacker can trigger a remote
233+
// service with code they own if server accepts code/location var unsanitized.
234+
// Low risk: -1
235+
if permissionIsPresent(perms, "deployments") {
236+
score--
237+
}
238+
239+
// contents.
240+
// Allows attacker to commit unreviewed code.
241+
// High risk: -10
242+
if permissionIsPresent(perms, "contents") {
243+
score -= checker.MaxResultScore
244+
}
245+
246+
// packages: https://docs.github.com/en/packages/learn-github-packages/about-permissions-for-github-packages.
247+
// Allows attacker to publish packages.
248+
// High risk: -10
249+
if permissionIsPresent(perms, "packages") {
250+
score -= checker.MaxResultScore
251+
}
252+
253+
// actions.
254+
// May allow an attacker to steal GitHub secrets by approving to run an action that needs approval.
255+
// High risk: -10
256+
if permissionIsPresent(perms, "actions") {
257+
score -= checker.MaxResultScore
258+
}
259+
260+
if score < checker.MinResultScore {
261+
break
262+
}
263+
}
264+
265+
// We're done, calculate the final score.
266+
if score < checker.MinResultScore {
267+
return checker.MinResultScore
268+
}
269+
270+
return int(score)
271+
}
272+
273+
func permissionIsPresent(perms permissions, name string) bool {
274+
return permissionIsPresentInTopLevel(perms, name) ||
275+
permissionIsPresentInRunLevel(perms, name)
276+
}
277+
278+
func permissionIsPresentInTopLevel(perms permissions, name string) bool {
279+
_, ok := perms.topLevelWritePermissions[name]
280+
return ok
281+
}
282+
283+
func permissionIsPresentInRunLevel(perms permissions, name string) bool {
284+
_, ok := perms.jobLevelWritePermissions[name]
285+
return ok
286+
}

0 commit comments

Comments
 (0)