Compare commits

...
Author SHA1 Message Date
sixsmith f648a94a25 update 2026-09-25 21:58:10 -04:00
sixsmith 12c29fd0be temp 2026-09-25 19:33:19 -04:00
5 changed files with 209 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
package app
import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/abra/pkg/vulncheck"
"github.com/spf13/cobra"
"context"
)
var appVulncheckAliases = i18n.G("vc")
var AppVulncheckCommand = &cobra.Command{
Use: i18n.G("vulncheck [flags] APP"),
Aliases: []string{appVulncheckAliases},
Short: i18n.G("Check for vulnerabilities in images for this app"),
Long: i18n.G("Check for vulnerabilities in images for this app"),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is not deployed?", app.Name))
}
// log.Debugf("App: %+v", app)
// log.Debugf("Deployed version: %s", deployMeta.Version)
status := stack.GetAllDeployedServices(cl, app.StackName())
ac := &vulncheck.AdvisoryClient{}
ac.Init()
rows := [][]string{}
for _, svc := range status.Services {
imgname := formatter.TrimNs(svc.Spec.Labels["com.docker.stack.image"])
advisories := ac.GetAdvisoriesForImage(imgname)
for _, advisory := range advisories {
rows = append(rows, []string{
advisory.Recipe,
svc.Spec.Name,
imgname,
advisory.ShortDesc,
advisory.Details,
})
}
}
if len(rows) == 0 {
log.Info(i18n.G("No advisories found for %s", app.Name))
return
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{
i18n.G("RECIPE"),
i18n.G("SERVICE"),
i18n.G("IMAGE"),
i18n.G("VERSION"),
i18n.G("DETAILS"),
}
table.
Headers(headers...).
Rows(rows...)
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
upgradeTo := ac.GetRecipeUpgradeTo(deployMeta.Version)
log.Infof(i18n.G("Security advisories found for %s, upgrade recipe from %s to %s"), app.Recipe.Name, deployMeta.Version, upgradeTo)
},
}
+1
View File
@@ -311,6 +311,7 @@ Config:
app.AppVolumeCommand,
app.AppLabelsCommand,
app.AppEnvCommand,
app.AppVulncheckCommand,
)
if err := rootCmd.Execute(); err != nil {
+18
View File
@@ -34,6 +34,24 @@ func SmallSHA(hash string) string {
return hash[:8]
}
// TrimNs strips any repository namespace/org and digest from an image string.
//
// Examples:
//
// "n8nio/n8n:1.81.2" -> "n8n:1.81.2"
// "nginx:1.29.0@sha256:3ab4ed..." -> "nginx:1.29.0"
// "docker.io/library/nginx:latest" -> "nginx:latest"
func TrimNs(image string) string {
// 1. Remove digest suffix if present (@sha256:...)
if idx := strings.Index(image, "@"); idx != -1 {
image = image[:idx]
}
// 2. Remove registry domain and org/namespace path (keep only the last element)
parts := strings.Split(image, "/")
return parts[len(parts)-1]
}
// RemoveSha remove image sha from a string that are added in some docker outputs
func RemoveSha(str string) string {
return strings.Split(str, "@")[0]
+92
View File
@@ -0,0 +1,92 @@
package vulncheck
import (
"encoding/json"
"os"
"path"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/git"
)
type Advisory struct {
Id string `json:"ccsa_id"`
Recipe string `json:"recipe"`
AdvisoryDate string `json:"date"`
RecipeVersions []string `json:"versions"`
RecipesUpgradeTo []string `json:"upgrade_to"`
Images []string `json:"images"`
ShortDesc string `json:"short"`
Details string `json:"description"`
Url string `json:"url"`
}
type AdvisoryClient struct {
Advisories []Advisory
}
func (a *AdvisoryClient) Init() {
dir := path.Join(config.ABRA_DIR, "security-advisories")
if _, err := os.Stat(dir); os.IsNotExist(err) {
log.Infof("Syncing abra security advisories...")
err := git.Clone(dir, "https://git.coopcloud.tech/sixsmith/security-advisories.git")
if err != nil {
log.Debugf("Error cloning security advisories repo: ", err)
log.Fatalf("Error cloning security advisories repo: ", err)
}
}
afp := path.Join(dir, "CCSA.json")
err := a.loadAdvisoriesFromFile(afp)
if err != nil {
log.Fatalf("Error loading advisories: %v\n", err)
}
}
func (a *AdvisoryClient) loadAdvisoriesFromFile(filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
var advisories []Advisory
decoder := json.NewDecoder(file)
err = decoder.Decode(&advisories)
if err != nil {
return err
}
a.Advisories = advisories
return nil
}
func (a *AdvisoryClient) GetAdvisoriesForImage(image string) []Advisory {
var relevantAdvisories []Advisory
for _, advisory := range a.Advisories {
for _, img := range advisory.Images {
if formatter.TrimNs(image) == formatter.TrimNs(img) {
relevantAdvisories = append(relevantAdvisories, advisory)
break
}
}
}
return relevantAdvisories
}
func (a *AdvisoryClient) GetRecipeUpgradeTo(recipeVer string) string {
for _, advisory := range a.Advisories {
for _, ver := range advisory.RecipeVersions {
if recipeVer == ver {
if len(advisory.RecipesUpgradeTo) > 0 {
return advisory.RecipesUpgradeTo[0]
}
}
}
}
return ""
}
+1
View File
@@ -0,0 +1 @@
package vulncheck