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

✨ Raw results for Packaging check #1913

Merged
merged 16 commits into from
Jun 1, 2022
21 changes: 21 additions & 0 deletions checker/raw_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
// is applied.
//nolint
type RawResults struct {
PackagingResults PackagingData
CIIBestPracticesResults CIIBestPracticesData
DangerousWorkflowResults DangerousWorkflowData
VulnerabilitiesResults VulnerabilitiesData
Expand All @@ -43,6 +44,26 @@ type FuzzingData struct {
Fuzzers []Tool
}

// TODO: Add Msg to all results.

// PackagingData contains results for the Packaging check.
type PackagingData struct {
Packages []Package
}

// Package represents a package.
// nolint
type Package struct {
// TODO: not supported yet. This needs to be unique across
// ecosystems: purl, OSV, CPE, etc.
Name *string
Job *WorkflowJob
File *File
// Note: Msg is populated only for debug messages.
Msg *string
Runs []Run
}

// MaintainedData contains the raw results
// for the Maintained check.
type MaintainedData struct {
Expand Down
89 changes: 89 additions & 0 deletions checks/evaluation/packaging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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"
)

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

pass := false
for _, p := range r.Packages {
if p.Msg != nil {
// This is a debug message. Let's just replay the message.
dl.Debug(&checker.LogMessage{
Text: *p.Msg,
})
continue
}

// Presence of a single non-debug message means the
// check passes.
pass = true

msg, err := createLogMessage(p)
if err != nil {
return checker.CreateRuntimeErrorResult(name, err)
}
dl.Info(&msg)
}

if pass {
return checker.CreateMaxScoreResult(name,
"publishing workflow detected")
}

dl.Warn(&checker.LogMessage{
Text: "no GitHub publishing workflow detected",
})

return checker.CreateInconclusiveResult(name,
"no published package detected")
}

func createLogMessage(p checker.Package) (checker.LogMessage, error) {
var msg checker.LogMessage

if p.Msg != nil {
return msg, sce.WithMessage(sce.ErrScorecardInternal, "Msg should be nil")
}

if p.File == nil {
return msg, sce.WithMessage(sce.ErrScorecardInternal, "File field is nil")
}

if p.File != nil {
msg.Path = p.File.Path
msg.Type = p.File.Type
msg.Offset = p.File.Offset
}

if len(p.Runs) == 0 {
return msg, sce.WithMessage(sce.ErrScorecardInternal, "no run data")
}

msg.Text = fmt.Sprintf("GitHub publishing workflow used in run %s", p.Runs[0].URL)

return msg, nil
}
38 changes: 38 additions & 0 deletions checks/fileparser/github_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,44 @@ type JobMatcherStep struct {
Run string
}

// JobMatchResult represents the result of a matche.
type JobMatchResult struct {
Msg string
File checker.File
}

// RawAnyJobsMatch returns true if any of the jobs have a match in the given workflow.
// TODO: Rename after migraiton is complete.
func RawAnyJobsMatch(workflow *actionlint.Workflow, jobMatchers []JobMatcher, fp string,
logMsgNoMatch string,
) (JobMatchResult, bool) {
for _, job := range workflow.Jobs {
for _, matcher := range jobMatchers {
if !matcher.matches(job) {
continue
}

return JobMatchResult{
File: checker.File{
Path: fp,
Type: checker.FileTypeSource,
Offset: GetLineNumber(job.Pos),
},
Msg: fmt.Sprintf("%v: %v", matcher.LogText, fp),
}, true
}
}

return JobMatchResult{
File: checker.File{
Path: fp,
Type: checker.FileTypeSource,
Offset: checker.OffsetDefault,
},
Msg: fmt.Sprintf("%v: %v", logMsgNoMatch, fp),
}, false
}

// AnyJobsMatch returns true if any of the jobs have a match in the given workflow.
func AnyJobsMatch(workflow *actionlint.Workflow, jobMatchers []JobMatcher, fp string, dl checker.DetailLogger,
logMsgNoMatch string,
Expand Down
183 changes: 8 additions & 175 deletions checks/packaging.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,9 @@
package checks

import (
"fmt"
"path/filepath"

"github.com/rhysd/actionlint"

"github.com/ossf/scorecard/v4/checker"
"github.com/ossf/scorecard/v4/checks/fileparser"
"github.com/ossf/scorecard/v4/checks/evaluation"
"github.com/ossf/scorecard/v4/checks/raw"
sce "github.com/ossf/scorecard/v4/errors"
)

Expand All @@ -38,179 +34,16 @@ func init() {

// Packaging runs Packaging check.
func Packaging(c *checker.CheckRequest) checker.CheckResult {
matchedFiles, err := c.RepoClient.ListFiles(fileparser.IsGithubWorkflowFileCb)
rawData, err := raw.Packaging(c)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("RepoClient.ListFiles: %v", err))
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
return checker.CreateRuntimeErrorResult(CheckPackaging, e)
}

for _, fp := range matchedFiles {
fc, err := c.RepoClient.GetFileContent(fp)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("RepoClient.GetFileContent: %v", err))
return checker.CreateRuntimeErrorResult(CheckPackaging, e)
}

