goscan/cmd/daemon.go
Shane C 97879670c1
All checks were successful
GoSec Scan / Gosec Check (push) Successful in 10s
fix gosec issues
2024-09-13 17:10:47 -04:00

450 lines
12 KiB
Go

/*
Package cmd
Copyright © 2024 Shane C. <shane@scaffoe.com>
*/
package cmd
import (
"bufio"
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"git.shadowhosting.xyz/actions/goscan/interfaces"
"git.shadowhosting.xyz/shanec/forgejo-sdk/forgejo"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/gofiber/fiber/v2/middleware/healthcheck"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"io"
"io/fs"
"net/http"
"os"
"strings"
"text/template"
"time"
)
type ActionTemplate struct {
DefaultBranch string
ServerURL string
}
const actionTemplate = `name: GoSec Scan
on:
schedule:
- cron: "@weekly"
push:
branches: ["{{.DefaultBranch}}"]
workflow_dispatch:
jobs:
gosec:
name: Gosec Check
runs-on: node20-bookworm
steps:
- name: Checkout
uses: {{.ServerURL}}/actions/checkout@v4
- name: Run Gosec Security Scanner
uses: {{.ServerURL}}/actions/goscan@main
`
// daemonCmd represents the daemon command
var daemonCmd = &cobra.Command{
Use: "daemon",
Short: "A brief description of your command",
Run: func(cmd *cobra.Command, args []string) {
forgeClient, err := forgejo.NewClient(viper.GetString("forgejo.url"), forgejo.SetToken(viper.GetString("forgejo.bot_token")))
if err != nil {
log.Fatal(err)
}
user, _, err := forgeClient.GetMyUserInfo()
if err != nil {
log.Fatal(err)
}
conf := &packet.Config{
Algorithm: packet.PubKeyAlgoEdDSA,
Curve: packet.Curve25519,
DefaultCipher: packet.CipherAES256,
}
var pgpEntity *openpgp.Entity
if _, err := os.Stat(os.Getenv("HOME") + "/keyring.pgp"); err != nil {
if errors.Is(err, fs.ErrNotExist) {
entity, err := openpgp.NewEntity("GoSec Git Signing", "", "gosec@git.shadowhosting.xyz", conf)
if err != nil {
log.Fatal(err)
}
publicKeyBuffer := bytes.NewBuffer(nil)
publicKeyEncoder, err := armor.Encode(publicKeyBuffer, openpgp.PublicKeyType, nil)
if err != nil {
log.Fatal(err)
}
defer func(publicKeyEncoder io.WriteCloser) {
err := publicKeyEncoder.Close()
if err != nil {
panic(err)
}
}(publicKeyEncoder)
err = entity.Serialize(publicKeyEncoder)
if err != nil {
log.Fatal(err)
}
err = publicKeyEncoder.Close()
if err != nil {
log.Fatal(err)
}
publicKeyArmor := publicKeyBuffer.String()
file, err := os.OpenFile("keyring.pgp", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatal(err)
}
err = entity.SerializePrivate(file, conf)
err = file.Close()
if err != nil {
log.Fatal(err)
}
token, _, err := forgeClient.GetGPGToken()
if err != nil {
log.Fatal(err)
}
signatureBuffer := bytes.NewBuffer(nil)
if err := openpgp.DetachSignText(signatureBuffer, entity, strings.NewReader(token), conf); err != nil {
log.Fatal(err)
}
encodedSignatureBuffer := bytes.NewBuffer(nil)
signatureEncoder, err := armor.Encode(encodedSignatureBuffer, openpgp.SignatureType, nil)
if err != nil {
log.Fatal(err)
}
defer func(signatureEncoder io.WriteCloser) {
err := signatureEncoder.Close()
if err != nil {
panic(err)
}
}(signatureEncoder)
if _, err := signatureEncoder.Write(signatureBuffer.Bytes()); err != nil {
log.Fatal(err)
}
err = signatureEncoder.Close()
if err != nil {
log.Fatal(err)
}
if _, _, err := forgeClient.CreateGPGKey(forgejo.CreateGPGKeyOption{
ArmoredKey: publicKeyArmor,
ArmoredSignature: encodedSignatureBuffer.String(),
}); err != nil {
log.Fatal(err)
}
pgpEntity = entity
} else {
log.Fatal(err)
}
} else if err == nil {
publicKeyFile, err := os.ReadFile(os.Getenv("HOME") + "/keyring.pgp")
if err != nil {
log.Fatal(err)
}
el, err := openpgp.ReadKeyRing(bytes.NewReader(publicKeyFile))
if err != nil {
log.Fatal(err)
}
if len(el) != 1 {
log.Fatal("invalid keyring")
}
pgpEntity = el[0]
}
appConfig := fiber.Config{
AppName: "GoScan (GoSec & Forgejo)",
Network: fiber.NetworkTCP,
JSONDecoder: json.Unmarshal,
JSONEncoder: json.Marshal,
EnableIPValidation: true,
}
if len(viper.GetString("webserver.proxy")) != 0 {
switch strings.ToLower("webserver.proxy") {
case "cloudflare":
var trustedProxies []string
v4Req, err := http.NewRequest("GET", "https://www.cloudflare.com/ips-v4/#", nil)
if err != nil {
log.Fatal("error creating request", err)
}
v6Req, err := http.NewRequest("GET", "https://www.cloudflare.com/ips-v6/#", nil)
if err != nil {
log.Fatal("error creating request", err)
}
client := &http.Client{}
v4Resp, err := client.Do(v4Req)
if err != nil {
log.Fatal("error doing request", err)
}
defer v4Resp.Body.Close()
v4Scanner := bufio.NewScanner(v4Resp.Body)
v4Scanner.Split(bufio.ScanLines)
for v4Scanner.Scan() {
trustedProxies = append(trustedProxies, v4Scanner.Text())
}
v6Resp, err := client.Do(v6Req)
if err != nil {
log.Fatal("error doing request", err)
}
defer v6Resp.Body.Close()
v6Scanner := bufio.NewScanner(v6Resp.Body)
v6Scanner.Split(bufio.ScanLines)
for v6Scanner.Scan() {
trustedProxies = append(trustedProxies, v6Scanner.Text())
}
appConfig.TrustedProxies = trustedProxies
appConfig.ProxyHeader = "Cf-Connecting-Ip"
appConfig.EnableTrustedProxyCheck = true
default:
log.Warnf("Unknown proxy type: %s", viper.GetString("webserver.proxy"))
}
}
app := fiber.New(appConfig)
app.Use(healthcheck.New())
app.Use(limiter.New(limiter.Config{
Max: 175,
Expiration: 25 * time.Second,
LimiterMiddleware: limiter.SlidingWindow{},
}))
app.Post("/webhook", func(c *fiber.Ctx) error {
event := c.Get("X-Forgejo-Event")
signature := c.Get("X-Forgejo-Signature")
bodyType := c.Get("Content-Type")
if len(event) == 0 || len(signature) == 0 || bodyType != "application/json" {
return c.SendStatus(fiber.StatusBadRequest)
}
mac := hmac.New(sha256.New, []byte(viper.GetString("forgejo.secret")))
mac.Write(c.Body())
bodyHash := hex.EncodeToString(mac.Sum(nil))
if signature != bodyHash {
return c.SendStatus(fiber.StatusBadRequest)
}
switch event {
case "push":
pushEvBody := new(interfaces.ForgejoPushEvent)
if err := c.BodyParser(pushEvBody); err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusBadRequest)
}
return handlePush(c, forgeClient, pushEvBody)
case "issues":
issueEvBody := new(interfaces.ForgejoIssueEvent)
if err := c.BodyParser(issueEvBody); err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusBadRequest)
}
return handleIssues(c, forgeClient, issueEvBody, pgpEntity, user)
default:
return c.SendStatus(fiber.StatusBadRequest)
}
})
if err := app.Listen("0.0.0.0:9000"); err != nil {
log.Fatal("error running webserver", err)
}
},
}
func handlePush(c *fiber.Ctx, forgeClient *forgejo.Client, event *interfaces.ForgejoPushEvent) error {
return c.SendStatus(fiber.StatusOK)
}
func handleIssues(c *fiber.Ctx, forgeClient *forgejo.Client, event *interfaces.ForgejoIssueEvent, entity *openpgp.Entity, user *forgejo.User) error {
if event.Action == "opened" && event.Issue.Title == "setup:goscan" {
if _, err := forgeClient.CreateRepoActionSecret(event.Issue.Repo.Owner, event.Issue.Repo.Name, forgejo.CreateSecretOption{
Name: "goscan_token",
Data: "hello!",
}); err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
gitDir, err := os.MkdirTemp("", "goscan-*")
if err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
defer func(path string) {
err := os.RemoveAll(path)
if err != nil {
panic(err)
}
}(gitDir)
repo, err := git.PlainClone(gitDir, false, &git.CloneOptions{
URL: event.Repo.CloneURL.String(),
ReferenceName: plumbing.NewBranchReferenceName(event.Repo.DefaultBranch),
Depth: 1,
Auth: &gitHttp.BasicAuth{
Username: "gosec",
Password: viper.GetString("forgejo.bot_token"),
},
})
if err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
if err := repo.CreateBranch(&config.Branch{
Name: plumbing.NewBranchReferenceName("goscan/setup").String(),
}); err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
worktree, err := repo.Worktree()
if err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
if err := os.MkdirAll(gitDir+"/.forgejo/workflows", 0775); /* #nosec G301 */ err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
if err := worktree.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName("goscan/setup"),
Create: true,
}); err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
tmpl, err := template.New("action_tmpl").Parse(actionTemplate)
if err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
var tmplBuffer bytes.Buffer
err = tmpl.Execute(&tmplBuffer, ActionTemplate{
DefaultBranch: event.Repo.DefaultBranch,
ServerURL: viper.GetString("forgejo.url"),
})
if err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
if err := os.WriteFile(gitDir+"/.forgejo/workflows/gosec.yml", []byte(tmplBuffer.String()), 0666); /* #nosec G306 */ err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
if _, err := worktree.Add(".forgejo/workflows/gosec.yml"); err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
signature := &object.Signature{
Name: user.FullName,
Email: user.Email,
When: time.Now(),
}
if _, err := worktree.Commit("Add GoScan action", &git.CommitOptions{
SignKey: entity,
Author: signature,
Committer: signature,
}); err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
if err := repo.Push(&git.PushOptions{
RemoteName: "origin",
Auth: &gitHttp.BasicAuth{
Username: "gosec",
Password: viper.GetString("forgejo.bot_token"),
},
}); err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
pr, _, err := forgeClient.CreatePullRequest(event.Issue.Repo.Owner, event.Issue.Repo.Name, forgejo.CreatePullRequestOption{
Base: event.Repo.DefaultBranch,
Head: "goscan/setup",
Assignees: []string{event.Issue.User.Username},
Title: "Setup GoScan",
Body: "Here is a default workflow configuration for gosec, feel free to edit it to your liking!",
})
if err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
if _, _, err := forgeClient.CreateIssueComment(event.Issue.Repo.Owner, event.Issue.Repo.Name, int64(event.Issue.Number), forgejo.CreateIssueCommentOption{
Body: fmt.Sprintf("@%s\nCreated GoScan Secret and created a [Pull Request](%s)", event.Issue.User.Username, pr.HTMLURL),
}); err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
stateClosed := forgejo.StateClosed
if _, _, err := forgeClient.EditIssue(event.Issue.Repo.Owner, event.Issue.Repo.Name, int64(event.Issue.Number), forgejo.EditIssueOption{
State: &stateClosed, //??
}); err != nil {
log.Error(err)
return c.SendStatus(fiber.StatusInternalServerError)
}
}
return c.SendStatus(fiber.StatusOK)
}
func init() {
rootCmd.AddCommand(daemonCmd)
}