[RAW]: dependency update tool (#1391)

* dependency update tool

* rename

* missing files

* add fields

* rm field
This commit is contained in:
laurentsimon 2021-12-15 09:02:31 -08:00 committed by GitHub
parent cef72f0f7d
commit f2cee41ca9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 285 additions and 65 deletions

View File

@ -139,6 +139,38 @@ type SecurityPolicyData struct {
Files []File
}
// Run represents a run.
type Run struct {
URL string
// TODO: add fields, e.g., Result=["success", "failure"]
}
// Issue represents an issue.
type Issue struct {
URL string
// TODO: add fields, e.g., state=[opened|closed]
}
// MergeRequest represents a merge request.
type MergeRequest struct {
URL string
// TODO: add fields, e.g., State=["merged"|"closed"]
}
// Tool represents a tool.
type Tool struct {
// Runs of the tool.
Runs []Run
// Issues created by the tool.
Issues []Issue
// Merges requests created by the tool.
MergeRequests []MergeRequest
Name string
URL string
Desc string
ConfigFiles []File
}
// BinaryArtifactData contains the raw results
// for the Binary-Artifact check.
type BinaryArtifactData struct {
@ -146,11 +178,20 @@ type BinaryArtifactData struct {
Files []File
}
// DependencyUpdateToolData contains the raw results
// for the Dependency-Update-Tool check.
type DependencyUpdateToolData struct {
// Tools contains a list of tools.
// Note: we only populate one entry at most.
Tools []Tool
}
// RawResults contains results before a policy
// is applied.
type RawResults struct {
BinaryArtifactResults BinaryArtifactData
SecurityPolicyResults SecurityPolicyData
BinaryArtifactResults BinaryArtifactData
SecurityPolicyResults SecurityPolicyData
DependencyUpdateToolResults DependencyUpdateToolData
}
// CreateProportionalScore creates a proportional score.

View File

@ -15,10 +15,9 @@
package checks
import (
"strings"
"github.com/ossf/scorecard/v3/checker"
"github.com/ossf/scorecard/v3/checks/fileparser"
"github.com/ossf/scorecard/v3/checks/evaluation"
"github.com/ossf/scorecard/v3/checks/raw"
sce "github.com/ossf/scorecard/v3/errors"
)
@ -27,60 +26,23 @@ const CheckDependencyUpdateTool = "Dependency-Update-Tool"
//nolint
func init() {
registerCheck(CheckDependencyUpdateTool, UsesDependencyUpdateTool)
registerCheck(CheckDependencyUpdateTool, DependencyUpdateTool)
}
// UsesDependencyUpdateTool will check the repository uses a dependency update tool.
func UsesDependencyUpdateTool(c *checker.CheckRequest) checker.CheckResult {
var r bool
err := fileparser.CheckIfFileExists(c, fileExists, &r)
// DependencyUpdateTool checks if the repository uses a dependency update tool.
func DependencyUpdateTool(c *checker.CheckRequest) checker.CheckResult {
rawData, err := raw.DependencyUpdateTool(c.RepoClient)
if err != nil {
e := sce.WithMessage(sce.ErrScorecardInternal, err.Error())
return checker.CreateRuntimeErrorResult(CheckDependencyUpdateTool, e)
}
if !r {
c.Dlogger.Warn3(&checker.LogMessage{
Text: `dependabot config file not detected in source location.
We recommend setting this configuration in code so it can be easily verified by others.`,
})
c.Dlogger.Warn3(&checker.LogMessage{
Text: `renovatebot config file not detected in source location.
We recommend setting this configuration in code so it can be easily verified by others.`,
})
return checker.CreateMinScoreResult(CheckDependencyUpdateTool, "no update tool detected")
// Return raw results.
if c.RawResults != nil {
c.RawResults.DependencyUpdateToolResults = rawData
return checker.CheckResult{}
}
// High score result.
return checker.CreateMaxScoreResult(CheckDependencyUpdateTool, "update tool detected")
}
// fileExists will validate the if frozen dependencies file name exists.
func fileExists(name string, dl checker.DetailLogger, data fileparser.FileCbData) (bool, error) {
pdata := fileparser.FileGetCbDataAsBoolPointer(data)
switch strings.ToLower(name) {
case ".github/dependabot.yml":
dl.Info3(&checker.LogMessage{
Path: name,
Type: checker.FileTypeSource,
Offset: checker.OffsetDefault,
Text: "dependabot detected",
})
// https://docs.renovatebot.com/configuration-options/
case ".github/renovate.json", ".github/renovate.json5", ".renovaterc.json", "renovate.json",
"renovate.json5", ".renovaterc":
dl.Info3(&checker.LogMessage{
Path: name,
Type: checker.FileTypeSource,
Offset: checker.OffsetDefault,
Text: "renovate detected",
})
default:
// Continue iterating.
return true, nil
}
*pdata = true
// We found the file, no need to continue iterating.
return false, nil
// Return the score evaluation.
return evaluation.DependencyUpdateTool(CheckDependencyUpdateTool, c.Dlogger, &rawData)
}

View File

@ -19,7 +19,7 @@ import (
sce "github.com/ossf/scorecard/v3/errors"
)
// BinaryArtifacts applies the score policy for the Binary-Artiacts check.
// BinaryArtifacts applies the score policy for the Binary-Artifacts check.
func BinaryArtifacts(name string, dl checker.DetailLogger,
r *checker.BinaryArtifactData) checker.CheckResult {
if r == nil {

View File

@ -0,0 +1,68 @@
// Copyright 2020 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/v3/checker"
sce "github.com/ossf/scorecard/v3/errors"
)
// DependencyUpdateTool applies the score policy for the Dependency-Update-Tool check.
func DependencyUpdateTool(name string, dl checker.DetailLogger,
r *checker.DependencyUpdateToolData) checker.CheckResult {
if r == nil {
e := sce.WithMessage(sce.ErrScorecardInternal, "empty raw data")
return checker.CreateRuntimeErrorResult(name, e)
}
// Apply the policy evaluation.
if r.Tools == nil || len(r.Tools) == 0 {
dl.Warn3(&checker.LogMessage{
Text: `dependabot config file not detected in source location.
We recommend setting this configuration in code so it can be easily verified by others.`,
})
dl.Warn3(&checker.LogMessage{
Text: `renovatebot config file not detected in source location.
We recommend setting this configuration in code so it can be easily verified by others.`,
})
return checker.CreateMinScoreResult(name, "no update tool detected")
}
// Validate the input.
if len(r.Tools) != 1 {
e := sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("found %d tools, expected 1", len(r.Tools)))
return checker.CreateRuntimeErrorResult(name, e)
}
if len(r.Tools[0].ConfigFiles) != 1 {
e := sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("found %d config files, expected 1", len(r.Tools[0].ConfigFiles)))
return checker.CreateRuntimeErrorResult(name, e)
}
// Note: only one file per tool is present,
// so we do not iterate thru all entries.
dl.Info3(&checker.LogMessage{
Path: r.Tools[0].ConfigFiles[0].Path,
Type: r.Tools[0].ConfigFiles[0].Type,
Offset: r.Tools[0].ConfigFiles[0].Offset,
Text: fmt.Sprintf("%s detected", r.Tools[0].Name),
})
// High score result.
return checker.CreateMaxScoreResult(name, "update tool detected")
}

View File

@ -168,6 +168,34 @@ func CheckFilesContentV6(shellPathFnPattern string,
return nil
}
// FileCbV6 is the callback.
// The bool returned indicates whether the FileCbData
// should continue iterating over files or not.
type FileCbV6 func(path string, data FileCbData) (bool, error)
// CheckIfFileExistsV6 downloads the tar of the repository and calls the onFile() to check
// for the occurrence.
func CheckIfFileExistsV6(repoClient clients.RepoClient,
onFile FileCbV6, data FileCbData) error {
matchedFiles, err := repoClient.ListFiles(func(string) (bool, error) { return true, nil })
if err != nil {
// nolint: wrapcheck
return err
}
for _, filename := range matchedFiles {
continueIter, err := onFile(filename, data)
if err != nil {
return err
}
if !continueIter {
break
}
}
return nil
}
// FileCb represents a callback fn.
type FileCb func(path string,
dl checker.DetailLogger, data FileCbData) (bool, error)

View File

@ -0,0 +1,82 @@
// Copyright 2020 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 raw
import (
"fmt"
"strings"
"github.com/ossf/scorecard/v3/checker"
"github.com/ossf/scorecard/v3/checks/fileparser"
"github.com/ossf/scorecard/v3/clients"
)
// DependencyUpdateTool is the exported name for Depdendency-Update-Tool.
func DependencyUpdateTool(c clients.RepoClient) (checker.DependencyUpdateToolData, error) {
var tools []checker.Tool
err := fileparser.CheckIfFileExistsV6(c, checkDependencyFileExists, &tools)
if err != nil {
return checker.DependencyUpdateToolData{}, fmt.Errorf("%w", err)
}
// No error, return the files.
return checker.DependencyUpdateToolData{Tools: tools}, nil
}
func checkDependencyFileExists(name string, data fileparser.FileCbData) (bool, error) {
ptools, ok := data.(*[]checker.Tool)
if !ok {
// This never happens.
panic("invalid type")
}
switch strings.ToLower(name) {
case ".github/dependabot.yml":
*ptools = append(*ptools, checker.Tool{
Name: "Dependabot",
URL: "https://github.com/dependabot",
Desc: "Automated dependency updates built into GitHub",
ConfigFiles: []checker.File{
{
Path: name,
Type: checker.FileTypeSource,
Offset: checker.OffsetDefault,
},
},
})
// https://docs.renovatebot.com/configuration-options/
case ".github/renovate.json", ".github/renovate.json5", ".renovaterc.json", "renovate.json",
"renovate.json5", ".renovaterc":
*ptools = append(*ptools, checker.Tool{
Name: "Renovabot",
URL: "https://github.com/renovatebot/renovate",
Desc: "Automated dependency updates. Multi-platform and multi-language.",
ConfigFiles: []checker.File{
{
Path: name,
Type: checker.FileTypeSource,
Offset: checker.OffsetDefault,
},
},
})
default:
// Continue iterating.
return true, nil
}
// We found a file, no need to continue iterating.
return false, nil
}

View File

@ -52,7 +52,7 @@ var _ = Describe("E2E TEST:"+checks.CheckDependencyUpdateTool, func() {
NumberOfDebug: 0,
}
result := checks.UsesDependencyUpdateTool(&req)
result := checks.DependencyUpdateTool(&req)
// UPGRADEv2: to remove.
// Old version.
Expect(result.Error).Should(BeNil())
@ -82,7 +82,7 @@ var _ = Describe("E2E TEST:"+checks.CheckDependencyUpdateTool, func() {
NumberOfInfo: 1,
NumberOfDebug: 0,
}
result := checks.UsesDependencyUpdateTool(&req)
result := checks.DependencyUpdateTool(&req)
// UPGRADEv2: to remove.
// Old version.
Expect(result.Error).Should(BeNil())

View File

@ -33,24 +33,35 @@ type jsonScorecardRawResult struct {
}
// TODO: separate each check extraction into its own file.
type jsonFiles struct {
type jsonFile struct {
Path string `json:"path"`
Offset int `json:"offset,omitempty"`
}
type jsonTool struct {
Name string `json:"name"`
URL string `json:"url"`
Desc string `json:"desc"`
ConfigFiles []jsonFile `json:"files"`
// TODO: Runs, Issues, Merge requests.
}
type jsonRawResults struct {
// List of binaries found in the repo.
Binaries []jsonFiles `json:"binaries"`
Binaries []jsonFile `json:"binaries"`
// List of security policy files found in the repo.
// Note: we return one at most.
SecurityPolicies []jsonFiles `json:"security-policies"`
SecurityPolicies []jsonFile `json:"security-policies"`
// List of update tools.
// Note: we return one at most.
DependencyUpdateTools []jsonTool `json:"dependency-update-tools"`
}
//nolint:unparam
func (r *jsonScorecardRawResult) addBinaryArtifactRawResults(ba *checker.BinaryArtifactData) error {
r.Results.Binaries = []jsonFiles{}
r.Results.Binaries = []jsonFile{}
for _, v := range ba.Files {
r.Results.Binaries = append(r.Results.Binaries, jsonFiles{
r.Results.Binaries = append(r.Results.Binaries, jsonFile{
Path: v.Path,
})
}
@ -58,16 +69,38 @@ func (r *jsonScorecardRawResult) addBinaryArtifactRawResults(ba *checker.BinaryA
}
//nolint:unparam
func (r *jsonScorecardRawResult) addSecurityPolicyRawResults(ba *checker.SecurityPolicyData) error {
r.Results.SecurityPolicies = []jsonFiles{}
for _, v := range ba.Files {
r.Results.SecurityPolicies = append(r.Results.SecurityPolicies, jsonFiles{
func (r *jsonScorecardRawResult) addSecurityPolicyRawResults(sp *checker.SecurityPolicyData) error {
r.Results.SecurityPolicies = []jsonFile{}
for _, v := range sp.Files {
r.Results.SecurityPolicies = append(r.Results.SecurityPolicies, jsonFile{
Path: v.Path,
})
}
return nil
}
//nolint:unparam
func (r *jsonScorecardRawResult) addDependencyUpdateToolRawResults(dut *checker.DependencyUpdateToolData) error {
r.Results.DependencyUpdateTools = []jsonTool{}
for i := range dut.Tools {
t := dut.Tools[i]
offset := len(r.Results.DependencyUpdateTools)
r.Results.DependencyUpdateTools = append(r.Results.DependencyUpdateTools, jsonTool{
Name: t.Name,
URL: t.URL,
Desc: t.Desc,
})
for _, f := range t.ConfigFiles {
r.Results.DependencyUpdateTools[offset].ConfigFiles =
append(r.Results.DependencyUpdateTools[offset].ConfigFiles, jsonFile{
Path: f.Path,
Offset: f.Offset,
})
}
}
return nil
}
func (r *jsonScorecardRawResult) fillJSONRawResults(raw *checker.RawResults) error {
// Binary-Artifacts.
if err := r.addBinaryArtifactRawResults(&raw.BinaryArtifactResults); err != nil {
@ -78,6 +111,12 @@ func (r *jsonScorecardRawResult) fillJSONRawResults(raw *checker.RawResults) err
if err := r.addSecurityPolicyRawResults(&raw.SecurityPolicyResults); err != nil {
return sce.WithMessage(sce.ErrScorecardInternal, err.Error())
}
// Dependecy-Update-Tool.
if err := r.addDependencyUpdateToolRawResults(&raw.DependencyUpdateToolResults); err != nil {
return sce.WithMessage(sce.ErrScorecardInternal, err.Error())
}
return nil
}