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

Create a Golang-based entrypoint for scorecard-action (1/n) #122

Merged
merged 17 commits into from Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
# Testing
unit-coverage.out
125 changes: 125 additions & 0 deletions entrypoint/entrypoint.go
@@ -0,0 +1,125 @@
// Copyright 2022 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 entrypoint

import (
"errors"
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/ossf/scorecard-action/options"
"github.com/ossf/scorecard/v4/cmd"
scopts "github.com/ossf/scorecard/v4/options"
)

// New creates a new scorecard command which can be used as an entrypoint for
// GitHub Actions.
func New() (*cobra.Command, error) {
opts, err := options.New()
if err != nil {
return nil, fmt.Errorf("creating new options: %w", err)
}

if err := opts.Initialize(); err != nil {
return nil, fmt.Errorf("initializing options: %w", err)
}

scOpts := opts.ScorecardOpts

actionCmd := cmd.New(scOpts)

actionCmd.Flags().StringVar(
&scOpts.ResultsFile,
"output-file",
scOpts.ResultsFile,
"path to output results to",
)

// Adapt scorecard's PreRunE to support an output file
// TODO(scorecard): Move this into scorecard
var out, stdout *os.File
actionCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
err := scOpts.Validate()
if err != nil {
return fmt.Errorf("validating options: %w", err)
}

if scOpts.ResultsFile != "" {
var err error
out, err = os.Create(scOpts.ResultsFile)
if err != nil {
return fmt.Errorf(
"creating output file (%s): %w",
scOpts.ResultsFile,
err,
)
}
stdout = os.Stdout
os.Stdout = out
actionCmd.SetOut(out)
}

return nil
}

actionCmd.PersistentPostRun = func(cmd *cobra.Command, args []string) {
if out != nil {
_ = out.Close()
}
os.Stdout = stdout
}

var hideErrs []error
hiddenFlags := []string{
scopts.FlagNPM,
scopts.FlagPyPI,
scopts.FlagRubyGems,
}

for _, f := range hiddenFlags {
err := actionCmd.Flags().MarkHidden(f)
if err != nil {
hideErrs = append(hideErrs, err)
}
}

if len(hideErrs) > 0 {
return nil, fmt.Errorf(
"%w: %+v",
errHideFlags,
hideErrs,
)
}

// Add sub-commands.
actionCmd.AddCommand(printConfigCmd(opts))

return actionCmd, nil
}

func printConfigCmd(o *options.Options) *cobra.Command {
c := &cobra.Command{
Use: "print-config",
Run: func(cmd *cobra.Command, args []string) {
o.Print()
},
}

return c
}

var errHideFlags = errors.New("errors occurred while trying to hide scorecard flags")
161 changes: 161 additions & 0 deletions github/github.go
@@ -0,0 +1,161 @@
// Copyright OpenSSF 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 github

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"

"github.com/ossf/scorecard/v4/clients/githubrepo/roundtripper"
"github.com/ossf/scorecard/v4/log"
)

const (
baseRepoURL = "https://api.github.com/repos/"
)

// RepoInfo is a struct for repository information.
type RepoInfo struct {
Repo repo `json:"repository"`
justaugustus marked this conversation as resolved.
Show resolved Hide resolved
respBytes []byte
}

type repo struct {
/*
https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#github_repository_is_fork

GITHUB_REPOSITORY_IS_FORK is true if the repository is a fork.
*/
DefaultBranch string `json:"default_branch"`
Fork bool `json:"fork"`
Private bool `json:"private"`
}

// Client holds a context and roundtripper for querying repo info from GitHub.
type Client struct {
ctx context.Context
rt http.RoundTripper
}

// NewClient returns a new Client for querying repo info from GitHub.
func NewClient(ctx context.Context) *Client {
c := &Client{}

defaultCtx := context.Background()
if ctx == nil {
ctx = defaultCtx
}

c.SetContext(ctx)
c.SetDefaultTransport()
return c
}

