goscan/cmd/root.go
Shane C 03e9e6bc40
All checks were successful
GoSec Scan / Gosec Check (push) Successful in 7s
add basic bot
2024-09-13 16:56:18 -04:00

307 lines
7.8 KiB
Go

/*
Package cmd
Copyright © 2024 Shane C. <shane@scaffoe.com>
*/
package cmd
import (
"fmt"
"git.eggactyl.cloud/Eggactyl/shell/linux"
"git.shadowhosting.xyz/shanec/forgejo-sdk/forgejo"
"github.com/go-git/go-git/v5"
"github.com/nao1215/markdown"
"github.com/owenrumney/go-sarif/sarif"
"github.com/sethvargo/go-githubactions"
"github.com/spf13/viper"
"log"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
)
var isAction bool
var isGithub bool
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "goscan",
Short: "A brief description of your application",
Run: func(cmd *cobra.Command, args []string) {
action := githubactions.New()
cwd, err := os.Getwd()
if err != nil {
log.Fatalln(err)
}
repo, err := git.PlainOpen(cwd)
if err != nil {
log.Fatalln(err)
}
ref, err := repo.Head()
if err != nil {
log.Fatalln(err)
}
gosecCmd, err := linux.NewCommand(linux.CommandOptions{
Cwd: cwd,
Shell: "/bin/sh",
Command: "gosec",
Args: []string{
"-r", "-no-fail", "-fmt", "sarif", "-out", "output.sarif", "./...",
},
})
if err != nil {
log.Fatalln(err)
}
if err := gosecCmd.Run(); err != nil {
log.Fatalln(err)
}
report, err := sarif.Open("output.sarif")
if err != nil {
log.Fatal(err)
}
if len(report.Runs) != 1 {
log.Fatalf("expected 1 result, got %d", len(report.Runs))
}
run := report.Runs[0]
var rows [][]string
sevCountMap := map[string]int{
"high": 0,
"medium": 0,
"low": 0,
}
for _, result := range run.Results {
rule, err := run.GetRuleById(*result.RuleID)
if err != nil {
log.Fatal(err)
}
if len(result.Locations) != 1 {
log.Fatalf("expected 1 result, got %d", len(result.Locations))
}
location := result.Locations[0]
severity := rule.Properties["tags"].([]interface{})[1].(string)
confidence := rule.Properties["precision"].(string)
var severityEmoji string
switch strings.ToLower(severity) {
case "low":
sevCountMap["low"] = sevCountMap["low"] + 1
severityEmoji = "🟨"
case "medium":
sevCountMap["medium"] = sevCountMap["medium"] + 1
severityEmoji = "🟧"
case "high":
sevCountMap["high"] = sevCountMap["high"] + 1
severityEmoji = "🟥"
}
var confidenceEmoji string
switch strings.ToLower(confidence) {
case "low":
confidenceEmoji = "🟧"
case "medium":
confidenceEmoji = "🟨"
case "high":
confidenceEmoji = "🟩"
}
var linkToFile strings.Builder
linkToFile.WriteString("./src/commit/")
linkToFile.WriteString(ref.Hash().String() + "/" + *location.PhysicalLocation.ArtifactLocation.URI + "#L" + strconv.Itoa(*location.PhysicalLocation.Region.StartLine))
rows = append(rows, []string{
fmt.Sprintf("`%s` - %s", *result.RuleID, *rule.Name),
fmt.Sprintf("%s **%c**", severityEmoji, severity[0]),
fmt.Sprintf("%s **%s**", confidenceEmoji, strings.ToUpper(string(confidence[0]))),
fmt.Sprintf("[%s L%d C%d](%s)", *location.PhysicalLocation.ArtifactLocation.URI, *location.PhysicalLocation.Region.StartLine, *location.PhysicalLocation.Region.StartColumn, linkToFile.String()),
})
}
var markdownOutput strings.Builder
markdownHandler := markdown.NewMarkdown(&markdownOutput)
markdownHandler.H1("GoSec Report:")
markdownHandler.PlainText("This report automatically updates each time the action runs.\n")
if len(rows) == 0 {
markdownHandler.PlainText("**Nothing Found! 🥳**")
} else {
markdownHandler.PlainText("<details>")
markdownHandler.PlainText("<summary>Results:</summary>\n")
if sevCountMap["high"] != 0 {
markdownHandler.PlainText(fmt.Sprintf("🟥 **%d** high severity issues\n", sevCountMap["high"]))
}
if sevCountMap["medium"] != 0 {
markdownHandler.PlainText(fmt.Sprintf("🟧 **%d** medium severity issues\n", sevCountMap["medium"]))
}
if sevCountMap["low"] != 0 {
markdownHandler.PlainText(fmt.Sprintf("🟨 **%d** low severity issues\n", sevCountMap["low"]))
}
markdownHandler.PlainTextf("Total of **%d** issues.\n", sevCountMap["high"]+sevCountMap["medium"]+sevCountMap["low"])
markdownHandler.CustomTable(markdown.TableSet{
Header: []string{"Rule", "Severity", "Confidence", "Location"},
Rows: rows,
}, markdown.TableOptions{
AutoWrapText: false,
AutoFormatHeaders: false,
})
markdownHandler.PlainText("</details>")
}
err = markdownHandler.Build()
if err != nil {
log.Fatalln(err)
}
markdownOutputStr := markdownOutput.String()
if isAction {
action.AddStepSummary(markdownOutputStr)
actionCtx, err := action.Context()
if err != nil {
log.Fatalln(err)
}
forgeClient, err := forgejo.NewClient(actionCtx.ServerURL, forgejo.SetToken(action.GetInput("token")))
if err != nil {
log.Fatalln(err)
}
repoOwner, repoName := actionCtx.Repo()
myUser, _, err := forgeClient.GetMyUserInfo()
if err != nil {
log.Fatalln(err)
}
issues, _, err := forgeClient.ListRepoIssues(repoOwner, repoName, forgejo.ListIssueOption{
State: forgejo.StateOpen,
ListOptions: forgejo.ListOptions{
Page: -1,
},
Type: forgejo.IssueTypeIssue,
})
if err != nil {
log.Fatalln(err)
}
for _, issue := range issues {
if issue.State == forgejo.StateOpen && issue.Title == "GoSec Report" && repoOwner+"/"+repoName == issue.Repository.FullName && myUser.UserName == issue.Poster.UserName {
if _, _, err := forgeClient.EditIssue(repoOwner, repoName, issue.Index, forgejo.EditIssueOption{
Body: &markdownOutputStr,
}); err != nil {
log.Fatalln(err)
}
os.Exit(0)
}
}
if _, _, err := forgeClient.CreateIssue(repoOwner, repoName, forgejo.CreateIssueOption{
Title: "GoSec Report",
Body: markdownOutputStr,
}); err != nil {
log.Fatalln(err)
}
}
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.Flags().BoolVar(&isAction, "is-action", false, "If set, will run some things specific to git actions")
}
func initConfig() {
viper.AddConfigPath("/opt/goscan")
viper.SetConfigType("yaml")
viper.SetConfigName("config")
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
for _, variable := range os.Environ() {
if strings.HasPrefix(variable, "GOSCAN__") {
sepEnv := strings.SplitN(variable, "=", 1)
key := sepEnv[0]
value := sepEnv[1]
key = strings.Replace(key, "GOSCAN__", "", 1)
if strings.HasPrefix(key, "database") {
key = strings.Replace(key, "database", "", 1)
switch strings.ToLower(key) {
case "host":
viper.Set("database.host", value)
case "port":
viper.Set("database.port", value)
case "user":
viper.Set("database.user", value)
case "password":
viper.Set("database.password", value)
case "name":
viper.Set("database.name", value)
case "tz":
viper.Set("database.tz", value)
default:
log.Fatalln("invalid config option")
}
} else if strings.HasPrefix(key, "forgejo") {
key = strings.Replace(key, "forgejo", "", 1)
switch strings.ToLower(key) {
case "url":
viper.Set("forgejo.url", value)
case "bot_token":
viper.Set("forgejo.bot_token", value)
case "secret":
viper.Set("forgejo.secret", value)
default:
log.Fatalln("invalid config option")
}
} else {
log.Fatalln("invalid config option")
}
}
}
if len(viper.ConfigFileUsed()) != 0 {
if err := viper.WriteConfig(); err != nil {
log.Fatalln("error writing to file", err)
}
}
}