450 lines
12 KiB
Go
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)
|
|
}
|