/* Package cmd Copyright © 2024 Shane C. */ package cmd import ( "codeberg.org/mvdkleijn/forgejo-sdk/forgejo" "fmt" "github.com/go-git/go-git/v5" "github.com/kr/pretty" "github.com/nao1215/markdown" "github.com/owenrumney/go-sarif/sarif" "github.com/sethvargo/go-githubactions" "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) } pretty.Logln(ref.Name().Short()) pretty.Logln(ref.Hash().String()) 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 if isGithub { linkToFile.WriteString("./blob/commit/") } else { 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"]) } if len(rows) != 0 { 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(os.Getenv("GITHUB_TOKEN"))) if err != nil { log.Fatalln(err) } repoOwner, repoName := actionCtx.Repo() issues, _, err := forgeClient.ListIssues(forgejo.ListIssueOption{ Type: forgejo.IssueTypeIssue, }) if err != nil { log.Fatalln(err) } file, err := os.OpenFile("testing.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm) if err != nil { log.Fatalln(err) } defer file.Close() if len(issues) == 0 { if _, _, err := forgeClient.CreateIssue(repoOwner, repoName, forgejo.CreateIssueOption{ Title: "GoSec Report", Body: markdownOutputStr, }); err != nil { log.Fatalln(err) } } else { for _, issue := range issues { pretty.Logln(issue) if issue.Title == "GoSec Report" && strings.Contains(issue.Body, "GoSec Report:") { issueID, err := strconv.Atoi(string(issue.HTMLURL[len(issue.HTMLURL)-1])) if err != nil { log.Fatalln(err) } if _, _, err := forgeClient.EditIssue(repoOwner, repoName, int64(issueID), forgejo.EditIssueOption{ 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() { rootCmd.Flags().BoolVar(&isAction, "is-action", false, "If set, will run some things specific to git actions") rootCmd.Flags().BoolVar(&isGithub, "is-github", false, "If set, will change some outputs to support github") }