// SetContext sets a context for a GitHub client.
func (c *Client) SetContext(ctx context.Context) {
c.ctx = ctx
}

// SetTransport sets a http.RoundTripper for a GitHub client.
func (c *Client) SetTransport(rt http.RoundTripper) {
c.rt = rt
}

// SetDefaultTransport sets the scorecard roundtripper for a GitHub client.
func (c *Client) SetDefaultTransport() {
logger := log.NewLogger(log.DefaultLevel)
rt := roundtripper.NewTransport(c.ctx, logger)
c.rt = rt
}

// WriteRepoInfo queries GitHub for repo info and writes it to a file.
func WriteRepoInfo(ctx context.Context, repoName, path string) error {
c := NewClient(ctx)
repoInfo, err := c.RepoInfo(repoName)
if err != nil {
return fmt.Errorf("getting repo info: %w", err)
}

repoFile, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating repo info file: %w", err)
}
defer repoFile.Close()

resp := repoInfo.respBytes
_, writeErr := repoFile.Write(resp)
if writeErr != nil {
return fmt.Errorf("writing repo info: %w", writeErr)
}

return nil
}

// RepoInfo is a function to get the repository information.
// It is decided to not use the golang GitHub library because of the
// dependency on the github.com/google/go-github/github library
// which will in turn require other dependencies.
func (c *Client) RepoInfo(repoName string) (RepoInfo, error) {
var r RepoInfo

baseURL, err := url.Parse(baseRepoURL)
if err != nil {
return r, fmt.Errorf("parsing base repo URL: %w", err)
}

repoURL, err := baseURL.Parse(repoName)
if err != nil {
return r, fmt.Errorf("parsing repo endpoint: %w", err)
}

method := "GET"
req, err := http.NewRequestWithContext(
c.ctx,
method,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use http.MethodGet here instead.

repoURL.String(),
nil,
)
if err != nil {
return r, fmt.Errorf("error creating request: %w", err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return r, fmt.Errorf("error creating request: %w", err)
}
defer resp.Body.Close()
if err != nil {
return r, fmt.Errorf("error reading response body: %w", err)
}

respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return r, fmt.Errorf("error reading response body: %w", err)
}

r.respBytes = respBytes

err = json.Unmarshal(respBytes, &r)
if err != nil {
return r, fmt.Errorf("error decoding response body: %w", err)
}

return r, nil
}
62 changes: 61 additions & 1 deletion go.mod
Expand Up @@ -2,4 +2,64 @@ module github.com/ossf/scorecard-action

go 1.17

require github.com/google/go-cmp v0.5.7
require (
github.com/caarlos0/env/v6 v6.9.1
github.com/google/go-cmp v0.5.7
github.com/ossf/scorecard/v4 v4.1.1-0.20220306220811-4b9f0389c6f6
github.com/spf13/cobra v1.3.0
)

require (
cloud.google.com/go v0.100.2 // indirect
cloud.google.com/go/compute v1.3.0 // indirect
cloud.google.com/go/iam v0.1.1 // indirect
cloud.google.com/go/storage v1.18.2 // indirect
github.com/bombsimon/logrusr/v2 v2.0.1 // indirect
github.com/bradleyfalzon/ghinstallation/v2 v2.0.4 // indirect
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
github.com/containerd/typeurl v1.0.2 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-github/v38 v38.1.0 // indirect
github.com/google/go-github/v41 v41.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/wire v0.5.0 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/moby/buildkit v0.8.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rhysd/actionlint v1.6.9 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa // indirect
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.opencensus.io v0.23.0 // indirect
gocloud.dev v0.24.0 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/api v0.70.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect
google.golang.org/grpc v1.44.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
mvdan.cc/sh/v3 v3.4.3 // indirect
sigs.k8s.io/release-utils v0.5.0 // indirect
)