307 lines
7.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|