workflow, errs := actionlint.Parse(fc)
if len(errs) > 0 && workflow == nil {
e := fileparser.FormatActionlintError(errs)
return checker.CreateRuntimeErrorResult(CheckPackaging, e)
}
if !isPackagingWorkflow(workflow, fp, c.Dlogger) {
continue
}

runs, err := c.RepoClient.ListSuccessfulWorkflowRuns(filepath.Base(fp))
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Client.Actions.ListWorkflowRunsByFileName: %v", err))
return checker.CreateRuntimeErrorResult(CheckPackaging, e)
}
if len(runs) > 0 {
c.Dlogger.Info(&checker.LogMessage{
Path: fp,
Type: checker.FileTypeSource,
Offset: checker.OffsetDefault,
Text: fmt.Sprintf("GitHub publishing workflow used in run %s", runs[0].URL),
})
return checker.CreateMaxScoreResult(CheckPackaging,
"publishing workflow detected")
}
c.Dlogger.Debug(&checker.LogMessage{
Path: fp,
Type: checker.FileTypeSource,
Offset: checker.OffsetDefault,
Text: "GitHub publishing workflow not used in runs",
})
}

c.Dlogger.Warn(&checker.LogMessage{
Text: "no GitHub publishing workflow detected",
})

return checker.CreateInconclusiveResult(CheckPackaging,
"no published package detected")
}

// A packaging workflow.
func isPackagingWorkflow(workflow *actionlint.Workflow, fp string, dl checker.DetailLogger) bool {
jobMatchers := []fileparser.JobMatcher{
{
Steps: []*fileparser.JobMatcherStep{
{
Uses: "actions/setup-node",
With: map[string]string{"registry-url": "https://registry.npmjs.org"},
},
{
Run: "npm.*publish",
},
},
LogText: "candidate node publishing workflow using npm",
},
{
// Java packages with maven.
Steps: []*fileparser.JobMatcherStep{
{
Uses: "actions/setup-java",
},
{
Run: "mvn.*deploy",
},
},
LogText: "candidate java publishing workflow using maven",
},
{
// Java packages with gradle.
Steps: []*fileparser.JobMatcherStep{
{
Uses: "actions/setup-java",
},
{
Run: "gradle.*publish",
},
},
LogText: "candidate java publishing workflow using gradle",
},
{
// Ruby packages.
Steps: []*fileparser.JobMatcherStep{
{
Run: "gem.*push",
},
},
LogText: "candidate ruby publishing workflow using gem",
},
{
// NuGet packages.
Steps: []*fileparser.JobMatcherStep{
{
Run: "nuget.*push",
},
},
LogText: "candidate nuget publishing workflow",
},
{
// Docker packages.
Steps: []*fileparser.JobMatcherStep{
{
Run: "docker.*push",
},
},
LogText: "candidate docker publishing workflow",
},
{
// Docker packages.
Steps: []*fileparser.JobMatcherStep{
{
Uses: "docker/build-push-action",
},
},
LogText: "candidate docker publishing workflow",
},
{
// Python packages.
Steps: []*fileparser.JobMatcherStep{
{
Uses: "actions/setup-python",
},
{
Uses: "pypa/gh-action-pypi-publish",
},
},
LogText: "candidate python publishing workflow using pypi",
},
{
// Python packages.
// This is a custom Python packaging workflow based on semantic versioning.
// TODO(#1642): accept custom workflows through a separate configuration.
Steps: []*fileparser.JobMatcherStep{
{
Uses: "relekang/python-semantic-release",
},
},
LogText: "candidate python publishing workflow using python-semantic-release",
},
{
// Go packages.
Steps: []*fileparser.JobMatcherStep{
{
Uses: "actions/setup-go",
},
{
Uses: "goreleaser/goreleaser-action",
},
},
LogText: "candidate golang publishing workflow",
},
{
// Rust packages. https://doc.rust-lang.org/cargo/reference/publishing.html
Steps: []*fileparser.JobMatcherStep{
{
Run: "cargo.*publish",
},
},
LogText: "candidate rust publishing workflow using cargo",
},
// Set the raw results.
if c.RawResults != nil {
c.RawResults.PackagingResults = rawData
}

return fileparser.AnyJobsMatch(workflow, jobMatchers, fp, dl, "not a publishing workflow")
return evaluation.Packaging(CheckPackaging, c.Dlogger, &rawData)
}
Loading