diff --git a/internal/handler/eolfunctions.go b/internal/handler/eolfunctions.go new file mode 100644 index 0000000..ea5bf29 --- /dev/null +++ b/internal/handler/eolfunctions.go @@ -0,0 +1,192 @@ +package handler + +import ( + "encoding/json" + "fmt" + "github.com/uselagoon/lagoon/services/insights-handler/internal/lagoonclient" + "io/ioutil" + "net/http" + "os" + "time" +) + +// eolfunctions.go contains all the basic functionality for checking key facts' end of life status against https://endoflife.date/docs/api + +const eloCacheLocation = "/tmp/eol.cache" + +type PackageInfo struct { + Cycle string `json:"cycle"` + ReleaseDate string `json:"releaseDate"` + EOL string `json:"eol"` + Latest string `json:"latest"` + LatestReleaseDate string `json:"latestReleaseDate"` + Link string `json:"link"` + LTS bool `json:"lts"` +} + +type EOLData struct { + Packages map[string][]PackageInfo + CacheLocation string + ComparisonDate *time.Time +} + +type NewEOLDataArgs struct { + Packages []string + CacheLocation string + PreventCacheRefresh bool + ForceCacheRefresh bool +} + +// NewEOLData creates a new EOLData struct with end-of-life information for the provided Packages. +func NewEOLData(args NewEOLDataArgs) (*EOLData, error) { + + // basic assertions of logic + if args.ForceCacheRefresh && args.PreventCacheRefresh { + return nil, fmt.Errorf("you cannot Force Cache Refresh AND Prevent Cache Refresh") + } + + packages := args.Packages + cacheLocation := args.CacheLocation + data := &EOLData{ + CacheLocation: cacheLocation, + } + + // Check if cache file exists + if _, err := os.Stat(data.CacheLocation); err == nil || args.ForceCacheRefresh { + // Cache file exists, load data from file + if err := loadDataFromFile(data.CacheLocation, data); err != nil { + return nil, err + } + } else if os.IsNotExist(err) { + if args.PreventCacheRefresh { + return nil, fmt.Errorf("cache not found and Prevent Cache Refresh enabled") + } + // Cache file does not exist, fetch data and write to file + endOfLifeInfo := GetEndOfLifeInfo(packages) + data.Packages = endOfLifeInfo + + // Write to cache file + if err := writeDataToFile(data.CacheLocation, data); err != nil { + return nil, err + } + } else { + // Some other error occurred + return nil, err + } + + timeNow := time.Now() + data.ComparisonDate = &timeNow + + return data, nil +} + +// GenerateProblemsForPackages takes in a map of package names to version (strings) and returns a set of outdated +func (t *EOLData) GenerateProblemsForPackages(packages map[string]string, environmentId int, service string) ([]lagoonclient.LagoonProblem, error) { + var problems []lagoonclient.LagoonProblem + now := time.Now() + for packageName, version := range packages { + packageData, err := t.EolDataForPackage(packageName, version) + if err == nil { + date, err := time.Parse("2006-01-02", packageData.EOL) + if err != nil { + return problems, fmt.Errorf("Unable to parse date '%v' for package information", packageData.EOL) + } + if t.ComparisonDate != nil { + date = *t.ComparisonDate + } + if date.Before(now) { + problems = append(problems, lagoonclient.LagoonProblem{ + Environment: environmentId, + Identifier: fmt.Sprintf("EOL-%v-%v", packageName, version), + Version: version, + FixedVersion: "", + Source: "insights-handler-EOLData", + Service: service, + Data: "{}", + Severity: "", + SeverityScore: 0, + AssociatedPackage: "", + Description: fmt.Sprintf("Package '%v' is at End-of-life as of '%v'", packageName, packageData.EOL), + Links: "", + }) + } + } + } + return problems, nil +} + +func (t *EOLData) EolDataForPackage(packageName, ver string) (PackageInfo, error) { + if packages := t.Packages[packageName]; packages != nil { + for _, p := range packages { + if p.Cycle == ver { + return p, nil + } + } + } else { + return PackageInfo{}, fmt.Errorf("Package '%v' not found in EOL list", packageName) + } + return PackageInfo{}, fmt.Errorf("Package '%v' version '%v' not found in EOL list", packageName, ver) +} + +func GetEndOfLifeInfo(packageNames []string) map[string][]PackageInfo { + endOfLifeInfo := make(map[string][]PackageInfo) + + for _, packageName := range packageNames { + url := fmt.Sprintf("https://endoflife.date/api/%s.json", packageName) + response, err := http.Get(url) + if err != nil { + fmt.Printf("Error getting end of life info for %s: %v\n", packageName, err) + continue + } + defer response.Body.Close() + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + fmt.Printf("Error reading response body for %s: %v\n", packageName, err) + continue + } + + var data []PackageInfo + if err := json.Unmarshal(body, &data); err != nil { + fmt.Printf("error parsing JSON for %s: %v\n", packageName, err) + continue + } + + // Assuming the API returns an array of PackageInfo + if len(data) > 0 { + endOfLifeInfo[packageName] = data // Assuming we're interested in the first entry + } + } + + return endOfLifeInfo +} + +// loadDataFromFile loads data from a file into an EOLData struct. +func loadDataFromFile(filename string, data *EOLData) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + decoder := json.NewDecoder(file) + if err := decoder.Decode(data); err != nil { + return err + } + + return nil +} + +// writeDataToFile writes data from an EOLData struct to a file. +func writeDataToFile(filename string, data *EOLData) error { + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + + if err := ioutil.WriteFile(filename, jsonData, 0644); err != nil { + return err + } + + return nil +} diff --git a/internal/handler/eolfunctions_test.go b/internal/handler/eolfunctions_test.go new file mode 100644 index 0000000..fffca43 --- /dev/null +++ b/internal/handler/eolfunctions_test.go @@ -0,0 +1,327 @@ +package handler + +import ( + "fmt" + "github.com/uselagoon/lagoon/services/insights-handler/internal/lagoonclient" + "os" + "path/filepath" + "reflect" + "testing" + "time" +) + +func TestGetEndOfLifeInfo(t *testing.T) { + type args struct { + packageNames []string + } + tests := []struct { + name string + args args + wantResponse bool + }{ + { + name: "Get alpine information", + args: args{packageNames: []string{ + "alpine", + "ubuntu", + }}, + wantResponse: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetEndOfLifeInfo(tt.args.packageNames) + + if tt.wantResponse == true { + for _, name := range tt.args.packageNames { + if len(got[name]) == 0 { + t.Errorf("Expected data for package %v, got nothing", name) + } + } + } + }) + } +} + +func TestNewEOLData(t *testing.T) { + type args struct { + EolArgs NewEOLDataArgs + } + tests := []struct { + name string + args args + want *EOLData + wantErr bool + }{ + { + name: "Test No Cache", + args: args{ + EolArgs: NewEOLDataArgs{ + Packages: []string{ + "alpine", + }, + CacheLocation: "testnocache.json", + }, + }, + }, + { + name: "Test Assertion Failure", + wantErr: true, + args: args{ + EolArgs: NewEOLDataArgs{ + Packages: []string{ + "alpine", + }, + CacheLocation: "testnocache.json", + ForceCacheRefresh: true, + PreventCacheRefresh: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Let's set up a temporary location to save the incoming data + dir, err := os.MkdirTemp("", "*-test") + if err != nil { + t.Errorf("Unable to create test directory") + return + } + defer func(path string) { + err := os.RemoveAll(path) + if err != nil { + fmt.Println("Unable to remove directory: ", path) + } + }(dir) + tt.args.EolArgs.CacheLocation = filepath.Join(dir, tt.args.EolArgs.CacheLocation) + got, err := NewEOLData(tt.args.EolArgs) + if (err != nil) != tt.wantErr { + t.Errorf("NewEOLData() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { // We've got an error, and probably don't need to report it, 'cause it was expected + return + } + if len(got.Packages[tt.args.EolArgs.Packages[0]]) == 0 { + t.Errorf("Could not find any Packages for package '%v'\n", tt.args.EolArgs.Packages[0]) + } + + }) + } +} + +func TestNewEOLDataWithExistingCache(t *testing.T) { + type args struct { + EolArgs NewEOLDataArgs + } + tests := []struct { + name string + args args + want *EOLData + wantErr bool + }{ + { + name: "Test No Cache", + args: args{ + EolArgs: NewEOLDataArgs{ + Packages: []string{ + "alpine", + }, + CacheLocation: "testassets/EOLdata/testnocache.json", + }, + }, + }, + { + name: "Test Nonexistent cache failure", + wantErr: true, + args: args{ + EolArgs: NewEOLDataArgs{ + Packages: []string{ + "alpine", + }, + CacheLocation: "doesntexist.json", + ForceCacheRefresh: false, + PreventCacheRefresh: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Let's set up a temporary location to save the incoming data + got, err := NewEOLData(tt.args.EolArgs) + if (err != nil) != tt.wantErr { + t.Errorf("NewEOLData() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil { + return + } + if len(got.Packages[tt.args.EolArgs.Packages[0]]) == 0 { + t.Errorf("Could not find any Packages for package '%v'\n", tt.args.EolArgs.Packages[0]) + } + }) + } +} + +func TestEOLData_EolDataForPackage(t1 *testing.T) { + type fields struct { + Packages []string + CacheLocation string + } + type args struct { + packageName string + ver string + } + tests := []struct { + name string + fields fields + args args + want PackageInfo + wantErr bool + }{ + { + name: "Find basic package", + fields: fields{ + Packages: []string{"alpine"}, + CacheLocation: "testassets/EOLdata/testnocache.json", + }, + args: args{ + packageName: "alpine", + ver: "3.16", + }, + want: PackageInfo{ + Cycle: "3.16", + ReleaseDate: "2022-05-23", + EOL: "2024-05-23", + Latest: "3.16.9", + LatestReleaseDate: "2024-01-26", + Link: "https://alpinelinux.org/posts/Alpine-3.16.9-3.17.7-3.18.6-released.html", + LTS: false, + }, + }, + { + name: "No match", + fields: fields{ + Packages: []string{"alpine"}, + CacheLocation: "testassets/EOLdata/testnocache.json", + }, + args: args{ + packageName: "NOMATCH", + ver: "1.1", + }, + want: PackageInfo{}, + wantErr: true, + }, + } + for _, tt := range tests { + t1.Run(tt.name, func(t1 *testing.T) { + t, err := NewEOLData(NewEOLDataArgs{ + Packages: tt.fields.Packages, + CacheLocation: tt.fields.CacheLocation, + PreventCacheRefresh: true, + ForceCacheRefresh: false, + }) + + if err != nil { + //This shouldn't happen, so let's just die + t1.Errorf("Issue with checking packages - have to exit") + } + + got, err := t.EolDataForPackage(tt.args.packageName, tt.args.ver) + if (err != nil) != tt.wantErr { + t1.Errorf("EolDataForPackage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t1.Errorf("EolDataForPackage() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEOLData_GenerateProblemsForPackages(t1 *testing.T) { + type initData struct { + Packages []string + InitTime time.Time + } + type args struct { + packages map[string]string + environmentId int + service string + } + tests := []struct { + name string + initData initData + args args + want []lagoonclient.LagoonProblem + wantErr bool + }{ + { + name: "Check > EOL", + initData: initData{ + Packages: []string{"alpine"}, + InitTime: time.Now(), + }, + args: args{ + packages: map[string]string{ + "alpine": "3.19", + }, + environmentId: 0, + service: "", + }, + wantErr: false, + want: nil, + }, + { + name: "Check < EOL", + initData: initData{ + Packages: []string{"alpine"}, + InitTime: time.Date(1990, time.January, 1, 1, 1, 1, 1, time.Local), + }, + args: args{ + packages: map[string]string{ + "alpine": "3.19", + }, + environmentId: 0, + service: "", + }, + wantErr: false, + want: []lagoonclient.LagoonProblem{ + lagoonclient.LagoonProblem{ + Environment: 0, + Identifier: fmt.Sprintf("EOL-%v-%v", "alpine", "3.19"), + Version: "3.19", + FixedVersion: "", + Source: "insights-handler-EOLData", + Service: "", + Data: "{}", + Severity: "", + SeverityScore: 0, + AssociatedPackage: "", + Description: fmt.Sprintf("Package '%v' is at End-of-life as of '%v'", "alpine", "2025-11-01"), + Links: "", + }, + }, + }, + } + for _, tt := range tests { + t1.Run(tt.name, func(t1 *testing.T) { + t, err := NewEOLData(NewEOLDataArgs{ + Packages: tt.initData.Packages, + CacheLocation: "testassets/EOLdata/cachedata.json", + PreventCacheRefresh: true, + ForceCacheRefresh: false, + }) + + got, err := t.GenerateProblemsForPackages(tt.args.packages, tt.args.environmentId, tt.args.service) + if (err != nil) != tt.wantErr { + t1.Errorf("GenerateProblemsForPackages() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t1.Errorf("GenerateProblemsForPackages() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/handler/insightsParserFilter.go b/internal/handler/insightsParserFilter.go index 8c3de48..b3cee18 100644 --- a/internal/handler/insightsParserFilter.go +++ b/internal/handler/insightsParserFilter.go @@ -4,56 +4,76 @@ import ( "fmt" cdx "github.com/CycloneDX/cyclonedx-go" "github.com/Khan/genqlient/graphql" + "github.com/uselagoon/lagoon/services/insights-handler/internal/lagoonclient" "log/slog" ) -func processSbomInsightsData(h *Messaging, insights InsightsData, v string, apiClient graphql.Client, resource ResourceDestination) ([]LagoonFact, string, error) { +func processSbomInsightsData(h *Messaging, insights InsightsData, v string, apiClient graphql.Client, resource ResourceDestination) ([]LagoonFact, []lagoonclient.LagoonProblem, string, error) { source := fmt.Sprintf("insights:sbom:%s", resource.Service) logger := slog.With("ProjectName", resource.Project, "EnvironmentName", resource.Environment, "Source", source) + // ret values + var problemSlice []lagoonclient.LagoonProblem + if insights.InsightsType != Sbom { - return []LagoonFact{}, "", nil + return []LagoonFact{}, problemSlice, "", nil } bom, err := getBOMfromPayload(v) if err != nil { - return []LagoonFact{}, "", err + return []LagoonFact{}, problemSlice, "", err } // Determine lagoon resource destination _, environment, apiErr := determineResourceFromLagoonAPI(apiClient, resource) if apiErr != nil { - return nil, "", apiErr + return nil, problemSlice, "", apiErr } // we process the SBOM here - // TODO: This should actually live in its own function somewhere else. if h.ProblemsFromSBOM == true { isAlive, err := IsTrivyServerIsAlive(h.TrivyServerEndpoint) if err != nil { - return nil, "", fmt.Errorf("trivy server not alive: %v", err.Error()) + return nil, problemSlice, "", fmt.Errorf("trivy server not alive: %v", err.Error()) } else { logger.Debug("Trivy is reachable") } if isAlive { - err = SbomToProblems(apiClient, h.TrivyServerEndpoint, "/tmp/", environment.Id, resource.Service, *bom) + problemSlice, err = SbomToProblems(apiClient, h.TrivyServerEndpoint, "/tmp/", environment.Id, resource.Service, *bom) } if err != nil { - return nil, "", err + return nil, problemSlice, "", err } } // Process SBOM into facts facts := processFactsFromSBOM(logger, bom.Components, environment.Id, source) + // Here, before we filter things, we run our facts through EOL data + problemSlice = append(problemSlice, lagoonclient.LagoonProblem{ + Id: 0, + Environment: environment.Id, + Identifier: "Testing EOL", + Version: "1.1", + FixedVersion: "1.2", + Source: "", + Service: "", + Data: "", + Severity: "", + SeverityScore: 0, + AssociatedPackage: "", + Description: "", + Links: "", + }) + facts, err = KeyFactsFilter(facts) if err != nil { - return nil, "", err + return nil, problemSlice, "", err } if len(facts) == 0 { - return nil, "", fmt.Errorf("no facts to process") + return nil, problemSlice, "", fmt.Errorf("no facts to process") } //log.Printf("Successfully decoded SBOM of image %s with %s, found %d for '%s:%s'", bom.Metadata.Component.Name, (*bom.Metadata.Tools)[0].Name, len(*bom.Components), resource.Project, resource.Environment) @@ -63,7 +83,7 @@ func processSbomInsightsData(h *Messaging, insights InsightsData, v string, apiC "Length", len(*bom.Components), ) - return facts, source, nil + return facts, problemSlice, source, nil } func processFactsFromSBOM(logger *slog.Logger, facts *[]cdx.Component, environmentId int, source string) []LagoonFact { diff --git a/internal/handler/main.go b/internal/handler/main.go index 1bf13a4..637b076 100644 --- a/internal/handler/main.go +++ b/internal/handler/main.go @@ -282,15 +282,17 @@ func (t *authedTransport) RoundTrip(req *http.Request) (*http.Response, error) { } type LagoonSourceFactMap map[string][]LagoonFact +type LagoonSourceProblemMap map[string][]lagoonclient.LagoonProblem // Incoming payload may contain facts or problems, so we need to handle these differently -func (h *Messaging) gatherFactsFromInsightData(incoming *InsightsMessage, resource ResourceDestination, insights InsightsData) ([]LagoonSourceFactMap, error) { +func (h *Messaging) gatherFactsFromInsightData(incoming *InsightsMessage, resource ResourceDestination, insights InsightsData) ([]LagoonSourceFactMap, []LagoonSourceProblemMap, error) { apiClient := h.getApiClient() // Here we collect all source fact maps before writing them _once_ lagoonSourceFactMapCollection := []LagoonSourceFactMap{} + lagoonSourceProblemMapCollection := []LagoonSourceProblemMap{} if resource.Project == "" && resource.Environment == "" { - return lagoonSourceFactMapCollection, fmt.Errorf("no resource definition labels could be found in payload (i.e. lagoon.sh/project or lagoon.sh/environment)") + return lagoonSourceFactMapCollection, lagoonSourceProblemMapCollection, fmt.Errorf("no resource definition labels could be found in payload (i.e. lagoon.sh/project or lagoon.sh/environment)") } slog.Debug("Processing data", "InputPayload", insights.InputPayload, "LagoonType", insights.LagoonType, "InsightsType", insights.InsightsType) @@ -303,6 +305,7 @@ func (h *Messaging) gatherFactsFromInsightData(incoming *InsightsMessage, resour break } lagoonSourceFactMap := LagoonSourceFactMap{} + lagoonSourceProblemMap := LagoonSourceProblemMap{} // since we only have two parser filter types now - let's explicitly call them // First we call the image inspect processor, in case there's anything there @@ -318,46 +321,19 @@ func (h *Messaging) gatherFactsFromInsightData(incoming *InsightsMessage, resour // Then we call the SBOM processor, in case we're dealing with this type if insights.InsightsType == Sbom { - result, source, err := processSbomInsightsData(h, insights, binaryPayload, apiClient, resource) + facts, problems, source, err := processSbomInsightsData(h, insights, binaryPayload, apiClient, resource) if err != nil { slog.Error("Error running filter", "error", err.Error()) } - lagoonSourceFactMap[source] = result + lagoonSourceFactMap[source] = facts + lagoonSourceProblemMap[source] = problems } lagoonSourceFactMapCollection = append(lagoonSourceFactMapCollection, lagoonSourceFactMap) + lagoonSourceProblemMapCollection = append(lagoonSourceProblemMapCollection, lagoonSourceProblemMap) } - return lagoonSourceFactMapCollection, nil -} - -func trivySBOMProcessing(apiClient graphql.Client, trivyServerEndpoint string, resource ResourceDestination, payload string) error { - - bom, err := getBOMfromPayload(payload) - if err != nil { - return err - } - - // Determine lagoon resource destination - _, environment, apiErr := determineResourceFromLagoonAPI(apiClient, resource) - if apiErr != nil { - return apiErr - } - - // we process the SBOM here - isAlive, err := IsTrivyServerIsAlive(trivyServerEndpoint) - if err != nil { - return fmt.Errorf("trivy server not alive: %v", err.Error()) - } else { - slog.Debug("Trivy is reachable") - } - if isAlive { - err = SbomToProblems(apiClient, trivyServerEndpoint, "/tmp/", environment.Id, resource.Service, *bom) - } - if err != nil { - return err - } - return nil + return lagoonSourceFactMapCollection, lagoonSourceProblemMapCollection, nil } // sendResultsetToLagoon will send results as facts to the lagoon api after processing via a parser filter @@ -423,6 +399,26 @@ func (h *Messaging) deleteExistingFactsBySource(apiClient graphql.Client, enviro return nil } +func (h *Messaging) SendProblemSliceToLagoon(result []lagoonclient.LagoonProblem, resource ResourceDestination, source string) error { + + apiClient := h.getApiClient() + _, environment, apiErr := determineResourceFromLagoonAPI(apiClient, resource) + + if apiErr != nil { + return apiErr + } + + slog.Info(fmt.Sprintf("Found the following problems for Project '%v', environment '%v', source '%v", resource.Project, resource.Environment, source)) + for _, prob := range result { + slog.Info(fmt.Sprintf("%v:%v", prob.Identifier, prob.Version)) + } + err := writeProblemsArrayToApi(apiClient, environment.Id, problemSource, resource.Service, result) + if err != nil { + return fmt.Errorf("unable to write problems to api: %v", err.Error()) + } + return nil +} + func (h *Messaging) getApiClient() graphql.Client { apiClient := graphql.NewClient(h.LagoonAPI.Endpoint, &http.Client{Transport: &authedTransport{wrapped: http.DefaultTransport, h: h}}) return apiClient diff --git a/internal/handler/messaging.go b/internal/handler/messaging.go index 836877a..f4b9154 100644 --- a/internal/handler/messaging.go +++ b/internal/handler/messaging.go @@ -135,7 +135,7 @@ func (h *Messaging) processMessageQueue(message mq.Message) { insights.InsightsType != Direct { slog.Error("only 'sbom', 'direct', 'raw', and 'image' types are currently supported for api processing") } else { - lagoonSourceFactMapCollection, err := h.gatherFactsFromInsightData(incoming, resource, insights) + lagoonSourceFactMapCollection, lagoonSourceProblemMapCollection, err := h.gatherFactsFromInsightData(incoming, resource, insights) if err != nil { slog.Error("Unable to gather facts from incoming data", "Error", err.Error()) @@ -154,7 +154,16 @@ func (h *Messaging) processMessageQueue(message mq.Message) { } } } - + for _, lspm := range lagoonSourceProblemMapCollection { + for sourceName, problems := range lspm { + err := h.SendProblemSliceToLagoon(problems, resource, sourceName) + if err != nil { + slog.Error("Unable to write problems to api", "Error", err.Error()) + rejectMessage(false) + return + } + } + } } } acknowledgeMessage() diff --git a/internal/handler/testassets/EOLdata/cachedata.json b/internal/handler/testassets/EOLdata/cachedata.json new file mode 100644 index 0000000..223702b --- /dev/null +++ b/internal/handler/testassets/EOLdata/cachedata.json @@ -0,0 +1,123 @@ +{ + "Packages": { + "alpine": [ + { + "cycle": "3.19", + "releaseDate": "2023-12-07", + "eol": "2025-11-01", + "latest": "3.19.1", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.19.1-released.html", + "lts": false + }, + { + "cycle": "3.18", + "releaseDate": "2023-05-09", + "eol": "2025-05-09", + "latest": "3.18.6", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.16.9-3.17.7-3.18.6-released.html", + "lts": false + }, + { + "cycle": "3.17", + "releaseDate": "2022-11-22", + "eol": "2024-11-22", + "latest": "3.17.7", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.16.9-3.17.7-3.18.6-released.html", + "lts": false + }, + { + "cycle": "3.16", + "releaseDate": "2022-05-23", + "eol": "2024-05-23", + "latest": "3.16.9", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.16.9-3.17.7-3.18.6-released.html", + "lts": false + }, + { + "cycle": "3.15", + "releaseDate": "2021-11-24", + "eol": "2023-11-01", + "latest": "3.15.11", + "latestReleaseDate": "2023-11-30", + "link": "https://alpinelinux.org/posts/Alpine-3.15.10-3.16.7-3.17.5-3.18.3-released.html", + "lts": false + }, + { + "cycle": "3.14", + "releaseDate": "2021-06-15", + "eol": "2023-05-01", + "latest": "3.14.10", + "latestReleaseDate": "2023-03-29", + "link": "https://alpinelinux.org/posts/Alpine-3.14.10-3.15.8-3.16.5-released.html", + "lts": false + }, + { + "cycle": "3.13", + "releaseDate": "2021-01-14", + "eol": "2022-11-01", + "latest": "3.13.12", + "latestReleaseDate": "2022-08-09", + "link": "https://alpinelinux.org/posts/Alpine-3.12.12-3.13.10-3.14.6-3.15.4-released.html", + "lts": false + }, + { + "cycle": "3.12", + "releaseDate": "2020-05-29", + "eol": "2022-05-01", + "latest": "3.12.12", + "latestReleaseDate": "2022-04-04", + "link": "https://alpinelinux.org/posts/Alpine-3.12.12-3.13.10-3.14.6-3.15.4-released.html", + "lts": false + }, + { + "cycle": "3.11", + "releaseDate": "2019-12-19", + "eol": "2021-11-01", + "latest": "3.11.13", + "latestReleaseDate": "2021-11-12", + "link": "https://alpinelinux.org/posts/Alpine-3.11.13-3.12.9-3.13.7-released.html", + "lts": false + }, + { + "cycle": "3.10", + "releaseDate": "2019-06-19", + "eol": "2021-05-01", + "latest": "3.10.9", + "latestReleaseDate": "2021-04-14", + "link": "https://alpinelinux.org/posts/Alpine-3.10.9-3.11.11-3.12.7-released.html", + "lts": false + }, + { + "cycle": "3.9", + "releaseDate": "2019-01-29", + "eol": "2020-11-01", + "latest": "3.9.6", + "latestReleaseDate": "2020-04-23", + "link": "https://alpinelinux.org/posts/Alpine-3.9.6-and-3.10.5-released.html", + "lts": false + }, + { + "cycle": "3.8", + "releaseDate": "2018-06-26", + "eol": "2020-05-01", + "latest": "3.8.5", + "latestReleaseDate": "2020-01-23", + "link": "https://git.alpinelinux.org/aports/log/?h=3.8-stable", + "lts": false + }, + { + "cycle": "3.7", + "releaseDate": "2017-11-30", + "eol": "2019-11-01", + "latest": "3.7.3", + "latestReleaseDate": "2019-03-06", + "link": "https://git.alpinelinux.org/aports/log/?h=3.7-stable", + "lts": false + } + ] + } +} \ No newline at end of file diff --git a/internal/handler/testassets/EOLdata/testnocache.json b/internal/handler/testassets/EOLdata/testnocache.json new file mode 100644 index 0000000..223702b --- /dev/null +++ b/internal/handler/testassets/EOLdata/testnocache.json @@ -0,0 +1,123 @@ +{ + "Packages": { + "alpine": [ + { + "cycle": "3.19", + "releaseDate": "2023-12-07", + "eol": "2025-11-01", + "latest": "3.19.1", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.19.1-released.html", + "lts": false + }, + { + "cycle": "3.18", + "releaseDate": "2023-05-09", + "eol": "2025-05-09", + "latest": "3.18.6", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.16.9-3.17.7-3.18.6-released.html", + "lts": false + }, + { + "cycle": "3.17", + "releaseDate": "2022-11-22", + "eol": "2024-11-22", + "latest": "3.17.7", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.16.9-3.17.7-3.18.6-released.html", + "lts": false + }, + { + "cycle": "3.16", + "releaseDate": "2022-05-23", + "eol": "2024-05-23", + "latest": "3.16.9", + "latestReleaseDate": "2024-01-26", + "link": "https://alpinelinux.org/posts/Alpine-3.16.9-3.17.7-3.18.6-released.html", + "lts": false + }, + { + "cycle": "3.15", + "releaseDate": "2021-11-24", + "eol": "2023-11-01", + "latest": "3.15.11", + "latestReleaseDate": "2023-11-30", + "link": "https://alpinelinux.org/posts/Alpine-3.15.10-3.16.7-3.17.5-3.18.3-released.html", + "lts": false + }, + { + "cycle": "3.14", + "releaseDate": "2021-06-15", + "eol": "2023-05-01", + "latest": "3.14.10", + "latestReleaseDate": "2023-03-29", + "link": "https://alpinelinux.org/posts/Alpine-3.14.10-3.15.8-3.16.5-released.html", + "lts": false + }, + { + "cycle": "3.13", + "releaseDate": "2021-01-14", + "eol": "2022-11-01", + "latest": "3.13.12", + "latestReleaseDate": "2022-08-09", + "link": "https://alpinelinux.org/posts/Alpine-3.12.12-3.13.10-3.14.6-3.15.4-released.html", + "lts": false + }, + { + "cycle": "3.12", + "releaseDate": "2020-05-29", + "eol": "2022-05-01", + "latest": "3.12.12", + "latestReleaseDate": "2022-04-04", + "link": "https://alpinelinux.org/posts/Alpine-3.12.12-3.13.10-3.14.6-3.15.4-released.html", + "lts": false + }, + { + "cycle": "3.11", + "releaseDate": "2019-12-19", + "eol": "2021-11-01", + "latest": "3.11.13", + "latestReleaseDate": "2021-11-12", + "link": "https://alpinelinux.org/posts/Alpine-3.11.13-3.12.9-3.13.7-released.html", + "lts": false + }, + { + "cycle": "3.10", + "releaseDate": "2019-06-19", + "eol": "2021-05-01", + "latest": "3.10.9", + "latestReleaseDate": "2021-04-14", + "link": "https://alpinelinux.org/posts/Alpine-3.10.9-3.11.11-3.12.7-released.html", + "lts": false + }, + { + "cycle": "3.9", + "releaseDate": "2019-01-29", + "eol": "2020-11-01", + "latest": "3.9.6", + "latestReleaseDate": "2020-04-23", + "link": "https://alpinelinux.org/posts/Alpine-3.9.6-and-3.10.5-released.html", + "lts": false + }, + { + "cycle": "3.8", + "releaseDate": "2018-06-26", + "eol": "2020-05-01", + "latest": "3.8.5", + "latestReleaseDate": "2020-01-23", + "link": "https://git.alpinelinux.org/aports/log/?h=3.8-stable", + "lts": false + }, + { + "cycle": "3.7", + "releaseDate": "2017-11-30", + "eol": "2019-11-01", + "latest": "3.7.3", + "latestReleaseDate": "2019-03-06", + "link": "https://git.alpinelinux.org/aports/log/?h=3.7-stable", + "lts": false + } + ] + } +} \ No newline at end of file diff --git a/internal/handler/trivyProcessing.go b/internal/handler/trivyProcessing.go index 65035be..45c57f5 100644 --- a/internal/handler/trivyProcessing.go +++ b/internal/handler/trivyProcessing.go @@ -17,10 +17,10 @@ import ( const problemSource = "insights-handler-trivy" -func SbomToProblems(apiClient graphql.Client, trivyRemoteAddress string, bomWriteDirectory string, environmentId int, service string, sbom cdx.BOM) error { +func SbomToProblems(apiClient graphql.Client, trivyRemoteAddress string, bomWriteDirectory string, environmentId int, service string, sbom cdx.BOM) ([]lagoonclient.LagoonProblem, error) { problemsArray, err := executeProcessingTrivy(trivyRemoteAddress, bomWriteDirectory, sbom) if err != nil { - return fmt.Errorf("unable to execute trivy processing: %v", err.Error()) + return problemsArray, fmt.Errorf("unable to execute trivy processing: %v", err.Error()) } for i := 0; i < len(problemsArray); i++ { @@ -29,11 +29,7 @@ func SbomToProblems(apiClient graphql.Client, trivyRemoteAddress string, bomWrit problemsArray[i].Source = problemSource } - err = writeProblemsArrayToApi(apiClient, environmentId, problemSource, service, problemsArray) - if err != nil { - return fmt.Errorf("unable to execute trivy processing- writing problems to api: %v", err.Error()) - } - return nil + return problemsArray, nil } func convertBOMToProblemsArray(environment int, source string, service string, bom cdx.BOM) ([]lagoonclient.LagoonProblem, error) {