forked from toolshed/abra
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f648a94a25 | ||
|
|
12c29fd0be | ||
|
|
ca39ff6e29 |
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -311,6 +311,7 @@ Config:
|
||||
app.AppVolumeCommand,
|
||||
app.AppLabelsCommand,
|
||||
app.AppEnvCommand,
|
||||
app.AppVulncheckCommand,
|
||||
)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.31.5
|
||||
image: nginx:1.31.6
|
||||
secrets:
|
||||
- test_pass_one
|
||||
- test_pass_two
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package vulncheck
|
||||
@@ -3,7 +3,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.31.5
|
||||
image: nginx:1.31.6
|
||||
networks:
|
||||
- proxy
|
||||
deploy:
|
||||
|
||||
Reference in New Issue
Block a user