/* Package cmd Copyright © 2024 Shane C. */ 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("
") markdownHandler.PlainText("Results:\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("
") } 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) } } }