Using library to parse github workflows

This commit is contained in:
Chris McGehee 2021-11-08 10:57:03 -08:00 committed by Naveen
parent f319aca82d
commit 3dc507b9e1
10 changed files with 181 additions and 202 deletions

View File

@ -150,3 +150,6 @@ linters-settings:
- ptrToRefParam
- typeUnparen
- unnecessaryBlock
wrapcheck:
ignorePackageGlobs:
- github.com/ossf/scorecard/v3/checks/fileparser

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.
package fileparser
import (
"errors"
)
var errInvalidGitHubWorkflow = errors.New("invalid GitHub workflow")

View File

@ -20,7 +20,7 @@ import (
"regexp"
"strings"
"gopkg.in/yaml.v3"
"github.com/rhysd/actionlint"
sce "github.com/ossf/scorecard/v3/errors"
)
@ -31,110 +31,51 @@ const defaultShellNonWindows = "bash"
// defaultShellWindows is the default shell used for GitHub workflow actions for Windows.
const defaultShellWindows = "pwsh"
// Structure for workflow config.
// We only declare the fields we need.
// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
type GitHubActionWorkflowConfig struct {
Jobs map[string]GitHubActionWorkflowJob
Name string `yaml:"name"`
}
// A Github Action Workflow Job.
// We only declare the fields we need.
// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
// nolint: govet
type GitHubActionWorkflowJob struct {
Name string `yaml:"name"`
Steps []GitHubActionWorkflowStep `yaml:"steps"`
Defaults struct {
Run struct {
Shell string `yaml:"shell"`
} `yaml:"run"`
} `yaml:"defaults"`
RunsOn stringOrSlice `yaml:"runs-on"`
Strategy struct {
// In most cases, the 'matrix' field will have a key of 'os' which is an array of strings, but there are
// some repos that have something like: 'matrix: ${{ fromJson(needs.matrix.outputs.latest) }}'.
Matrix interface{} `yaml:"matrix"`
} `yaml:"strategy"`
}
// A Github Action Workflow Step.
// We only declare the fields we need.
// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
type GitHubActionWorkflowStep struct {
Name string `yaml:"name"`
ID string `yaml:"id"`
Shell string `yaml:"shell"`
Run string `yaml:"run"`
If string `yaml:"if"`
Uses stringWithLine `yaml:"uses"`
}
// stringOrSlice is for fields that can be a single string or a slice of strings. If the field is a single string,
// this value will be a slice with a single string item.
type stringOrSlice []string
func (s *stringOrSlice) UnmarshalYAML(value *yaml.Node) error {
var stringSlice []string
err := value.Decode(&stringSlice)
if err == nil {
*s = stringSlice
// FormatActionlintError combines the errors into a single one.
func FormatActionlintError(errs []*actionlint.Error) error {
if len(errs) == 0 {
return nil
}
var single string
err = value.Decode(&single)
if err != nil {
return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error decoding stringOrSlice Value: %v", err))
builder := strings.Builder{}
builder.WriteString(errInvalidGitHubWorkflow.Error() + ":")
for _, err := range errs {
builder.WriteString("\n" + err.Error())
}
*s = []string{single}
return nil
}
// stringWithLine is for when you want to keep track of the line number that the string came from.
type stringWithLine struct {
Value string
Line int
}
func (ws *stringWithLine) UnmarshalYAML(value *yaml.Node) error {
err := value.Decode(&ws.Value)
if err != nil {
return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error decoding stringWithLine Value: %v", err))
}
ws.Line = value.Line
return nil
return sce.WithMessage(sce.ErrScorecardInternal, builder.String())
}
// GetOSesForJob returns the OSes this job runs on.
func GetOSesForJob(job *GitHubActionWorkflowJob) ([]string, error) {
func GetOSesForJob(job *actionlint.Job) ([]string, error) {
// The 'runs-on' field either lists the OS'es directly, or it can have an expression '${{ matrix.os }}' which
// is where the OS'es are actually listed.
getFromMatrix := len(job.RunsOn) == 1 && strings.Contains(job.RunsOn[0], "matrix.os")
if !getFromMatrix {
return job.RunsOn, nil
}
jobOSes := make([]string, 0)
// nolint: nestif
if m, ok := job.Strategy.Matrix.(map[string]interface{}); ok {
if osVal, ok := m["os"]; ok {
if oses, ok := osVal.([]interface{}); ok {
for _, os := range oses {
if strVal, ok := os.(string); ok {
jobOSes = append(jobOSes, strVal)
}
}
return jobOSes, nil
}
getFromMatrix := len(job.RunsOn.Labels) == 1 && strings.Contains(job.RunsOn.Labels[0].Value, "matrix.os")
if !getFromMatrix {
// We can get the OSes straight from 'runs-on'.
for _, os := range job.RunsOn.Labels {
jobOSes = append(jobOSes, os.Value)
}
return jobOSes, nil
}
for rowKey, rowValue := range job.Strategy.Matrix.Rows {
if rowKey != "os" {
continue
}
for _, os := range rowValue.Values {
jobOSes = append(jobOSes, strings.Trim(os.String(), "'\""))
}
}
return jobOSes, sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("unable to determine OS for job: %v", job.Name))
if len(jobOSes) == 0 {
return jobOSes, sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("unable to determine OS for job: %v", job.Name.Value))
}
return jobOSes, nil
}
// JobAlwaysRunsOnWindows returns true if the only OS that this job runs on is Windows.
func JobAlwaysRunsOnWindows(job *GitHubActionWorkflowJob) (bool, error) {
func JobAlwaysRunsOnWindows(job *actionlint.Job) (bool, error) {
jobOSes, err := GetOSesForJob(job)
if err != nil {
return false, err
@ -148,13 +89,27 @@ func JobAlwaysRunsOnWindows(job *GitHubActionWorkflowJob) (bool, error) {
}
// GetShellForStep returns the shell that is used to run the given step.
func GetShellForStep(step *GitHubActionWorkflowStep, job *GitHubActionWorkflowJob) (string, error) {
func GetShellForStep(step *actionlint.Step, job *actionlint.Job) (string, error) {
// https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell.
if step.Shell != "" {
return step.Shell, nil
execRun, ok := step.Exec.(*actionlint.ExecRun)
if !ok {
jobName := ""
if job.Name != nil {
jobName = job.Name.Value
}
stepName := ""
if step.Name != nil {
stepName = step.Name.Value
}
return "", sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("unable to parse step '%v' for job '%v'", jobName, stepName))
}
if job.Defaults.Run.Shell != "" {
return job.Defaults.Run.Shell, nil
if execRun != nil && execRun.Shell != nil && execRun.Shell.Value != "" {
return execRun.Shell.Value, nil
}
if job.Defaults != nil && job.Defaults.Run != nil && job.Defaults.Run.Shell != nil &&
job.Defaults.Run.Shell.Value != "" {
return job.Defaults.Run.Shell.Value, nil
}
isStepWindows, err := IsStepWindows(step)
@ -177,7 +132,10 @@ func GetShellForStep(step *GitHubActionWorkflowStep, job *GitHubActionWorkflowJo
}
// IsStepWindows returns true if the step will be run on Windows.
func IsStepWindows(step *GitHubActionWorkflowStep) (bool, error) {
func IsStepWindows(step *actionlint.Step) (bool, error) {
if step.If == nil {
return false, nil
}
windowsRegexes := []string{
// Looking for "if: runner.os == 'Windows'" (and variants)
`(?i)runner\.os\s*==\s*['"]windows['"]`,
@ -188,7 +146,7 @@ func IsStepWindows(step *GitHubActionWorkflowStep) (bool, error) {
}
for _, windowsRegex := range windowsRegexes {
matches, err := regexp.MatchString(windowsRegex, step.If)
matches, err := regexp.MatchString(windowsRegex, step.If.Value)
if err != nil {
return false, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error matching Windows regex: %v", err))
}

View File

@ -18,7 +18,7 @@ import (
"io/ioutil"
"testing"
"gopkg.in/yaml.v3"
"github.com/rhysd/actionlint"
"gotest.tools/assert/cmp"
)
@ -113,17 +113,17 @@ func TestGitHubWorkflowShell(t *testing.T) {
if err != nil {
t.Errorf("cannot read file: %v", err)
}
var workflow GitHubActionWorkflowConfig
err = yaml.Unmarshal(content, &workflow)
if err != nil {
t.Errorf("cannot unmarshal file: %v", err)
workflow, errs := actionlint.Parse(content)
if len(errs) > 0 && workflow == nil {
t.Errorf("cannot unmarshal file: %v", errs[0])
}
actualShells := make([]string, 0)
for _, job := range workflow.Jobs {
job := job
for _, step := range job.Steps {
step := step
shell, err := GetShellForStep(&step, &job)
shell, err := GetShellForStep(step, job)
if err != nil {
t.Errorf("error getting shell: %v", err)
}

View File

@ -18,7 +18,7 @@ import (
"fmt"
"strings"
"gopkg.in/yaml.v2"
"github.com/rhysd/actionlint"
"github.com/ossf/scorecard/v3/checker"
"github.com/ossf/scorecard/v3/checks/fileparser"
@ -53,25 +53,24 @@ func TokenPermissions(c *checker.CheckRequest) checker.CheckResult {
return createResultForLeastPrivilegeTokens(data, err)
}
func validatePermission(key string, value interface{}, path string,
func validatePermission(permissionKey string, permissionValue *actionlint.PermissionScope, path string,
dl checker.DetailLogger, pPermissions map[string]bool,
ignoredPermissions map[string]bool) error {
val, ok := value.(string)
if !ok {
if permissionValue.Value == nil {
return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error())
}
val := permissionValue.Value.Value
if strings.EqualFold(val, "write") {
if isPermissionOfInterest(key, ignoredPermissions) {
if isPermissionOfInterest(permissionKey, ignoredPermissions) {
dl.Warn3(&checker.LogMessage{
Path: path,
Type: checker.FileTypeSource,
// TODO: set line.
Offset: 1,
Text: fmt.Sprintf("'%v' permission set to '%v'", key, val),
Text: fmt.Sprintf("'%v' permission set to '%v'", permissionKey, val),
// TODO: set Snippet.
})
recordPermissionWrite(key, pPermissions)
recordPermissionWrite(permissionKey, pPermissions)
} else {
// Only log for debugging, otherwise
// it may confuse users.
@ -80,7 +79,7 @@ func validatePermission(key string, value interface{}, path string,
Type: checker.FileTypeSource,
// TODO: set line.
Offset: 1,
Text: fmt.Sprintf("'%v' permission set to '%v'", key, val),
Text: fmt.Sprintf("'%v' permission set to '%v'", permissionKey, val),
// TODO: set Snippet.
})
}
@ -92,22 +91,16 @@ func validatePermission(key string, value interface{}, path string,
Type: checker.FileTypeSource,
// TODO: set line correctly.
Offset: 1,
Text: fmt.Sprintf("'%v' permission set to '%v'", key, val),
Text: fmt.Sprintf("'%v' permission set to '%v'", permissionKey, val),
// TODO: set Snippet.
})
return nil
}
func validateMapPermissions(values map[interface{}]interface{}, path string,
func validateMapPermissions(scopes map[string]*actionlint.PermissionScope, path string,
dl checker.DetailLogger, pPermissions map[string]bool,
ignoredPermissions map[string]bool) error {
// Iterate over the permission, verify keys and values are strings.
for k, v := range values {
key, ok := k.(string)
if !ok {
return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error())
}
for key, v := range scopes {
if err := validatePermission(key, v, path, dl, pPermissions, ignoredPermissions); err != nil {
return err
}
@ -125,14 +118,12 @@ func recordAllPermissionsWrite(pPermissions map[string]bool) {
pPermissions["all"] = true
}
func validatePermissions(permissions interface{}, path string,
func validatePermissions(permissions *actionlint.Permissions, path string,
dl checker.DetailLogger, pPermissions map[string]bool,
ignoredPermissions map[string]bool) error {
// Check the type of our values.
switch val := permissions.(type) {
// Empty string is nil type.
// It defaults to 'none'
case nil:
allIsSet := permissions != nil && permissions.All != nil && permissions.All.Value != ""
scopeIsSet := permissions != nil && len(permissions.Scopes) > 0
if permissions == nil || (!allIsSet && !scopeIsSet) {
dl.Info3(&checker.LogMessage{
Path: path,
Type: checker.FileTypeSource,
@ -141,8 +132,9 @@ func validatePermissions(permissions interface{}, path string,
Text: "permissions set to 'none'",
// TODO: set Snippet.
})
// String type.
case string:
}
if allIsSet {
val := permissions.All.Value
if !strings.EqualFold(val, "read-all") && val != "" {
dl.Warn3(&checker.LogMessage{
Path: path,
@ -164,25 +156,17 @@ func validatePermissions(permissions interface{}, path string,
Text: fmt.Sprintf("permissions set to '%v'", val),
// TODO: set Snippet.
})
// Map type.
case map[interface{}]interface{}:
if err := validateMapPermissions(val, path, dl, pPermissions, ignoredPermissions); err != nil {
return err
}
// Invalid type.
default:
return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error())
} else /* scopeIsSet == true */ if err := validateMapPermissions(permissions.Scopes, path, dl, pPermissions,
ignoredPermissions); err != nil {
return err
}
return nil
}
func validateTopLevelPermissions(config map[interface{}]interface{}, path string,
func validateTopLevelPermissions(workflow *actionlint.Workflow, path string,
dl checker.DetailLogger, pdata *permissionCbData) error {
// Check if permissions are set explicitly.
permissions, ok := config["permissions"]
if !ok {
if workflow.Permissions == nil {
dl.Warn3(&checker.LogMessage{
Path: path,
Type: checker.FileTypeSource,
@ -193,35 +177,18 @@ func validateTopLevelPermissions(config map[interface{}]interface{}, path string
return nil
}
return validatePermissions(permissions, path, dl,
return validatePermissions(workflow.Permissions, path, dl,
pdata.topLevelWritePermissions, map[string]bool{})
}
func validateRunLevelPermissions(config map[interface{}]interface{}, path string,
func validateRunLevelPermissions(workflow *actionlint.Workflow, path string,
dl checker.DetailLogger, pdata *permissionCbData,
ignoredPermissions map[string]bool) error {
var jobs interface{}
jobs, ok := config["jobs"]
if !ok {
return nil
}
mjobs, ok := jobs.(map[interface{}]interface{})
if !ok {
return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error())
}
for _, value := range mjobs {
job, ok := value.(map[interface{}]interface{})
if !ok {
return sce.WithMessage(sce.ErrScorecardInternal, errInvalidGitHubWorkflow.Error())
}
for _, job := range workflow.Jobs {
// Run-level permissions may be left undefined.
// For most workflows, no write permissions are needed,
// so only top-level read-only permissions need to be declared.
permissions, ok := job["permissions"]
if !ok {
if job.Permissions == nil {
dl.Debug3(&checker.LogMessage{
Path: path,
Type: checker.FileTypeSource,
@ -230,8 +197,7 @@ func validateRunLevelPermissions(config map[interface{}]interface{}, path string
})
continue
}
err := validatePermissions(permissions, path, dl,
pdata.runLevelWritePermissions, ignoredPermissions)
err := validatePermissions(job.Permissions, path, dl, pdata.runLevelWritePermissions, ignoredPermissions)
if err != nil {
return err
}
@ -375,11 +341,9 @@ func validateGitHubActionTokenPermissions(path string, content []byte,
return true, nil
}
var workflow map[interface{}]interface{}
err := yaml.Unmarshal(content, &workflow)
if err != nil {
return false,
sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("yaml.Unmarshal: %v", err))
workflow, errs := actionlint.Parse(content)
if len(errs) > 0 && workflow == nil {
return false, fileparser.FormatActionlintError(errs)
}
// 1. Top-level permission definitions.

View File

@ -20,7 +20,7 @@ import (
"strings"
"github.com/moby/buildkit/frontend/dockerfile/parser"
"gopkg.in/yaml.v3"
"github.com/rhysd/actionlint"
"github.com/ossf/scorecard/v3/checker"
"github.com/ossf/scorecard/v3/checks/fileparser"
@ -451,11 +451,11 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
return true, nil
}
var workflow fileparser.GitHubActionWorkflowConfig
err := yaml.Unmarshal(content, &workflow)
if err != nil {
return false, sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("%v: %v", errInternalInvalidYamlFile, err))
workflow, errs := actionlint.Parse(content)
if len(errs) > 0 && workflow == nil {
// actionlint is a linter, so it will return errors when the yaml file does not meet its linting standards.
// Often we don't care about these errors.
return false, fileparser.FormatActionlintError(errs)
}
githubVarRegex := regexp.MustCompile(`{{[^{}]*}}`)
@ -465,12 +465,12 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
job := job
for _, step := range job.Steps {
step := step
if step.Run == "" {
if step.Exec.Kind() != actionlint.ExecKindRun {
continue
}
// https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun.
shell, err := fileparser.GetShellForStep(&step, &job)
shell, err := fileparser.GetShellForStep(step, job)
if err != nil {
return false, err
}
@ -479,7 +479,7 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
continue
}
run := step.Run
run := step.Exec.(*actionlint.ExecRun).Run.Value
// We replace the `${{ github.variable }}` to avoid shell parsing failures.
script := githubVarRegex.ReplaceAll([]byte(run), []byte("GITHUB_REDACTED_VAR"))
scriptContent = fmt.Sprintf("%v\n%v", scriptContent, string(script))
@ -487,6 +487,7 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by
}
if scriptContent != "" {
var err error
validated, err = validateShellFile(pathfn, []byte(scriptContent), dl)
if err != nil {
return false, err
@ -534,30 +535,42 @@ func validateGitHubActionWorkflow(pathfn string, content []byte,
return true, nil
}
var workflow fileparser.GitHubActionWorkflowConfig
err := yaml.Unmarshal(content, &workflow)
if err != nil {
return false, sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("%v: %v", errInternalInvalidYamlFile, err))
workflow, errs := actionlint.Parse(content)
if len(errs) > 0 && workflow == nil {
// actionlint is a linter, so it will return errors when the yaml file does not meet its linting standards.
// Often we don't care about these errors.
return false, fileparser.FormatActionlintError(errs)
}
hashRegex := regexp.MustCompile(`^.*@[a-f\d]{40,}`)
for jobName, job := range workflow.Jobs {
if len(job.Name) > 0 {
jobName = job.Name
if job.Name != nil && len(job.Name.Value) > 0 {
jobName = job.Name.Value
}
for _, step := range job.Steps {
if len(step.Uses.Value) > 0 {
// Ensure a hash at least as large as SHA1 is used (40 hex characters).
// Example: action-name@hash
match := hashRegex.Match([]byte(step.Uses.Value))
if !match {
dl.Warn3(&checker.LogMessage{
Path: pathfn, Type: checker.FileTypeSource, Offset: step.Uses.Line, Snippet: step.Uses.Value,
Text: fmt.Sprintf("dependency not pinned by hash (job '%v')", jobName),
})
if step.Exec.Kind() != actionlint.ExecKindAction {
continue
}
execAction, ok := step.Exec.(*actionlint.ExecAction)
if !ok {
stepName := ""
if step.Name != nil {
stepName = step.Name.Value
}
return false, sce.WithMessage(sce.ErrScorecardInternal,
fmt.Sprintf("unable to parse step '%v' for job '%v'", jobName, stepName))
}
// Ensure a hash at least as large as SHA1 is used (40 hex characters).
// Example: action-name@hash
match := hashRegex.Match([]byte(execAction.Uses.Value))
if !match {
dl.Warn3(&checker.LogMessage{
Path: pathfn, Type: checker.FileTypeSource, Offset: execAction.Uses.Pos.Line, Snippet: execAction.Uses.Value,
Text: fmt.Sprintf("dependency not pinned by hash (job '%v')", jobName),
})
}
githubOwned := fileparser.IsGitHubOwnedAction(execAction.Uses.Value)
addWorkflowPinnedResult(pdata, match, githubOwned)
}

View File

@ -933,7 +933,7 @@ func TestGitHubWorkflowUsesLineNumber(t *testing.T) {
},
{
dependency: "docker/build-push-action@1.2.3",
lineNumber: 26,
lineNumber: 24,
},
},
},

View File

@ -20,7 +20,5 @@ jobs:
steps:
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
run: echo "unpinned dependency"
- name: some name
run: echo "unpinned dependency"
uses: docker/build-push-action@1.2.3

12
go.mod
View File

@ -35,7 +35,10 @@ require (
mvdan.cc/sh/v3 v3.4.0
)
require gotest.tools v2.2.0+incompatible
require (
github.com/rhysd/actionlint v1.6.7
gotest.tools v2.2.0+incompatible
)
require (
cloud.google.com/go v0.94.1 // indirect
@ -52,6 +55,7 @@ require (
github.com/docker/docker v20.10.7+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.3 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fatih/color v1.12.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
@ -69,12 +73,16 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/klauspost/compress v1.13.5 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect

16
go.sum
View File

@ -483,6 +483,7 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvyukov/go-fuzz v0.0.0-20210602112143-b1f3d6f4ef4e/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@ -504,6 +505,8 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
@ -931,6 +934,8 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
@ -941,9 +946,12 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -1127,6 +1135,12 @@ github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:
github.com/quasilyte/go-ruleguard v0.1.2-0.20200318202121-b00d7a75d3d8/go.mod h1:CGFX09Ci3pq9QZdj86B+VGIdNj4VyCo2iPOGS9esB/k=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
github.com/rhysd/actionlint v1.6.7 h1:cOojXJ/N3OAoOaTo8zEEA6uWovUC+0yP3dLYEZnRWg4=
github.com/rhysd/actionlint v1.6.7/go.mod h1:vFbgcUNEK84kr9Lb3vEfqjTov/Q3X5py+ppKihWgH+w=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=