diff --git a/policy/data/cache/cache.go b/policy/data/cache/cache.go deleted file mode 100644 index 409d0be..0000000 --- a/policy/data/cache/cache.go +++ /dev/null @@ -1,67 +0,0 @@ -package cache - -import ( - "context" - "encoding/json" - "sync" -) - -type QueryCache interface { - Read(ctx context.Context, query string, variables map[string]interface{}) ([]byte, error) - Write(ctx context.Context, query string, variables map[string]interface{}, res []byte) error -} - -type cacheKey struct { - query string - variables string -} - -type SimpleQueryCache struct { - mu sync.Mutex - cache map[cacheKey][]byte -} - -func NewQueryCache() SimpleQueryCache { - return SimpleQueryCache{ - cache: make(map[cacheKey][]byte), - } -} - -func getKey(query string, variables map[string]interface{}) (cacheKey, error) { - variablesJSON, err := json.Marshal(variables) - if err != nil { - return cacheKey{}, err - } - - return cacheKey{query: query, variables: string(variablesJSON)}, nil -} - -func (d *SimpleQueryCache) Read(ctx context.Context, query string, variables map[string]interface{}) ([]byte, error) { - key, err := getKey(query, variables) - if err != nil { - return nil, err - } - - d.mu.Lock() - defer d.mu.Unlock() - - if result, ok := d.cache[key]; ok { - return result, nil - } - - return nil, nil -} - -func (d *SimpleQueryCache) Write(ctx context.Context, query string, variables map[string]interface{}, res []byte) error { - key, err := getKey(query, variables) - if err != nil { - return err - } - - d.mu.Lock() - defer d.mu.Unlock() - - d.cache[key] = res - - return nil -} diff --git a/policy/data/convert.go b/policy/data/convert.go deleted file mode 100644 index b44dd0c..0000000 --- a/policy/data/convert.go +++ /dev/null @@ -1,105 +0,0 @@ -package data - -import ( - "github.com/atomist-skills/go-skill/policy/data/query/jynx" - "github.com/atomist-skills/go-skill/policy/types" -) - -func convertGraphqlToPackages(imagePackages jynx.ImagePackagesByDigest) ([]types.Package, map[string][]types.Vulnerability) { - var nonEmptyHistories []jynx.ImageHistory - for _, history := range imagePackages.ImageHistories { - if !history.EmptyLayer { - nonEmptyHistories = append(nonEmptyHistories, history) - } - } - - var pkgs []types.Package - var vulns = map[string][]types.Vulnerability{} - for _, p := range imagePackages.ImagePackages.Packages { - var locations []types.Location - for _, location := range p.Locations { - layerOrdinal := -1 - for _, layer := range imagePackages.ImageLayers.Layers { - if location.DiffId == layer.DiffId { - layerOrdinal = layer.Ordinal - break - } - } - - historyOrdinal := -1 - if len(nonEmptyHistories) > 0 && layerOrdinal > -1 { - historyOrdinal = nonEmptyHistories[layerOrdinal].Ordinal - } - - locations = append(locations, types.Location{ - Ordinal: historyOrdinal, - Path: location.Path, - }) - } - - var namespace string - if p.Package.Namespace == nil { - namespace = "" - } else { - namespace = *p.Package.Namespace - } - - vulnerabilities := convertToVulnerabilities(p.Package.Vulnerabilities) - - pkgs = append(pkgs, types.Package{ - Purl: p.Package.Purl, - Licenses: p.Package.Licenses, - Name: p.Package.Name, - Namespace: namespace, - Version: p.Package.Version, - Locations: locations, - }) - - vulns[p.Package.Purl] = vulnerabilities - } - - return pkgs, vulns -} - -func convertToVulnerabilities(vulnerabilities []jynx.Vulnerability) []types.Vulnerability { - var result []types.Vulnerability - - for _, vulnerability := range vulnerabilities { - vulnerabilityResult := types.Vulnerability{ - Cvss: types.Cvss{}, - PublishedAt: vulnerability.PublishedAt, - Source: vulnerability.Source, - SourceId: vulnerability.SourceID, - UpdatedAt: vulnerability.UpdatedAt, - VulnerableRange: vulnerability.VulnerableRange, - CisaExploited: vulnerability.CisaExploited, - } - - if vulnerability.Cvss.Score != nil { - vulnerabilityResult.Cvss.Score = *vulnerability.Cvss.Score - } - - if vulnerability.Cvss.Severity != nil { - vulnerabilityResult.Cvss.Severity = *vulnerability.Cvss.Severity - } - - if vulnerability.URL != nil { - vulnerabilityResult.Url = *vulnerability.URL - } - - if vulnerability.FixedBy != nil { - vulnerabilityResult.FixedBy = *vulnerability.FixedBy - } - - if vulnerability.Epss != nil { - vulnerabilityResult.Epss = &types.Epss{ - Percentile: vulnerability.Epss.Percentile, - Score: vulnerability.Epss.Score, - } - } - - result = append(result, vulnerabilityResult) - } - - return result -} diff --git a/policy/data/proxy/client.go b/policy/data/proxy/client.go deleted file mode 100644 index cd8bf93..0000000 --- a/policy/data/proxy/client.go +++ /dev/null @@ -1,82 +0,0 @@ -package proxy - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/atomist-skills/go-skill" - "github.com/atomist-skills/go-skill/policy/goals" - "github.com/atomist-skills/go-skill/policy/types" - "golang.org/x/oauth2" -) - -type ProxyClient struct { - httpClient http.Client - correlationId string - gqlUrl string - entitlementsUrl string -} - -func NewProxyClientFromSkillRequest(ctx context.Context, req skill.RequestContext) ProxyClient { - return NewProxyClient(ctx, req.Event.Urls.Graphql, req.Event.Urls.Entitlements, req.Event.Token, req.Event.ExecutionId) -} - -func NewProxyClient(ctx context.Context, graphqlUrl, entitlementsUrl, token, correlationId string) ProxyClient { - httpClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token, TokenType: "Bearer"}, - )) - - return ProxyClient{ - httpClient: *httpClient, - correlationId: correlationId, - gqlUrl: graphqlUrl, - entitlementsUrl: entitlementsUrl, - } -} - -func (c *ProxyClient) Evaluate(ctx context.Context, organization, teamId, url string, sbom *types.SBOM, args map[string]interface{}) (goals.EvaluationResult, error) { - preq := EvaluateRequest{ - EvaluateOptions: EvaluateOptions{ - Organization: organization, - WorkspaceId: teamId, - Parameters: args, - URLs: struct { - GraphQL string `json:"graphql"` - Entitlements string `json:"entitlements"` - }{GraphQL: c.gqlUrl, Entitlements: c.entitlementsUrl}, - }, - SBOM: sbom, - } - - data, err := json.Marshal(preq) - if err != nil { - return goals.EvaluationResult{}, err - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(data)) - if err != nil { - return goals.EvaluationResult{}, err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Add("X-Atomist-Correlation-Id", c.correlationId) - - res, err := c.httpClient.Do(req) - if err != nil { - return goals.EvaluationResult{}, err - } - - if res.StatusCode != http.StatusAccepted { - return goals.EvaluationResult{}, fmt.Errorf("unexpected status code: %d", res.StatusCode) - } - - defer res.Body.Close() //nolint:errcheck - var resp EvaluateResponse - err = json.NewDecoder(res.Body).Decode(&resp) - if err != nil { - return goals.EvaluationResult{}, err - } - return resp.Result, nil -} diff --git a/policy/data/proxy/types.go b/policy/data/proxy/types.go deleted file mode 100644 index 5f3a64a..0000000 --- a/policy/data/proxy/types.go +++ /dev/null @@ -1,25 +0,0 @@ -package proxy - -import ( - "github.com/atomist-skills/go-skill/policy/goals" - "github.com/atomist-skills/go-skill/policy/types" -) - -type EvaluateRequest struct { - EvaluateOptions - SBOM *types.SBOM `json:"sbom"` -} - -type EvaluateResponse struct { - Result goals.EvaluationResult `json:"result"` -} - -type EvaluateOptions struct { - Organization string `json:"organization"` - WorkspaceId string `json:"workspaceId"` - Parameters map[string]interface{} `json:"parameters"` - URLs struct { - GraphQL string `json:"graphql"` - Entitlements string `json:"entitlements"` - } -} diff --git a/policy/data/query/async.go b/policy/data/query/async.go deleted file mode 100644 index 6dd8d7a..0000000 --- a/policy/data/query/async.go +++ /dev/null @@ -1,165 +0,0 @@ -package query - -import ( - "bytes" - "context" - b64 "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" - - "github.com/atomist-skills/go-skill/policy/goals" - - "github.com/atomist-skills/go-skill" - "olympos.io/encoding/edn" -) - -const AsyncQueryName = "async-query" - -type ( - AsyncGraphQLQueryBody struct { - Query string `edn:"query"` - Variables map[edn.Keyword]interface{} `edn:"variables"` - BasisT *int64 `edn:"basis-t,omitempty"` - } - - AsyncQueryRequest struct { - Name string `edn:"name"` - Body AsyncGraphQLQueryBody `edn:"body"` - Metadata string `edn:"metadata"` - } - - AsyncQueryResponse struct { - Data edn.RawMessage `edn:"data"` - Errors []struct { - Message string `edn:"message"` - } - } - - AsyncResultMetadata struct { - EvaluationMetadata goals.EvaluationMetadata `edn:"evalMeta"` - AsyncQueryResults map[string]AsyncQueryResponse `edn:"results"` - InFlightQueryName string `edn:"query-name"` - } - - AsyncQueryClient struct { - multipleQuerySupport bool - log skill.Logger - url string - token string - evaluationMetadata goals.EvaluationMetadata - asyncResults map[string]AsyncQueryResponse - } -) - -func NewAsyncQueryClient( - multipleQuerySupport bool, - req skill.RequestContext, - evaluationMetadata goals.EvaluationMetadata, - asyncResults map[string]AsyncQueryResponse, -) AsyncQueryClient { - return AsyncQueryClient{ - multipleQuerySupport: multipleQuerySupport, - log: req.Log, - url: fmt.Sprintf("%s:enqueue", req.Event.Urls.Graphql), - token: req.Event.Token, - evaluationMetadata: evaluationMetadata, - asyncResults: asyncResults, - } -} - -func (ds AsyncQueryClient) Query(ctx context.Context, queryName string, query string, variables map[string]interface{}, output interface{}) (*QueryResponse, error) { - if existingResult, ok := ds.asyncResults[queryName]; ok { - return &QueryResponse{}, edn.Unmarshal(existingResult.Data, output) - } - - if len(ds.asyncResults) > 0 && !ds.multipleQuerySupport { - ds.log.Debugf("skipping async query for query %s due to lack of multipleQuerySupport", queryName) - return nil, nil // don't error, in case there is another applicable query executor down-chain - } - - metadata := AsyncResultMetadata{ - EvaluationMetadata: ds.evaluationMetadata, - AsyncQueryResults: ds.asyncResults, - InFlightQueryName: queryName, - } - metadataEdn, err := edn.Marshal(metadata) - if err != nil { - return nil, fmt.Errorf("failed to marshal metadata: %w", err) - } - - metadata64 := b64.StdEncoding.EncodeToString(metadataEdn) - if len(metadata64) > 1024 { - ds.log.Warnf("Skipping async data source usage for query %s due to metadata overflow!", queryName) - return nil, nil - } - - ednVariables := map[edn.Keyword]interface{}{} - for k, v := range variables { - ednVariables[edn.Keyword(k)] = v - } - - request := AsyncQueryRequest{ - Name: AsyncQueryName, - Body: AsyncGraphQLQueryBody{ - Query: query, - Variables: ednVariables, - BasisT: &ds.evaluationMetadata.SubscriptionBasisT, - }, - Metadata: metadata64, - } - - reqEdn, err := edn.Marshal(request) - if err != nil { - return nil, err - } - - ds.log.Infof("Async request: %s", string(reqEdn)) - - req, err := http.NewRequest(http.MethodPost, ds.url, bytes.NewBuffer(reqEdn)) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", "application/edn") - - authToken := ds.token - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken)) - - r, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer r.Body.Close() - - if r.StatusCode >= 500 { - ds.log.Infof("Retrying async request in 30s (failed with status %s)", r.Status) - time.Sleep(30 * time.Second) - - r, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer r.Body.Close() - } - - if r.StatusCode >= 400 { - buf := new(strings.Builder) - _, _ = io.Copy(buf, r.Body) - body := buf.String() - - headers := "" - if responseHeaderBytes, err := json.Marshal(req.Header); err != nil { - headers = "Unable to read headers" - } else { - headers = string(responseHeaderBytes) - } - - return nil, fmt.Errorf("async request returned unexpected status %s - HEADERS: %s BODY: %s", r.Status, headers, body) - } - - return &QueryResponse{AsyncRequestMade: true}, nil -} diff --git a/policy/data/query/chain.go b/policy/data/query/chain.go deleted file mode 100644 index fd41ea5..0000000 --- a/policy/data/query/chain.go +++ /dev/null @@ -1,29 +0,0 @@ -package query - -import ( - "context" - "fmt" -) - -// ChainQueryClient is a wrapper QueryClient that takes a list of other QueryClients -// and returns query results from the first applicable downstream source -type ChainQueryClient struct { - links []QueryClient -} - -func NewChainQueryClient(links ...QueryClient) *ChainQueryClient { - return &ChainQueryClient{ - links: links, - } -} - -func (ds ChainQueryClient) Query(ctx context.Context, queryName string, query string, variables map[string]interface{}, output interface{}) (*QueryResponse, error) { - for _, l := range ds.links { - res, err := l.Query(ctx, queryName, query, variables, output) - if res != nil || err != nil { - return res, err - } - } - - return nil, fmt.Errorf("no QueryClient was available to process query %s", queryName) -} diff --git a/policy/data/query/client.go b/policy/data/query/client.go index 8f036f4..afa2d78 100644 --- a/policy/data/query/client.go +++ b/policy/data/query/client.go @@ -2,8 +2,6 @@ package query import ( "context" - - "github.com/atomist-skills/go-skill/policy/goals" ) type QueryResponse struct { @@ -13,17 +11,3 @@ type QueryResponse struct { type QueryClient interface { Query(ctx context.Context, queryName string, query string, variables map[string]interface{}, output interface{}) (*QueryResponse, error) } - -func GqlContext(ctx goals.GoalEvaluationContext) map[string]interface{} { - result := map[string]interface{}{} - - if ctx.TeamId != "" { - result["teamId"] = ctx.TeamId - } - - if ctx.Organization != "" { - result["organization"] = ctx.Organization - } - - return result -} diff --git a/policy/data/query/fixed.go b/policy/data/query/fixed.go deleted file mode 100644 index dfcfb12..0000000 --- a/policy/data/query/fixed.go +++ /dev/null @@ -1,34 +0,0 @@ -package query - -import ( - "context" -) - -type FixedQueryClientUnmarshaler func(data []byte, output interface{}) error - -// FixedQueryClient returns static data from responses passed in at construction time -type FixedQueryClient struct { - unmarshaler FixedQueryClientUnmarshaler - data map[string][]byte -} - -func NewFixedQueryClient(unmarshaler FixedQueryClientUnmarshaler, data map[string][]byte) FixedQueryClient { - return FixedQueryClient{ - unmarshaler: unmarshaler, - data: data, - } -} - -func (ds FixedQueryClient) Query(ctx context.Context, queryName string, query string, variables map[string]interface{}, output interface{}) (*QueryResponse, error) { - res, ok := ds.data[queryName] - if !ok { - return nil, nil - } - - err := ds.unmarshaler(res, output) - if err != nil { - return nil, err - } - - return &QueryResponse{}, nil -} diff --git a/policy/data/query/jynx/query.go b/policy/data/query/jynx/query.go deleted file mode 100644 index fb92c8f..0000000 --- a/policy/data/query/jynx/query.go +++ /dev/null @@ -1,85 +0,0 @@ -package jynx - -const ( - ImagePackagesByDigestQueryName = "image-packages-by-digest" - - ImagePackagesByDigestQuery = ` - query ($context: Context!, $digest: String!) { - imagePackagesByDigest(context: $context, digest: $digest) { - digest - imagePackages { - packages { - locations { - diffId - path - } - package { - licenses - name - namespace - version - purl - type - vulnerabilities { - cvss { - severity - score - } - epss { - percentile - score - } - fixedBy - publishedAt - source - sourceId - updatedAt - url - vulnerableRange - cisaExploited - } - } - } - } - imageHistories { - emptyLayer - ordinal - } - imageLayers { - layers { - diffId - ordinal - } - } - } - } -` - - VulnerabilitiesByPackageQueryName = "vulnerabilities-by-package" - - // language=graphql - VulnerabilitiesByPackageQuery = ` - query ($context: Context!, $purls: [String!]!) { - vulnerabilitiesByPackage(context: $context, packageUrls: $purls) { - purl - vulnerabilities { - cvss { - severity - score - } - epss { - percentile - score - } - fixedBy - publishedAt - source - sourceId - updatedAt - url - vulnerableRange - cisaExploited - } - } - }` -) diff --git a/policy/data/query/jynx/types.go b/policy/data/query/jynx/types.go deleted file mode 100644 index e1cb997..0000000 --- a/policy/data/query/jynx/types.go +++ /dev/null @@ -1,75 +0,0 @@ -package jynx - -type ( - ImagePackagesByDigestResponse struct { - ImagePackagesByDigest *ImagePackagesByDigest `json:"imagePackagesByDigest" edn:"imagePackagesByDigest"` - } - - ImagePackagesByDigest struct { - Digest string `json:"digest" edn:"digest"` - ImagePackages ImagePackages `json:"imagePackages" edn:"imagePackages"` - ImageHistories []ImageHistory `json:"imageHistories" edn:"imageHistories"` - ImageLayers ImageLayers `json:"imageLayers" edn:"imageLayers"` - } - - ImagePackages struct { - Packages []Packages `json:"packages" edn:"packages"` - } - - ImageHistory struct { - EmptyLayer bool `json:"emptyLayer" edn:"emptyLayer"` - Ordinal int `json:"ordinal" edn:"ordinal"` - } - - ImageLayers struct { - Layers []ImageLayer `json:"layers" edn:"layers"` - } - - ImageLayer struct { - DiffId string `json:"diffId" edn:"diffId"` - Ordinal int `json:"ordinal" edn:"ordinal"` - } - - Packages struct { - Package Package `json:"package" edn:"package"` - Locations []PackageLocation `json:"locations" edn:"locations"` - } - - Package struct { - Licenses []string `json:"licenses" edn:"licenses"` - Name string `json:"name" edn:"name"` - Namespace *string `json:"namespace" edn:"namespace"` - Version string `json:"version" edn:"version"` - Purl string `json:"purl" edn:"purl"` - Type string `json:"type" edn:"type"` - Vulnerabilities []Vulnerability `json:"vulnerabilities" edn:"vulnerabilities"` - } - - PackageLocation struct { - DiffId string `json:"diffId" edn:"diffId"` - Path string `json:"path" edn:"path"` - } - - Vulnerability struct { - Cvss Cvss `json:"cvss" edn:"cvss"` - Epss *Epss `json:"epss" edn:"epss"` - FixedBy *string `json:"fixedBy" edn:"fixedBy"` - PublishedAt string `json:"publishedAt" edn:"publishedAt"` - Source string `json:"source" edn:"source"` - SourceID string `json:"sourceId" edn:"sourceId"` - UpdatedAt string `json:"updatedAt" edn:"updatedAt"` - URL *string `json:"url" edn:"url"` - VulnerableRange string `json:"vulnerableRange" edn:"vulnerableRange"` - CisaExploited bool `json:"cisaExploited" edn:"cisaExploited"` - } - - Cvss struct { - Severity *string `json:"severity" edn:"severity"` - Score *float32 `json:"score" edn:"score"` - } - - Epss struct { - Percentile float32 `json:"percentile" edn:"percentile"` - Score float32 `json:"score" edn:"score"` - } -) diff --git a/policy/data/query/sync.go b/policy/data/query/sync.go deleted file mode 100644 index d57a02b..0000000 --- a/policy/data/query/sync.go +++ /dev/null @@ -1,203 +0,0 @@ -package query - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "golang.org/x/oauth2" - - "github.com/hasura/go-graphql-client" - - "github.com/atomist-skills/go-skill" - "github.com/atomist-skills/go-skill/policy/data/cache" - "github.com/atomist-skills/go-skill/policy/goals" -) - -type SyncGraphqlQueryClient struct { - url string - httpClient http.Client - logger skill.Logger - correlationId *string - basisT *int64 - cache *cache.QueryCache - retryBackoff time.Duration -} - -type SyncGraphQLQueryBody struct { - Query string `json:"query"` - Variables map[string]interface{} `json:"variables"` - BasisT *int64 `json:"basis-t,omitempty"` -} - -func NewSyncGraphqlQueryClientFromSkillRequest(ctx context.Context, req skill.RequestContext, evalMeta goals.EvaluationMetadata) SyncGraphqlQueryClient { - return NewSyncGraphqlQueryClient(ctx, req.Event.Token, req.Event.Urls.Graphql, req.Log).WithBasisT(evalMeta.SubscriptionBasisT) -} - -func NewSyncGraphqlQueryClient(ctx context.Context, token string, url string, logger skill.Logger) SyncGraphqlQueryClient { - httpClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token, TokenType: "Bearer"}, - )) - - return SyncGraphqlQueryClient{ - url: url, - httpClient: *httpClient, - logger: logger, - retryBackoff: 10 * time.Second, - } -} - -func (ds SyncGraphqlQueryClient) WithCorrelationId(correlationId string) SyncGraphqlQueryClient { - ds.correlationId = &correlationId - - return ds -} - -func (ds SyncGraphqlQueryClient) WithBasisT(basisT int64) SyncGraphqlQueryClient { - if basisT == 0 { - ds.basisT = nil - } else { - ds.basisT = &basisT - } - - return ds -} - -func (ds SyncGraphqlQueryClient) WithQueryCache(cache cache.QueryCache) SyncGraphqlQueryClient { - ds.cache = &cache - - return ds -} - -func (ds SyncGraphqlQueryClient) WithRetryBackoff(backoff time.Duration) SyncGraphqlQueryClient { - ds.retryBackoff = backoff - - return ds -} - -func (ds SyncGraphqlQueryClient) Query(ctx context.Context, queryName string, query string, variables map[string]interface{}, output interface{}) (*QueryResponse, error) { - log := ds.logger - - log.Infof("Graphql endpoint: %s", ds.url) - log.Infof("Executing query %s: %s", queryName, query) - log.Debugf("Query variables: %v", variables) - - res, err := ds.requestWithCache(ctx, query, variables) - if err != nil { - return nil, err - } - - err = graphql.UnmarshalGraphQL(res, output) - if err != nil { - return nil, err - } - - return &QueryResponse{}, nil -} - -func (ds SyncGraphqlQueryClient) requestWithCache(ctx context.Context, query string, variables map[string]interface{}) ([]byte, error) { - if ds.cache != nil { - res, err := (*ds.cache).Read(ctx, query, variables) - if err != nil { - return nil, err - } - - if res != nil { - ds.logger.Info("Cache hit for query") - return res, nil - } - } - - res, canRetry, err := ds.request(ctx, query, variables) - - if err != nil && canRetry && ds.retryBackoff > 0 { - time.Sleep(ds.retryBackoff) - res, _, err = ds.request(ctx, query, variables) - } - - if err != nil { - return nil, err - } - - if ds.cache != nil { - err = (*ds.cache).Write(ctx, query, variables, res) - } - - return res, err -} - -func (ds SyncGraphqlQueryClient) request(ctx context.Context, query string, variables map[string]interface{}) ([]byte, bool, error) { - in := SyncGraphQLQueryBody{ - Query: query, - Variables: variables, - BasisT: ds.basisT, - } - var buf bytes.Buffer - err := json.NewEncoder(&buf).Encode(in) - if err != nil { - return nil, false, fmt.Errorf("problem encoding request: %w", err) - } - - reqReader := bytes.NewReader(buf.Bytes()) - request, err := http.NewRequestWithContext(ctx, http.MethodPost, ds.url, reqReader) - if err != nil { - e := fmt.Errorf("problem encoding request: %w", err) - - return nil, false, e - } - request.Header.Add("Content-Type", "application/json") - - request.Header.Add("Accept", "application/json") - - if ds.correlationId != nil { - request.Header.Add("X-Atomist-Correlation-Id", *ds.correlationId) - } - - resp, err := ds.httpClient.Do(request) - - if err != nil { - e := fmt.Errorf("problem making request: %w", err) - return nil, false, e - } - defer resp.Body.Close() - - r := resp.Body - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - err := fmt.Errorf("%v; body: %q", resp.Status, body) - - return nil, resp.StatusCode >= http.StatusInternalServerError, err - } - - var out struct { - Data *json.RawMessage - Errors graphql.Errors - } - - err = json.NewDecoder(r).Decode(&out) - - if err != nil { - return nil, false, err - } - - var rawData []byte - if out.Data != nil && len(*out.Data) > 0 { - rawData = []byte(*out.Data) - } - - if len(out.Errors) > 0 { - errorMessage := out.Errors[0].Message - retryable := errorMessage == "An unexpected error has occurred" - - return rawData, retryable, out.Errors - } - - ds.logger.Debugf("Sync GQL query response: %s", string(rawData)) - - return rawData, false, nil -} diff --git a/policy/data/source.go b/policy/data/source.go index 4f70ccc..092be26 100644 --- a/policy/data/source.go +++ b/policy/data/source.go @@ -1,32 +1,15 @@ package data import ( - "fmt" + "context" - "github.com/atomist-skills/go-skill/policy/data/proxy" "github.com/atomist-skills/go-skill/policy/data/query" + "github.com/atomist-skills/go-skill/policy/goals" + "github.com/atomist-skills/go-skill/policy/types" ) -type DataSource struct { - jynxGQLClient query.QueryClient - proxyClient *proxy.ProxyClient -} - -func NewDataSource(graphQLClient query.QueryClient, proxyClient *proxy.ProxyClient) DataSource { - return DataSource{ - jynxGQLClient: graphQLClient, - proxyClient: proxyClient, - } -} - -func (ds *DataSource) GetQueryClient() query.QueryClient { - return ds.jynxGQLClient -} - -func (ds *DataSource) GetProxyClient() (*proxy.ProxyClient, error) { - if ds.proxyClient == nil { - return nil, fmt.Errorf("no proxy client is configured") - } +type DataSource interface { + GetQueryClient() query.QueryClient - return ds.proxyClient, nil + GetImageVulnerabilities(ctx context.Context, evalCtx goals.GoalEvaluationContext, imageSbom types.SBOM) (*query.QueryResponse, []types.Package, map[string][]types.Vulnerability, error) } diff --git a/policy/data/vulnerabilities.go b/policy/data/vulnerabilities.go deleted file mode 100644 index 0ddd1ac..0000000 --- a/policy/data/vulnerabilities.go +++ /dev/null @@ -1,136 +0,0 @@ -package data - -import ( - "context" - - "github.com/openvex/go-vex/pkg/vex" - govex "github.com/openvex/go-vex/pkg/vex" - - "github.com/atomist-skills/go-skill/policy/data/query" - "github.com/atomist-skills/go-skill/policy/data/query/jynx" - "github.com/atomist-skills/go-skill/policy/goals" - "github.com/atomist-skills/go-skill/policy/types" - "github.com/atomist-skills/go-skill/sbom/normalization" -) - -func (ds *DataSource) GetImageVulnerabilities(ctx context.Context, evalCtx goals.GoalEvaluationContext, imageSbom types.SBOM) (*query.QueryResponse, []types.Package, map[string][]types.Vulnerability, error) { - var packages []types.Package - vulns := map[string][]types.Vulnerability{} - if len(imageSbom.Vulnerabilities) > 0 { - for _, vulnsByPurl := range imageSbom.Vulnerabilities { - vulns[vulnsByPurl.Purl] = vulnsByPurl.Vulnerabilities - } - - packages = imageSbom.Artifacts - } else if len(imageSbom.Artifacts) > 0 { - packages = imageSbom.Artifacts - - evalCtx.Log.Debug("Normalizing purls from SBOM before fetching vulnerabilities") - purls, purlMapping := normalization.NormalizeSBOM(&imageSbom) - evalCtx.Log.Debugf("Normalized purls: %+v", purls) - evalCtx.Log.Debugf("Purl mapping: %+v", purlMapping) - - var vulnsResponse types.VulnerabilitiesByPurls - r, err := ds.jynxGQLClient.Query(ctx, jynx.VulnerabilitiesByPackageQueryName, jynx.VulnerabilitiesByPackageQuery, map[string]interface{}{ - "context": query.GqlContext(evalCtx), - "purls": purls, - "digest": imageSbom.Source.Image.Digest, - }, &vulnsResponse) - if err != nil || r.AsyncRequestMade { - return r, nil, nil, err - } - - evalCtx.Log.Debug("Denormalizing purls after fetching vulnerabilities") - normalization.DenormalizeSBOM(&vulnsResponse, purlMapping) - - for _, vulnsByPurl := range vulnsResponse.VulnerabilitiesByPackage { - vulns[vulnsByPurl.Purl] = applyVEX(vulnsByPurl, imageSbom.VexDocuments) - } - } else { - var response jynx.ImagePackagesByDigestResponse - r, err := ds.jynxGQLClient.Query(ctx, jynx.ImagePackagesByDigestQueryName, jynx.ImagePackagesByDigestQuery, map[string]interface{}{ - "context": query.GqlContext(evalCtx), - "digest": imageSbom.Source.Image.Digest, - }, &response) - if err != nil || r.AsyncRequestMade { - return r, nil, nil, err - } - - if response.ImagePackagesByDigest == nil { - return r, nil, nil, nil - } - - packages, vulns = convertGraphqlToPackages(*response.ImagePackagesByDigest) - } - - return &query.QueryResponse{}, packages, vulns, nil -} - -// applyVEX returns the CVEs that remain relevant after cross-referencing them with VEX documents. -func applyVEX(vulnsByPurl types.VulnerabilitiesByPurl, vexDocs []vex.VEX) []types.Vulnerability { - filteredOutCVEs := []types.Vulnerability{} - - for _, cve := range vulnsByPurl.Vulnerabilities { - for _, v := range vexDocs { - for _, stmt := range v.Statements { - if cveMatch(cve.SourceId, stmt) { - if purlMatch(vulnsByPurl.Purl, stmt) { - if notAffectedOrFixed(stmt) { - filteredOutCVEs = append(filteredOutCVEs, cve) - } - } - } - } - } - } - - vexedCVEsMap := make(map[string]bool, len(filteredOutCVEs)) - for _, cve := range filteredOutCVEs { - vexedCVEsMap[cve.SourceId] = true - } - - // Filter out the VEXed CVEs - cves := make([]types.Vulnerability, 0, len(vulnsByPurl.Vulnerabilities)) - for _, cve := range vulnsByPurl.Vulnerabilities { - if !vexedCVEsMap[cve.SourceId] { - cves = append(cves, cve) - } - } - return cves -} - -// cveMatch checks whether a CVE is present in a VEX statement -func cveMatch(cveID string, stmt govex.Statement) bool { - return stmt.Vulnerability.ID == cveID || string(stmt.Vulnerability.Name) == cveID -} - -// purlMatch checks whether a purl is present in at least one of the following locations: -// - Component -// - Subcomponent(s) -// - Special case for image-scoped exceptions. -func purlMatch(purl string, stmt govex.Statement) bool { - purl, upstreamPurl := normalization.NormalizePURL(purl, nil) - - for _, p := range stmt.Products { - // Check if purl is defined as the top-level component - if purl == p.Component.ID { - return true - } - // Check if purl is defined as one of the subcomponents - if normalization.ContainsPurl(p.Subcomponents, purl) || normalization.ContainsPurl(p.Subcomponents, upstreamPurl) { - return true - } - // If none of the previous conditions matched, we add this special case to support image-scoped exceptions. - // The purpose of this is to align with how VEX works in the platform side. - if len(p.Subcomponents) == 0 { - return true - } - } - - return false -} - -// notAffectedOrFixed checks whether the statement status is not affected or fixed. -func notAffectedOrFixed(stmt govex.Statement) bool { - return stmt.Status == govex.StatusNotAffected || stmt.Status == govex.StatusFixed -} diff --git a/policy/data/vulnerabilities_test.go b/policy/data/vulnerabilities_test.go deleted file mode 100644 index 85ae44d..0000000 --- a/policy/data/vulnerabilities_test.go +++ /dev/null @@ -1,648 +0,0 @@ -package data - -import ( - "context" - "testing" - - govex "github.com/openvex/go-vex/pkg/vex" - - "github.com/atomist-skills/go-skill/internal/test_util" - "github.com/atomist-skills/go-skill/policy/data/query" - "github.com/atomist-skills/go-skill/policy/data/query/jynx" - "github.com/atomist-skills/go-skill/policy/goals" - "github.com/atomist-skills/go-skill/policy/types" - "github.com/openvex/go-vex/pkg/vex" - "github.com/stretchr/testify/assert" -) - -type VulnTestQueryClient struct { - vulnsByPurls types.VulnerabilitiesByPurls - packagesResponse jynx.ImagePackagesByDigestResponse -} - -func NewVulnTestQueryClient(vulnsByPurls types.VulnerabilitiesByPurls, packagesResponse jynx.ImagePackagesByDigestResponse) VulnTestQueryClient { - return VulnTestQueryClient{ - vulnsByPurls: vulnsByPurls, - packagesResponse: packagesResponse, - } -} - -func (ds VulnTestQueryClient) Query(ctx context.Context, queryName string, queryBody string, variables map[string]interface{}, output interface{}) (*query.QueryResponse, error) { - if queryName == jynx.VulnerabilitiesByPackageQueryName { - output.(*types.VulnerabilitiesByPurls).VulnerabilitiesByPackage = ds.vulnsByPurls.VulnerabilitiesByPackage - } else if queryName == jynx.ImagePackagesByDigestQueryName { - output.(*jynx.ImagePackagesByDigestResponse).ImagePackagesByDigest = ds.packagesResponse.ImagePackagesByDigest - } - - return &query.QueryResponse{}, nil -} - -func Test_GetImageVulnerabilities_WhenSbomHasVulnerabilities(t *testing.T) { - sbom := types.SBOM{ - Vulnerabilities: []types.VulnerabilitiesByPurl{ - { - Purl: "pkg:pypi/requests@2.25.1", - Vulnerabilities: []types.Vulnerability{ - { - SourceId: "CVE-2021-3456", - Cvss: types.Cvss{ - Score: 9.8, - Severity: "CRITICAL", - }, - }, - { - SourceId: "CVE-2022-1226", - Cvss: types.Cvss{ - Score: 7.5, - Severity: "HIGH", - }, - }, - }, - }, - { - Purl: "pkg:npm/my-package@1.2.3", - Vulnerabilities: []types.Vulnerability{ - { - SourceId: "CVE-2021-2256", - FixedBy: "1.2.4", - Cvss: types.Cvss{ - Score: 5.6, - Severity: "MEDIUM", - }, - }, - }, - }, - }, - Artifacts: []types.Package{ - { - Purl: "pkg:pypi/requests@2.25.1", - }, - { - Purl: "pkg:npm/my-package@1.2.3", - }, - }, - } - - expectedPackages := []types.Package{ - { - Purl: "pkg:pypi/requests@2.25.1", - }, - { - Purl: "pkg:npm/my-package@1.2.3", - }, - } - - expectedVulnerabilities := map[string][]types.Vulnerability{ - "pkg:pypi/requests@2.25.1": { - { - SourceId: "CVE-2021-3456", - Cvss: types.Cvss{ - Score: 9.8, - Severity: "CRITICAL", - }, - }, - { - SourceId: "CVE-2022-1226", - Cvss: types.Cvss{ - Score: 7.5, - Severity: "HIGH", - }, - }, - }, - "pkg:npm/my-package@1.2.3": { - { - SourceId: "CVE-2021-2256", - FixedBy: "1.2.4", - Cvss: types.Cvss{ - Score: 5.6, - Severity: "MEDIUM", - }, - }, - }, - } - - ds := DataSource{} - - response, packages, vulnerabilities, err := ds.GetImageVulnerabilities(context.Background(), goals.GoalEvaluationContext{}, sbom) - - assert.Nil(t, err) - assert.False(t, response.AsyncRequestMade) - - assert.Equal(t, expectedPackages, packages) - assert.Equal(t, expectedVulnerabilities, vulnerabilities) -} - -func Test_GetImageVulnerabilities_WhenSbomHasArtifacts_AndNoVulnerabilities(t *testing.T) { - sbom := types.SBOM{ - Source: types.Source{ - Image: &types.ImageSource{ - Digest: "sha256:123456", - }, - }, - Artifacts: []types.Package{ - { - Purl: "pkg:pypi/requests@2.25.1", - }, - { - Purl: "pkg:npm/my-package@1.2.3", - }, - }, - } - - expectedPackages := []types.Package{ - { - Purl: "pkg:pypi/requests@2.25.1", - }, - { - Purl: "pkg:npm/my-package@1.2.3", - }, - } - - expectedVulnerabilities := map[string][]types.Vulnerability{ - "pkg:pypi/requests@2.25.1": { - { - SourceId: "CVE-2021-3456", - Cvss: types.Cvss{ - Score: 9.8, - Severity: "CRITICAL", - }, - }, - { - SourceId: "CVE-2022-1226", - Cvss: types.Cvss{ - Score: 7.5, - Severity: "HIGH", - }, - }, - }, - "pkg:npm/my-package@1.2.3": { - { - SourceId: "CVE-2021-2256", - FixedBy: "1.2.4", - Cvss: types.Cvss{ - Score: 5.6, - Severity: "MEDIUM", - }, - }, - }, - } - - ds := DataSource{ - jynxGQLClient: NewVulnTestQueryClient(types.VulnerabilitiesByPurls{ - VulnerabilitiesByPackage: []types.VulnerabilitiesByPurl{ - { - Purl: "pkg:pypi/requests@2.25.1", - Vulnerabilities: []types.Vulnerability{ - { - SourceId: "CVE-2021-3456", - Cvss: types.Cvss{ - Score: 9.8, - Severity: "CRITICAL", - }, - }, - { - SourceId: "CVE-2022-1226", - Cvss: types.Cvss{ - Score: 7.5, - Severity: "HIGH", - }, - }, - }, - }, - { - Purl: "pkg:npm/my-package@1.2.3", - Vulnerabilities: []types.Vulnerability{ - { - SourceId: "CVE-2021-2256", - FixedBy: "1.2.4", - Cvss: types.Cvss{ - Score: 5.6, - Severity: "MEDIUM", - }, - }, - }, - }, - }, - }, - jynx.ImagePackagesByDigestResponse{}), - } - - response, packages, vulnerabilities, err := ds.GetImageVulnerabilities(context.Background(), goals.GoalEvaluationContext{Log: test_util.CreateEmptyLogger()}, sbom) - - assert.Nil(t, err) - assert.False(t, response.AsyncRequestMade) - - assert.Equal(t, expectedPackages, packages) - assert.Equal(t, expectedVulnerabilities, vulnerabilities) -} - -func Test_GetImageVulnerabilities_WhenSbomHasNoArtifacts_AndNoVulnerabilities(t *testing.T) { - sbom := types.SBOM{ - Source: types.Source{ - Image: &types.ImageSource{ - Digest: "sha256:123456", - }, - }, - } - - expectedPackages := []types.Package{ - { - Purl: "pkg:pypi/requests@2.25.1", - }, - { - Purl: "pkg:npm/my-package@1.2.3", - }, - } - - expectedVulnerabilities := map[string][]types.Vulnerability{ - "pkg:pypi/requests@2.25.1": { - { - SourceId: "CVE-2021-3456", - Cvss: types.Cvss{ - Score: 9.8, - Severity: "CRITICAL", - }, - }, - { - SourceId: "CVE-2022-1226", - Cvss: types.Cvss{ - Score: 7.5, - Severity: "HIGH", - }, - }, - }, - "pkg:npm/my-package@1.2.3": { - { - SourceId: "CVE-2021-2256", - FixedBy: "1.2.4", - Cvss: types.Cvss{ - Score: 5.6, - Severity: "MEDIUM", - }, - }, - }, - } - - ds := DataSource{ - jynxGQLClient: NewVulnTestQueryClient( - types.VulnerabilitiesByPurls{}, - jynx.ImagePackagesByDigestResponse{ - ImagePackagesByDigest: &jynx.ImagePackagesByDigest{ - ImagePackages: jynx.ImagePackages{ - Packages: []jynx.Packages{ - { - Package: jynx.Package{ - Purl: "pkg:pypi/requests@2.25.1", - Vulnerabilities: []jynx.Vulnerability{ - { - SourceID: "CVE-2021-3456", - Cvss: jynx.Cvss{ - Score: Ptr(float32(9.8)), - Severity: Ptr("CRITICAL"), - }, - }, - { - SourceID: "CVE-2022-1226", - Cvss: jynx.Cvss{ - Score: Ptr(float32(7.5)), - Severity: Ptr("HIGH"), - }, - }, - }, - }, - }, - { - Package: jynx.Package{ - Purl: "pkg:npm/my-package@1.2.3", - Vulnerabilities: []jynx.Vulnerability{ - { - SourceID: "CVE-2021-2256", - FixedBy: Ptr("1.2.4"), - Cvss: jynx.Cvss{ - Score: Ptr(float32(5.6)), - Severity: Ptr("MEDIUM"), - }, - }, - }, - }, - }, - }, - }, - }, - }), - } - - response, packages, vulnerabilities, err := ds.GetImageVulnerabilities(context.Background(), goals.GoalEvaluationContext{Log: test_util.CreateEmptyLogger()}, sbom) - - assert.Nil(t, err) - assert.False(t, response.AsyncRequestMade) - - assert.Equal(t, expectedPackages, packages) - assert.Equal(t, expectedVulnerabilities, vulnerabilities) -} - -func Test_applyVEX(t *testing.T) { - const ( - openSSLPurl = "pkg:apk/alpine/openssl@3.0.12-r1?os_name=alpine&os_version=3.17" - alpineImgPurl = "pkg:docker/alpine@sha256:6e94b5cda2d6fd57d85abf81e81dabaea97a5885f919da676cc19d3551da4061" - awsPurl = "pkg:golang/github.com/aws/aws-sdk-go@1.44.288" - ) - - tests := []struct { - name string - vulnsByPurl types.VulnerabilitiesByPurl - vexDocs []vex.VEX - expectedCVEs []types.Vulnerability // CVEs after applying VEX - }{ - { - name: "CVE-2024-5535 is not filtered out when there aren't VEX documents", - vulnsByPurl: types.VulnerabilitiesByPurl{ - Purl: openSSLPurl, - Vulnerabilities: cves("CVE-2024-5535"), - }, - vexDocs: []vex.VEX{}, // empty on purpose - expectedCVEs: cves("CVE-2024-5535"), - }, - { - name: "CVE-2024-5535 is not filtered out when the VEX document has no statements", - vulnsByPurl: types.VulnerabilitiesByPurl{ - Purl: openSSLPurl, - Vulnerabilities: cves("CVE-2024-5535"), - }, - vexDocs: []vex.VEX{ - { - Statements: []vex.Statement{}, // empty on purpose - }, - }, - expectedCVEs: cves("CVE-2024-5535"), - }, - { - name: "CVE-2024-5535 is not filtered out when purl is not present in either the product id or subcomponents", - vulnsByPurl: types.VulnerabilitiesByPurl{ - Purl: openSSLPurl, - Vulnerabilities: cves("CVE-2024-5535"), - }, - vexDocs: []vex.VEX{ - { - Statements: []vex.Statement{ - { - Vulnerability: vex.Vulnerability{ - ID: "CVE-2024-5535", - }, - Products: []vex.Product{ - { - Component: govex.Component{ - ID: alpineImgPurl, - }, - Subcomponents: []vex.Subcomponent{ - { - Component: vex.Component{ - ID: awsPurl, - }, - }, - }, - }, - }, - Status: govex.StatusNotAffected, - Justification: vex.VulnerableCodeNotInExecutePath, - }, - }, - }, - }, - expectedCVEs: cves("CVE-2024-5535"), - }, - { - name: "CVE-2024-5535 is filtered out when purl matches the product id", - vulnsByPurl: types.VulnerabilitiesByPurl{ - Purl: openSSLPurl, - Vulnerabilities: cves("CVE-2024-5535", "CVE-2024-5536"), - }, - vexDocs: []vex.VEX{ - { - Statements: []vex.Statement{ - { - Vulnerability: vex.Vulnerability{ - ID: "CVE-2024-5535", - }, - Products: []vex.Product{ - { - Component: govex.Component{ - ID: openSSLPurl, - }, - }, - }, - Status: govex.StatusNotAffected, - Justification: vex.VulnerableCodeNotInExecutePath, - }, - }, - }, - }, - expectedCVEs: cves("CVE-2024-5536"), - }, - { - name: "CVE-2024-5535 is filtered out when purl is present in subcomponents", - vulnsByPurl: types.VulnerabilitiesByPurl{ - Purl: openSSLPurl, - Vulnerabilities: cves("CVE-2024-5535", "CVE-2024-5536"), - }, - vexDocs: []vex.VEX{ - { - Statements: []vex.Statement{ - { - Vulnerability: vex.Vulnerability{ - ID: "CVE-2024-5535", - }, - Products: []vex.Product{ - { - Component: govex.Component{ - ID: alpineImgPurl, - }, - Subcomponents: []vex.Subcomponent{ - { - Component: vex.Component{ - ID: openSSLPurl, - }, - }, - }, - }, - }, - Status: govex.StatusNotAffected, - Justification: vex.VulnerableCodeNotInExecutePath, - }, - }, - }, - }, - expectedCVEs: cves("CVE-2024-5536"), - }, - { - name: "CVE-2024-5535 is filtered out when there are no subcomponents (even if there is a product id mismatch)", - vulnsByPurl: types.VulnerabilitiesByPurl{ - Purl: openSSLPurl, - Vulnerabilities: cves("CVE-2024-5535", "CVE-2024-5536"), - }, - vexDocs: []vex.VEX{ - { - Statements: []vex.Statement{ - { - Vulnerability: vex.Vulnerability{ - ID: "CVE-2024-5535", - }, - Products: []vex.Product{ - { - Component: govex.Component{ - ID: alpineImgPurl, // notice product id mismatch with openSSLPurl - }, - Subcomponents: []vex.Subcomponent{}, // empty on purpose - }, - }, - Status: govex.StatusNotAffected, - Justification: vex.VulnerableCodeNotInExecutePath, - }, - }, - }, - }, - expectedCVEs: cves("CVE-2024-5536"), - }, - { - name: "CVE-2024-0001 is not filtered out when its source id does not match the vulnerability id in the VEX statement", - vulnsByPurl: types.VulnerabilitiesByPurl{ - Purl: openSSLPurl, - Vulnerabilities: cves("CVE-2024-0001"), - }, - vexDocs: []vex.VEX{ - { - Statements: []vex.Statement{ - { - Vulnerability: vex.Vulnerability{ - ID: "CVE-2024-5535", - }, - Products: []vex.Product{ - { - Component: govex.Component{ - ID: alpineImgPurl, - }, - }, - }, - Status: govex.StatusNotAffected, - Justification: vex.VulnerableCodeNotInExecutePath, - }, - }, - }, - }, - expectedCVEs: cves("CVE-2024-0001"), - }, - { - name: "CVE-2024-0001 is not filtered out when its source id does not match the vulnerability name in the VEX statement", - vulnsByPurl: types.VulnerabilitiesByPurl{ - Purl: openSSLPurl, - Vulnerabilities: cves("CVE-2024-0001"), - }, - vexDocs: []vex.VEX{ - { - Statements: []vex.Statement{ - { - Vulnerability: vex.Vulnerability{ - Name: "CVE-2024-5535", - }, - Products: []vex.Product{ - { - Component: govex.Component{ - ID: alpineImgPurl, - }, - }, - }, - Status: govex.StatusNotAffected, - Justification: vex.VulnerableCodeNotInExecutePath, - }, - }, - }, - }, - expectedCVEs: cves("CVE-2024-0001"), - }, - { - name: "CVE-2024-5535 is filtered out when status is not_affected", - vulnsByPurl: types.VulnerabilitiesByPurl{ - Purl: openSSLPurl, - Vulnerabilities: cves("CVE-2024-5535"), - }, - vexDocs: []vex.VEX{ - { - Statements: []vex.Statement{ - { - Vulnerability: vex.Vulnerability{ - Name: "CVE-2024-5535", - }, - Products: []vex.Product{ - { - Component: govex.Component{ - ID: openSSLPurl, - }, - }, - }, - Status: govex.StatusNotAffected, - Justification: vex.VulnerableCodeNotInExecutePath, - }, - }, - }, - }, - expectedCVEs: []types.Vulnerability{}, - }, - { - name: "CVE-2024-5535 is filtered out when status is fixed", - vulnsByPurl: types.VulnerabilitiesByPurl{ - Purl: openSSLPurl, - Vulnerabilities: cves("CVE-2024-5535"), - }, - vexDocs: []vex.VEX{ - { - Statements: []vex.Statement{ - { - Vulnerability: vex.Vulnerability{ - Name: "CVE-2024-5535", - }, - Products: []vex.Product{ - { - Component: govex.Component{ - ID: openSSLPurl, - }, - }, - }, - Status: govex.StatusFixed, - }, - }, - }, - }, - expectedCVEs: []types.Vulnerability{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - actual := applyVEX(tt.vulnsByPurl, tt.vexDocs) - if len(actual) != len(tt.expectedCVEs) { - t.Errorf("applyVEX() = %d, want %d", len(actual), len(tt.expectedCVEs)) - } - if len(actual) == len(tt.expectedCVEs) { - for i, v := range actual { - if tt.expectedCVEs[i].SourceId != v.SourceId { - t.Errorf("applyVEX() = %v, want %v", v.SourceId, tt.expectedCVEs[i].SourceId) - } - } - } - }) - } -} - -func cves(cveIDs ...string) []types.Vulnerability { - var cves = make([]types.Vulnerability, 0, len(cveIDs)) - for _, cve := range cveIDs { - cves = append(cves, types.Vulnerability{ - SourceId: cve, - }) - } - return cves -} - -func Ptr[T any](v T) *T { - return &v -} diff --git a/policy/goals/differ.go b/policy/goals/differ.go deleted file mode 100644 index e6233a0..0000000 --- a/policy/goals/differ.go +++ /dev/null @@ -1,82 +0,0 @@ -package goals - -import ( - "fmt" - - "github.com/atomist-skills/go-skill" - "github.com/mitchellh/hashstructure/v2" -) - -// GoalResultsDiffer checks if the current query results differ from the previous ones. -// It returns the storage id for the current query results. -func GoalResultsDiffer(log skill.Logger, queryResults []GoalEvaluationQueryResult, digest string, previousStorageId string) (bool, string, error) { - log.Infof("Generating storage id for image %s", digest) - - storageId := "no-data" - - if queryResults != nil { - hashOptions := hashstructure.HashOptions{ - SlicesAsSets: true, - } - hash, err := hashstructure.Hash(queryResults, hashstructure.FormatV2, &hashOptions) - if err != nil { - return false, "", fmt.Errorf("failed to generate storage id for image %s: %s", digest, err) - } - - storageId = fmt.Sprint(hash) - } - - differ := storageId != previousStorageId - - if differ { - log.Infof("New storage id [%s] differs from previous [%s]", storageId, previousStorageId) - } else { - log.Infof("New storage id matches previous [%s]", storageId) - } - - return differ, storageId, nil -} - -func isRelevantParam(str string) bool { - irrelevantParams := []string{"definitionName", "displayName", "description", "remediationLink", "resultType", "detailsOrder"} - for _, v := range irrelevantParams { - if v == str { - return false - } - } - - return true -} - -// Returns the config hash for the current skill config -func GoalConfigsDiffer(log skill.Logger, config skill.Configuration, digest string, previousConfigHash string) (bool, string, error) { - log.Debugf("Generating config hash for image %s", digest) - - params := config.Parameters - values := map[string]interface{}{} - for _, p := range params { - if isRelevantParam(p.Name) { - values[p.Name] = p.Value - } - } - - hashOptions := hashstructure.HashOptions{ - SlicesAsSets: true, - } - hash, err := hashstructure.Hash(values, hashstructure.FormatV2, &hashOptions) - if err != nil { - return false, "", fmt.Errorf("failed to generate config hash for image %s: %s", digest, err) - } - - configHash := fmt.Sprint(hash) - - differ := configHash != previousConfigHash - - if differ { - log.Infof("New config hash [%s] differs from previous [%s]", configHash, previousConfigHash) - } else { - log.Infof("New config hash matches previous [%s]", configHash) - } - - return differ, configHash, nil -} diff --git a/policy/goals/entities.go b/policy/goals/entities.go deleted file mode 100644 index 31e30eb..0000000 --- a/policy/goals/entities.go +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright © 2023 Atomist, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package goals - -import "time" - -func CreateEntitiesFromResults(results []GoalEvaluationQueryResult, goalDefinition string, goalConfiguration string, image string, storageId string, configHash string, evaluationTs time.Time, tx int64, retract bool) GoalEvaluationResultEntity { - entity := GoalEvaluationResultEntity{ - Definition: goalDefinition, - Configuration: goalConfiguration, - Subject: DockerImageEntity{Digest: image}, - DeviationCount: nil, - StorageId: nil, - ConfigHash: configHash, - CreatedAt: evaluationTs, - TransactionCondition: TransactionConditionEntity{ - Args: map[string]interface{}{"tx-arg": tx}, - Where: []byte(`[[?entity :goal.result/created-at _ ?tx true] - [(< ?tx ?tx-arg)]]`), - }, - } - - if storageId != "no-data" { - deviationCount := len(results) - - entity.DeviationCount = deviationCount - entity.StorageId = storageId - } else if retract { - entity.DeviationCount = RetractionEntity{Retract: true} - entity.StorageId = RetractionEntity{Retract: true} - } - - return entity -} diff --git a/policy/goals/entities_test.go b/policy/goals/entities_test.go deleted file mode 100644 index 9a291ee..0000000 --- a/policy/goals/entities_test.go +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright © 2023 Atomist, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package goals - -import ( - "testing" - "time" - - "olympos.io/encoding/edn" -) - -func TestCreateEntitiesFromResult(t *testing.T) { - result := `[{:name "CVE-2023-2650", :details {:purl "pkg:alpine/openssl@3.1.0-r4?os_name=alpine&os_version=3.18", :cve "CVE-2023-2650", :severity "HIGH", :fixed-by "3.1.1-r0"} }]` - - resultModel := []GoalEvaluationQueryResult{} - - edn.Unmarshal([]byte(result), &resultModel) - - evaluationTs := time.Date(2023, 7, 10, 20, 1, 41, 0, time.UTC) - - entity := CreateEntitiesFromResults(resultModel, "test-definition", "test-configuration", "test-image", "storage-id", "config-hash", evaluationTs, 123, false) - - if entity.Definition != "test-definition" || entity.Configuration != "test-configuration" || entity.StorageId != "storage-id" || entity.CreatedAt.Format("2006-01-02T15:04:05.000Z") != "2023-07-10T20:01:41.000Z" { - t.Errorf("metadata not set correctly") - } - - if entity.DeviationCount != 1 { - t.Errorf("incorrect number of deviations, expected %d, got %d", 1, entity.DeviationCount) - } -} - -func TestNoDataSetsRetractionWhenPreviousResultsExit(t *testing.T) { - result := `[{:name "CVE-2023-2650", :details {:purl "pkg:alpine/openssl@3.1.0-r4?os_name=alpine&os_version=3.18", :cve "CVE-2023-2650", :severity "HIGH", :fixed-by "3.1.1-r0"} }]` - - resultModel := []GoalEvaluationQueryResult{} - - edn.Unmarshal([]byte(result), &resultModel) - - evaluationTs := time.Date(2023, 7, 10, 20, 1, 41, 0, time.UTC) - - entity := CreateEntitiesFromResults(resultModel, "test-definition", "test-configuration", "test-image", "no-data", "config-hash", evaluationTs, 123, true) - - if !entity.StorageId.(RetractionEntity).Retract || !entity.DeviationCount.(RetractionEntity).Retract { - t.Errorf("metadata not set correctly") - } -} - -func TestNoDataNilsOutDeviationCountWhenNoPreviousResultsExist(t *testing.T) { - result := `[{:name "CVE-2023-2650", :details {:purl "pkg:alpine/openssl@3.1.0-r4?os_name=alpine&os_version=3.18", :cve "CVE-2023-2650", :severity "HIGH", :fixed-by "3.1.1-r0"} }]` - - resultModel := []GoalEvaluationQueryResult{} - - edn.Unmarshal([]byte(result), &resultModel) - - evaluationTs := time.Date(2023, 7, 10, 20, 1, 41, 0, time.UTC) - - entity := CreateEntitiesFromResults(resultModel, "test-definition", "test-configuration", "test-image", "no-data", "config-hash", evaluationTs, 123, false) - - if entity.StorageId != nil || entity.DeviationCount != nil { - t.Errorf("metadata not set correctly") - } -} diff --git a/policy/goals/types.go b/policy/goals/types.go index dfb4a03..f95de51 100644 --- a/policy/goals/types.go +++ b/policy/goals/types.go @@ -18,7 +18,6 @@ package goals import ( "context" - "time" "github.com/atomist-skills/go-skill" "github.com/atomist-skills/go-skill/policy/types" @@ -36,83 +35,6 @@ type ( Details map[edn.Keyword]interface{} `edn:"details" json:"details"` } - DockerImageEntity struct { - skill.Entity `entity-type:"docker/image"` - Digest string `edn:"docker.image/digest"` - } - - RetractionEntity struct { - Retract bool `edn:"retract"` - } - - GoalEvaluationResultEntity struct { - skill.Entity `entity-type:"goal/result"` - Definition string `edn:"goal.definition/name"` - Configuration string `edn:"goal.configuration/name"` - Subject DockerImageEntity `edn:"goal.result/subject"` - DeviationCount interface{} `edn:"goal.result/deviation-count,omitempty"` - StorageId interface{} `edn:"goal.result/storage-id,omitempty"` - ConfigHash string `edn:"goal.result/config-hash"` - CreatedAt time.Time `edn:"goal.result/created-at"` - TransactionCondition TransactionConditionEntity `edn:"atomist/tx-iff"` - } - - TransactionConditionEntity struct { - Args map[string]interface{} `edn:"args"` - Where edn.RawMessage `edn:"where"` - } - - OsDistro struct { - Name string `edn:"os.distro/name"` - Version string `edn:"os.distro/version"` - } - - SubscriptionImage struct { - Digest string `edn:"docker.image/digest"` - Distro *OsDistro `edn:"docker.image/distro"` - } - - SubscriptionRepository struct { - Host string `edn:"docker.repository/host"` - Repository string `edn:"docker.repository/repository"` - } - - ImagePlatform struct { - Architecture string `edn:"docker.platform/architecture" json:"architecture"` - Os string `edn:"docker.platform/os" json:"os"` - Variant string `edn:"docker.platform/variant" json:"variant"` - } - - Attestation struct { - PredicateType *string `edn:"intoto.attestation/predicate-type"` - Predicates []Predicate `edn:"intoto.predicate/_attestation"` - } - - BuildKitProvenanceMode struct { - Ident edn.Keyword `edn:"db/ident"` - } - - Predicate struct { - ProvenanceMode *BuildKitProvenanceMode `edn:"buildkit.provenance/mode,omitempty"` - } - - ImageSubscriptionQueryResult struct { - ImageDigest string `edn:"docker.image/digest"` - ImagePlatforms []ImagePlatform `edn:"docker.image/platform" json:"platforms"` - ImageRepo *SubscriptionRepository `edn:"docker.image/repository"` - FromReference *SubscriptionImage `edn:"docker.image/from"` - FromRepo *SubscriptionRepository `edn:"docker.image/from-repository"` - FromTag string `edn:"docker.image/from-tag"` - Attestations []Attestation `edn:"intoto.attestation/_subject"` - User string `edn:"docker.image/user,omitempty"` - } - - EvaluationMetadata struct { - SubscriptionResult []map[edn.Keyword]edn.RawMessage `edn:"subscription-result"` - SubscriptionTx int64 `edn:"subscription-tx"` - SubscriptionBasisT int64 `edn:"subscription-basis-t"` - } - GoalEvaluator interface { EvaluateGoal(ctx context.Context, evalCtx GoalEvaluationContext, sbom types.SBOM, extraData []map[edn.Keyword]edn.RawMessage) (EvaluationResult, error) } diff --git a/policy/policy.go b/policy/policy.go index a090430..2115f92 100644 --- a/policy/policy.go +++ b/policy/policy.go @@ -4,13 +4,11 @@ import ( "github.com/atomist-skills/go-skill" "github.com/atomist-skills/go-skill/policy/data" "github.com/atomist-skills/go-skill/policy/goals" - "github.com/atomist-skills/go-skill/policy/policy_handler" ) const VulnerabilityChangeEvent = "VulnerabilityChangeEvent" type Policy struct { - SkillHandler policy_handler.EventHandler CreateEvaluatorFunc func(map[string]interface{}, data.DataSource) (goals.GoalEvaluator, error) Spec *skill.SkillSpec EventSubscriptions []string diff --git a/policy/policy_handler/async.go b/policy/policy_handler/async.go deleted file mode 100644 index 4ac0ecf..0000000 --- a/policy/policy_handler/async.go +++ /dev/null @@ -1,110 +0,0 @@ -package policy_handler - -import ( - "context" - b64 "encoding/base64" - "fmt" - - "github.com/atomist-skills/go-skill/policy/goals" - "github.com/atomist-skills/go-skill/policy/types" - - "github.com/atomist-skills/go-skill" - "github.com/atomist-skills/go-skill/policy/data/query" - "olympos.io/encoding/edn" -) - -const eventNameAsyncQuery = query.AsyncQueryName // these must match for the event handler to be registered - -// WithAsyncMultiQuery will enable the async graphql data source to spool results across multiple queries. -// These intermediate results are stored in the following requests' metadata, -// and as such risk hitting the upper limit on the metadata field, and failing. -func WithAsyncMultiQuery() Opt { - return func(h *EventHandler) { - h.subscriptionNames = append(h.subscriptionNames, eventNameAsyncQuery) - h.evalInputProviders = append(h.evalInputProviders, getAsyncInputData) - h.queryClientProviders = append(h.queryClientProviders, buildAsyncDataSources(true)) - } -} - -// WithAsync is enabled by default, added last after all other Opts if not explicitly registered early. -func WithAsync() Opt { - return func(h *EventHandler) { - // don't register if WithAsync / WithAsyncMultiQuery is already enabled - for _, s := range h.subscriptionNames { - if s == eventNameAsyncQuery { - return - } - } - - h.subscriptionNames = append(h.subscriptionNames, eventNameAsyncQuery) - h.evalInputProviders = append(h.evalInputProviders, getAsyncInputData) - h.queryClientProviders = append(h.queryClientProviders, buildAsyncDataSources(false)) - } -} - -func getAsyncInputData(ctx context.Context, req skill.RequestContext) (*goals.EvaluationMetadata, skill.Configuration, *types.SBOM, error) { - if req.Event.Context.AsyncQueryResult.Name != eventNameAsyncQuery { - return nil, skill.Configuration{}, nil, nil - } - - metaEdn, err := b64.StdEncoding.DecodeString(req.Event.Context.AsyncQueryResult.Metadata) - if err != nil { - return nil, skill.Configuration{}, nil, fmt.Errorf("failed to decode async metadata: %w", err) - } - - var metadata query.AsyncResultMetadata - err = edn.Unmarshal(metaEdn, &metadata) - if err != nil { - return nil, skill.Configuration{}, nil, fmt.Errorf("failed to unmarshal async metadata: %w", err) - } - - sbom, err := createSBOMFromSubscriptionResult(req, metadata.EvaluationMetadata.SubscriptionResult) - if err != nil { - return nil, skill.Configuration{}, nil, fmt.Errorf("failed to create SBOM from subscription result: %w", err) - } - - return &metadata.EvaluationMetadata, req.Event.Context.AsyncQueryResult.Configuration, sbom, nil -} - -func buildAsyncDataSources(multipleQuerySupport bool) queryClientProvider { - return func(ctx context.Context, req skill.RequestContext, evalMeta goals.EvaluationMetadata) ([]query.QueryClient, error) { - if req.Event.Type == "sync-request" { - return []query.QueryClient{}, nil - } - - if req.Event.Context.AsyncQueryResult.Name != eventNameAsyncQuery { - return []query.QueryClient{ - query.NewAsyncQueryClient(multipleQuerySupport, req, evalMeta, map[string]query.AsyncQueryResponse{}), - }, nil - } - - metaEdn, err := b64.StdEncoding.DecodeString(req.Event.Context.AsyncQueryResult.Metadata) - if err != nil { - return nil, fmt.Errorf("failed to decode metadata: %w", err) - } - - var metadata query.AsyncResultMetadata - err = edn.Unmarshal(metaEdn, &metadata) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal metadata: %w", err) - } - - var queryResponse query.AsyncQueryResponse - err = edn.Unmarshal(req.Event.Context.AsyncQueryResult.Result, &queryResponse) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal async query result: %w", err) - } - if len(queryResponse.Errors) > 0 { - errorMessage := queryResponse.Errors[0].Message - if errorMessage == "An unexpected error has occurred" { - return nil, types.RetryableExecutionError(fmt.Sprintf("async query contained error: %s", errorMessage)) - } - return nil, fmt.Errorf("async query contained error: %s", errorMessage) - } - metadata.AsyncQueryResults[metadata.InFlightQueryName] = queryResponse - - return []query.QueryClient{ - query.NewAsyncQueryClient(multipleQuerySupport, req, metadata.EvaluationMetadata, metadata.AsyncQueryResults), - }, nil - } -} diff --git a/policy/policy_handler/handler.go b/policy/policy_handler/handler.go deleted file mode 100644 index b619dc7..0000000 --- a/policy/policy_handler/handler.go +++ /dev/null @@ -1,259 +0,0 @@ -package policy_handler - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "os/user" - "strings" - "time" - - "github.com/atomist-skills/go-skill" - "github.com/atomist-skills/go-skill/policy/data" - "github.com/atomist-skills/go-skill/policy/data/proxy" - "github.com/atomist-skills/go-skill/policy/data/query" - "github.com/atomist-skills/go-skill/policy/goals" - "github.com/atomist-skills/go-skill/policy/transact" - "github.com/atomist-skills/go-skill/policy/types" - "github.com/atomist-skills/go-skill/util" - - intoto "github.com/in-toto/in-toto-golang/in_toto" -) - -type ( - EvaluatorSelector func(ctx context.Context, goal goals.Goal, dataSource data.DataSource) (goals.GoalEvaluator, error) - - evalInputProvider func(ctx context.Context, req skill.RequestContext) (*goals.EvaluationMetadata, skill.Configuration, *types.SBOM, error) - queryClientProvider func(ctx context.Context, req skill.RequestContext, evalMeta goals.EvaluationMetadata) ([]query.QueryClient, error) - transactionFilter func(ctx context.Context, req skill.RequestContext) bool - proxyClientProvider func(ctx context.Context, req skill.RequestContext) proxy.ProxyClient - - EventHandler struct { - // parameters - evalSelector EvaluatorSelector - subscriptionNames []string - - // hooks used by opts - evalInputProviders []evalInputProvider - queryClientProviders []queryClientProvider - transactFilters []transactionFilter - proxyClientProvider proxyClientProvider - } - - Opt func(handler *EventHandler) -) - -var defaultOpts = []Opt{ - WithAsync(), - withSubscription(), -} - -func NewPolicyEventHandler(subscriptionNames []string, evalSelector EvaluatorSelector, opts ...Opt) EventHandler { - p := EventHandler{ - subscriptionNames: subscriptionNames, - evalSelector: evalSelector, - } - - for _, o := range opts { - o(&p) - } - for _, o := range defaultOpts { - o(&p) - } - - return p -} - -func (h EventHandler) createSkillHandlers() skill.Handlers { - handlers := skill.Handlers{} - for _, n := range h.subscriptionNames { - handlers[n] = h.handle - } - - return handlers -} - -func (h EventHandler) Start() { - handlers := h.createSkillHandlers() - - skill.Start(handlers) -} - -func (h EventHandler) CreateHttpHandler() func(http.ResponseWriter, *http.Request) { - handlers := h.createSkillHandlers() - - return skill.CreateHttpHandler(handlers) -} - -func (h EventHandler) ExecuteSyncRequest(ctx context.Context, req skill.RequestContext) ([]goals.GoalEvaluationQueryResult, error) { - handlers := h.createSkillHandlers() - - syncHandler, ok := handlers[eventNameLocalEval] - if !ok { - return nil, fmt.Errorf("no handler for sync request") - } - - result := syncHandler(ctx, req) - - if result.State != skill.Completed { - return nil, fmt.Errorf("sync request did not complete successfully [%s]", result.Reason) - } - - return result.SyncRequest.([]goals.GoalEvaluationQueryResult), nil -} - -func (h EventHandler) handle(ctx context.Context, req skill.RequestContext) skill.Status { - var ( - evaluationMetadata *goals.EvaluationMetadata - configuration skill.Configuration - sbom *types.SBOM - err error - ) - for _, provider := range h.evalInputProviders { - evaluationMetadata, configuration, sbom, err = provider(ctx, req) - if err != nil { - return skill.NewFailedStatus(fmt.Sprintf("failed to retrieve subscription result [%s]", err.Error())) - } - if evaluationMetadata != nil { - break - } - } - - if evaluationMetadata == nil { - return skill.NewFailedStatus("subscription result was not found") - } - - sources := []query.QueryClient{} - for _, provider := range h.queryClientProviders { - qc, err := provider(ctx, req, *evaluationMetadata) - if err != nil { - if retryableError, ok := err.(types.RetryableExecutionError); ok { - return skill.NewRetryableStatus(fmt.Sprintf("Failed to create data source [%s]", retryableError.Error())) - } - return skill.NewFailedStatus(fmt.Sprintf("failed to create data source [%s]", err.Error())) - } - sources = append(sources, qc...) - } - - queryClient := query.NewChainQueryClient(sources...) - - var proxyClient *proxy.ProxyClient - if h.proxyClientProvider != nil { - provider := h.proxyClientProvider - client := provider(ctx, req) - proxyClient = &client - } - dataSource := data.NewDataSource(queryClient, proxyClient) - - return h.evaluate(ctx, req, dataSource, *evaluationMetadata, *sbom, configuration) -} - -func (h EventHandler) evaluate(ctx context.Context, req skill.RequestContext, dataSource data.DataSource, evaluationMetadata goals.EvaluationMetadata, sbom types.SBOM, configuration skill.Configuration) skill.Status { - goalName := req.Event.Skill.Name - tx := evaluationMetadata.SubscriptionTx - subscriptionResult := evaluationMetadata.SubscriptionResult - - cfg := configuration.Name - params := configuration.Parameters - - paramValues := map[string]interface{}{} - for _, p := range params { - paramValues[p.Name] = p.Value - } - - // atm-skill local appends the current user's name to the skill name - // we can strip that suffix off before calling evalSelector to let it match on the original name - goalDefName := goalName - u, err := user.Current() - if err == nil { - goalDefName = strings.TrimSuffix(goalDefName, fmt.Sprintf("-%s", u.Username)) - } - - goal := goals.Goal{ - Definition: goalDefName, - Configuration: cfg, - Args: paramValues, - } - - evaluator, err := h.evalSelector(ctx, goal, dataSource) - if err != nil { - req.Log.Errorf(err.Error()) - return skill.NewFailedStatus(fmt.Sprintf("Failed to create goal evaluator: %s", err.Error())) - } - - digest := sbom.Source.Image.Digest - - req.Log.Infof("Evaluating goal %s for digest %s ", goalName, digest) - evaluationTs := time.Now().UTC() - - evalContext := goals.GoalEvaluationContext{ - Log: req.Log, - TeamId: req.Event.WorkspaceId, - Organization: req.Event.Organization, - Goal: goal, - } - - evaluationResult, err := evaluator.EvaluateGoal(ctx, evalContext, sbom, subscriptionResult) - if err != nil { - req.Log.Errorf("Failed to evaluate goal %s for digest %s: %s", goal.Definition, digest, err.Error()) - return skill.NewFailedStatus("Failed to evaluate goal") - } - - if !evaluationResult.EvaluationCompleted { - req.Log.Info("evaluation incomplete") - return skill.NewCompletedStatus("Evaluation incomplete") - } - - goalResults := evaluationResult.Result - - for _, f := range h.transactFilters { - if !f(ctx, req) { - // if not transacting, we return results as part of the skill result - return skill.Status{ - State: skill.Completed, - Reason: fmt.Sprintf("Goal %s evaluated", goalName), - SyncRequest: goalResults, - } - } - } - - storageTuple := util.Decode[[]string](subscriptionResult[0]["previous"]) - - if len(storageTuple) != 2 { - req.Log.Error("could not find previous result in subscription result") - return skill.Status{ - State: skill.Failed, - Reason: "could not find previous result in subscription result", - } - } - - previousResult := transact.PreviousResult{ - StorageId: storageTuple[0], - ConfigHash: storageTuple[1], - } - - _, err = transact.TransactPolicyResult( - ctx, - evalContext, - configuration, - digest, - &previousResult, - evaluationTs, - goalResults, - tx, - req.NewTransaction, - ) - - if err != nil { - req.Log.Errorf("Failed to transact goal results: %s", err.Error()) - return skill.NewFailedStatus(fmt.Sprintf("Failed to transact goal results: %s", err.Error())) - } - - return skill.NewCompletedStatus(fmt.Sprintf("Goal %s evaluated", goalName)) -} - -type intotoStatement struct { - intoto.StatementHeader - Predicate json.RawMessage `json:"predicate"` -} diff --git a/policy/policy_handler/local.go b/policy/policy_handler/local.go deleted file mode 100644 index 5afadac..0000000 --- a/policy/policy_handler/local.go +++ /dev/null @@ -1,151 +0,0 @@ -package policy_handler - -import ( - "bytes" - "compress/gzip" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - - "github.com/atomist-skills/go-skill" - "github.com/atomist-skills/go-skill/policy/goals" - "github.com/atomist-skills/go-skill/policy/types" - v1 "github.com/google/go-containerregistry/pkg/v1" - "olympos.io/encoding/edn" -) - -const eventNameLocalEval = "evaluate_goals_locally" - -type SyncRequestMetadata struct { - QueryResults map[edn.Keyword]edn.RawMessage `edn:"fixedQueryResults"` - Packages []Package `edn:"packages"` // todo remove when no longer used - User string `edn:"imgConfigUser"` // The user from the image config blob // todo remove when no longer used - SBOM string `edn:"sbom"` - ContentType string `edn:"contentType"` - Encoding string `edn:"encoding"` -} - -type Package struct { - Licenses []string `edn:"licenses,omitempty"` // only needed for the license policy evaluation - Name string `edn:"name"` - Namespace string `edn:"namespace"` - Version string `edn:"version"` - Purl string `edn:"purl"` - Type string `edn:"type"` -} - -func WithLocal() Opt { - return func(h *EventHandler) { - h.subscriptionNames = append(h.subscriptionNames, eventNameLocalEval) - h.evalInputProviders = append(h.evalInputProviders, getLocalSubscriptionData) - h.transactFilters = append(h.transactFilters, shouldTransactLocal) - } -} - -func getLocalSubscriptionData(_ context.Context, req skill.RequestContext) (*goals.EvaluationMetadata, skill.Configuration, *types.SBOM, error) { - if req.Event.Context.SyncRequest.Name != eventNameLocalEval { - return nil, skill.Configuration{}, nil, nil - } - - syncRequestMetadata, sbom, err := parseMetadata(req) - if err != nil { - return nil, skill.Configuration{}, nil, err - } - - var commonSubscriptionData goals.ImageSubscriptionQueryResult - if sbom != nil { - commonSubscriptionData = goals.ImageSubscriptionQueryResult{ - ImageDigest: sbom.Source.Image.Digest, - ImagePlatforms: []goals.ImagePlatform{{ - Architecture: sbom.Source.Image.Platform.Architecture, - Os: sbom.Source.Image.Platform.Os, - }}, - } - } else { - commonSubscriptionData = goals.ImageSubscriptionQueryResult{ - ImageDigest: "localDigest", - } - - artifacts := []types.Package{} - for _, pkg := range syncRequestMetadata.Packages { - artifacts = append(artifacts, types.Package{ - Name: pkg.Name, - Version: pkg.Version, - Type: pkg.Type, - Purl: pkg.Purl, - Licenses: pkg.Licenses, - Namespace: pkg.Namespace, - }) - } - - sbom = &types.SBOM{ - Source: types.Source{ - Image: &types.ImageSource{ - Digest: "localDigest", - Config: &v1.ConfigFile{ - Config: v1.Config{ - User: syncRequestMetadata.User, - }, - }, - }, - }, - Artifacts: artifacts, - } - } - - subscriptionData, err := edn.Marshal(commonSubscriptionData) - if err != nil { - return nil, skill.Configuration{}, nil, err - } - - subscriptionResult := map[edn.Keyword]edn.RawMessage{} - subscriptionResult[edn.Keyword("image")] = subscriptionData - - return &goals.EvaluationMetadata{ - SubscriptionResult: []map[edn.Keyword]edn.RawMessage{ - subscriptionResult, - }}, req.Event.Context.SyncRequest.Configuration, sbom, nil -} - -func shouldTransactLocal(_ context.Context, req skill.RequestContext) bool { - return req.Event.Context.SyncRequest.Name != eventNameLocalEval -} - -func parseMetadata(req skill.RequestContext) (SyncRequestMetadata, *types.SBOM, error) { - var srMeta SyncRequestMetadata - err := edn.Unmarshal(req.Event.Context.SyncRequest.Metadata, &srMeta) - if err != nil { - return SyncRequestMetadata{}, nil, fmt.Errorf("failed to unmarshal SyncRequest metadata: %w", err) - } - - if srMeta.SBOM == "" { - return srMeta, nil, nil - } - - decodedSBOM, err := base64.StdEncoding.DecodeString(srMeta.SBOM) - if err != nil { - return srMeta, nil, fmt.Errorf("failed to base64-decode SBOM: %w", err) - } - if srMeta.Encoding == "base64+gzip" { - reader := bytes.NewReader(decodedSBOM) - gzreader, err := gzip.NewReader(reader) - defer gzreader.Close() //nolint:errcheck - if err != nil { - return srMeta, nil, fmt.Errorf("failed to decompress SBOM: %w", err) - } - decodedSBOM, err = io.ReadAll(gzreader) - if err != nil { - return srMeta, nil, fmt.Errorf("failed to base64-decode SBOM: %w", err) - } - } - - var sbom *types.SBOM - // THE SBOM is a JSON here, not edn - if err := json.Unmarshal(decodedSBOM, &sbom); err != nil { - return srMeta, nil, fmt.Errorf("failed to unmarshal SBOM: %w", err) - } - - return srMeta, sbom, nil -} diff --git a/policy/policy_handler/local_test.go b/policy/policy_handler/local_test.go deleted file mode 100644 index ae454ac..0000000 --- a/policy/policy_handler/local_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package policy_handler - -import ( - "os" - "testing" - - "github.com/atomist-skills/go-skill" - "olympos.io/encoding/edn" -) - -func Test_parseMetadata_NullAttestations(t *testing.T) { - req, err := createSyncReqFromFile("./test_data/sync_req_attest_null.edn") - if err != nil { - t.Fatal(err) - } - - _, got, err := parseMetadata(*req) - if err != nil { - t.Fatalf("parseMetadata() error = %v, want nil", err) - return - } - - if got.Attestations != nil { - t.Fatalf("parseMetadata() got.Attestations = %+v, want nil", got.Attestations) - } -} - -func Test_parseMetadata_NoAttestations(t *testing.T) { - req, err := createSyncReqFromFile("./test_data/sync_req_attest_empty.edn") - if err != nil { - t.Fatal(err) - } - - _, got, err := parseMetadata(*req) - if err != nil { - t.Fatalf("parseMetadata() error = %v, want nil", err) - return - } - - if got.Attestations == nil || len(got.Attestations) != 0 { - t.Fatalf("parseMetadata() got.Attestations = %+v, want empty slice", got.Attestations) - } -} - -func Test_parseMetadata_Attestations(t *testing.T) { - req, err := createSyncReqFromFile("./test_data/sync_req_attest.edn") - if err != nil { - t.Fatal(err) - } - - _, got, err := parseMetadata(*req) - if err != nil { - t.Fatalf("parseMetadata() error = %v, want nil", err) - return - } - - if len(got.Attestations) != 2 { - t.Fatalf("parseMetadata() got.Attestations = %+v, want 2", got.Attestations) - } -} - -// createSyncReqFromFile creates a skill.RequestContext from a file. -// The file represents the sync-request payload which contains the base64-encoded and gzipped SBOM from a local evaluation. -func createSyncReqFromFile(filename string) (*skill.RequestContext, error) { - f, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - - var syncReq skill.EventContextSyncRequest - if err := edn.Unmarshal(f, &syncReq); err != nil { - return nil, err - } - - return &skill.RequestContext{ - Event: skill.EventIncoming{ - Context: skill.EventContext{ - SyncRequest: syncReq, - }, - }, - }, nil -} diff --git a/policy/policy_handler/proxy.go b/policy/policy_handler/proxy.go deleted file mode 100644 index 6c8d067..0000000 --- a/policy/policy_handler/proxy.go +++ /dev/null @@ -1,16 +0,0 @@ -package policy_handler - -import ( - "context" - - "github.com/atomist-skills/go-skill" - "github.com/atomist-skills/go-skill/policy/data/proxy" -) - -func WithProxyClient() Opt { - return func(h *EventHandler) { - h.proxyClientProvider = func(ctx context.Context, req skill.RequestContext) proxy.ProxyClient { - return proxy.NewProxyClientFromSkillRequest(ctx, req) - } - } -} diff --git a/policy/policy_handler/subscription.go b/policy/policy_handler/subscription.go deleted file mode 100644 index 301d0c9..0000000 --- a/policy/policy_handler/subscription.go +++ /dev/null @@ -1,175 +0,0 @@ -package policy_handler - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - - "github.com/atomist-skills/go-skill" - "github.com/atomist-skills/go-skill/policy/goals" - "github.com/atomist-skills/go-skill/policy/storage" - "github.com/atomist-skills/go-skill/policy/types" - "github.com/atomist-skills/go-skill/util" - v1 "github.com/google/go-containerregistry/pkg/v1" - intoto "github.com/in-toto/in-toto-golang/in_toto" - "github.com/secure-systems-lab/go-securesystemslib/dsse" - "olympos.io/encoding/edn" -) - -func withSubscription() Opt { - return func(h *EventHandler) { - h.evalInputProviders = append(h.evalInputProviders, getSubscriptionData) - } -} - -func getSubscriptionData(ctx context.Context, req skill.RequestContext) (*goals.EvaluationMetadata, skill.Configuration, *types.SBOM, error) { - if req.Event.Context.Subscription.Name == "" { - return nil, skill.Configuration{}, nil, nil - } - - evalMeta := &goals.EvaluationMetadata{ - SubscriptionResult: req.Event.Context.Subscription.GetResultInMapForm(), - SubscriptionTx: req.Event.Context.Subscription.Metadata.Tx, - SubscriptionBasisT: req.Event.Context.Subscription.Metadata.AfterBasisT, - } - - sb, err := createSBOMFromManifest(ctx, evalMeta.SubscriptionResult) - if err == nil { - req.Log.Debug("found sbom in storage") - return evalMeta, req.Event.Context.Subscription.Configuration, sb, nil - } - - sb, err = createSBOMFromSubscriptionResult(req, evalMeta.SubscriptionResult) - if err != nil { - return nil, skill.Configuration{}, nil, fmt.Errorf("failed to create SBOM from subscription result: %w", err) - } - - return evalMeta, req.Event.Context.Subscription.Configuration, sb, nil -} - -func createSBOMFromManifest(ctx context.Context, subscriptionResult []map[edn.Keyword]edn.RawMessage) (*types.SBOM, error) { - imageEdn, ok := subscriptionResult[0][edn.Keyword("image")] - - if !ok { - return nil, fmt.Errorf("image not found in subscription result") - } - - image := util.Decode[goals.ImageSubscriptionQueryResult](imageEdn) - - if image.ImageRepo == nil { - return nil, fmt.Errorf("image repository not found in subscription result") - } - - ref := image.ImageRepo.Repository - if image.ImageRepo.Host != "hub.docker.com" { - ref = image.ImageRepo.Host + "/" + ref - } - digest := image.ImageDigest - - sst := storage.NewSBOMStore(ctx) - if sb, ok := sst.Read(ref, digest); ok { - return sb, nil - } else { - return nil, fmt.Errorf("sbom not found in storage") - } -} - -func createSBOMFromSubscriptionResult(req skill.RequestContext, subscriptionResult []map[edn.Keyword]edn.RawMessage) (*types.SBOM, error) { - imageEdn, ok := subscriptionResult[0][edn.Keyword("image")] - - if !ok { - return nil, fmt.Errorf("image not found in subscription result") - } - - image := util.Decode[goals.ImageSubscriptionQueryResult](imageEdn) - - attestations := []dsse.Envelope{} - - var provenanceMode *string - - if image.Attestations != nil { - for _, attestation := range image.Attestations { - if attestation.PredicateType == nil { - req.Log.Debug("skipping attestation without predicate type") - continue - } - - intotoStatement := intotoStatement{ - StatementHeader: intoto.StatementHeader{ - PredicateType: *attestation.PredicateType, - }, - } - - req.Log.Debugf("found attestation with predicate type %s", *attestation.PredicateType) - - payloadBytes, _ := json.Marshal(intotoStatement) - - payload := base64.StdEncoding.EncodeToString(payloadBytes) - - env := dsse.Envelope{ - PayloadType: "application/vnd.in-toto+json", - Payload: payload, - } - - attestations = append(attestations, env) - - for _, predicate := range attestation.Predicates { - if predicate.ProvenanceMode != nil { - var mode string - - switch predicate.ProvenanceMode.Ident { - case edn.Keyword("buildkit.provenance.mode/MAX"): - mode = types.BuildKitMaxMode - case edn.Keyword("buildkit.provenance.mode/MIN"): - mode = types.BuildKitMinMode - } - - provenanceMode = &mode - } - } - } - } - - sbom := types.SBOM{ - Source: types.Source{ - Image: &types.ImageSource{ - Digest: image.ImageDigest, - - Config: &v1.ConfigFile{ - Config: v1.Config{ - User: image.User, - }, - }, - }, - }, - Attestations: attestations, - } - - if image.ImagePlatforms != nil && len(image.ImagePlatforms) > 0 { - req.Log.Debugf("found image platform: %s/%s", image.ImagePlatforms[0].Architecture, image.ImagePlatforms[0].Os) - sbom.Source.Image.Platform = types.Platform{ - Architecture: image.ImagePlatforms[0].Architecture, - Os: image.ImagePlatforms[0].Os, - Variant: image.ImagePlatforms[0].Variant, - } - } - - if provenanceMode != nil { - req.Log.Debugf("found provenance with mode %s", *provenanceMode) - sbom.Source.Provenance = &types.Provenance{ - Mode: *provenanceMode, - } - - if image.FromRepo != nil && image.FromReference != nil { - req.Log.Debugf("found provenance data for base image: %s/%s:%s", image.FromRepo.Host, image.FromRepo.Repository, image.FromTag) - sbom.Source.Provenance.BaseImage = &types.ProvenanceBaseImage{ - Digest: image.FromReference.Digest, - Tag: image.FromTag, - Name: fmt.Sprintf("%s/%s", image.FromRepo.Host, image.FromRepo.Repository), - } - } - } - - return &sbom, nil -} diff --git a/policy/policy_handler/sync.go b/policy/policy_handler/sync.go deleted file mode 100644 index 33b807b..0000000 --- a/policy/policy_handler/sync.go +++ /dev/null @@ -1,24 +0,0 @@ -package policy_handler - -import ( - "context" - - "github.com/atomist-skills/go-skill/policy/data/query" - "github.com/atomist-skills/go-skill/policy/goals" - - "github.com/atomist-skills/go-skill" -) - -func WithSyncQuery() Opt { - return func(h *EventHandler) { - h.queryClientProviders = append(h.queryClientProviders, getSyncQueryClients) - } -} - -func getSyncQueryClients(ctx context.Context, req skill.RequestContext, evalMeta goals.EvaluationMetadata) ([]query.QueryClient, error) { - gqlDs := query.NewSyncGraphqlQueryClientFromSkillRequest(ctx, req, evalMeta) - - return []query.QueryClient{ - gqlDs, - }, nil -} diff --git a/policy/policy_handler/test_data/sync_req_attest.edn b/policy/policy_handler/test_data/sync_req_attest.edn deleted file mode 100644 index 9d21644..0000000 --- a/policy/policy_handler/test_data/sync_req_attest.edn +++ /dev/null @@ -1 +0,0 @@ -{:name "evaluate_goals_locally" :metadata {:contentType "application/json" :encoding "base64+gzip" :sbom "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"} :configuration {:eTag "" :name "" :displayName "" :updatedAt "" :parameters nil :enabled false :author {:email "" :name ""}}} diff --git a/policy/policy_handler/test_data/sync_req_attest_empty.edn b/policy/policy_handler/test_data/sync_req_attest_empty.edn deleted file mode 100644 index b204e81..0000000 --- a/policy/policy_handler/test_data/sync_req_attest_empty.edn +++ /dev/null @@ -1 +0,0 @@ -{:name "evaluate_goals_locally" :metadata {:contentType "application/json" :encoding "base64+gzip" :sbom "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"} :configuration {:eTag "" :name "" :displayName "" :updatedAt "" :parameters nil :enabled false :author {:email "" :name ""}}} diff --git a/policy/policy_handler/test_data/sync_req_attest_null.edn b/policy/policy_handler/test_data/sync_req_attest_null.edn deleted file mode 100644 index b80440b..0000000 --- a/policy/policy_handler/test_data/sync_req_attest_null.edn +++ /dev/null @@ -1 +0,0 @@ -{:name "evaluate_goals_locally" :metadata {:contentType "application/json" :encoding "base64+gzip" :sbom "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"} :configuration {:eTag "" :name "" :displayName "" :updatedAt "" :parameters nil :enabled false :author {:email "" :name ""}}} diff --git a/policy/storage/fs.go b/policy/storage/fs.go deleted file mode 100644 index 7d620e4..0000000 --- a/policy/storage/fs.go +++ /dev/null @@ -1,44 +0,0 @@ -package storage - -import ( - "context" - "os" - - "github.com/atomist-skills/go-skill/policy/goals" - - "github.com/atomist-skills/go-skill" - "olympos.io/encoding/edn" -) - -type FsStorage struct { - path string -} - -func NewFsStorage(ctx context.Context) (EvaluationStorage, error) { - return &FsStorage{ - path: os.TempDir(), - }, nil -} - -func (f *FsStorage) Store(ctx context.Context, results []goals.GoalEvaluationQueryResult, storageId string, log skill.Logger) error { - log.Infof("Storing %d results", len(results)) - - content, err := edn.Marshal(results) - if err != nil { - return err - } - log.Infof("Content to store: %s", string(content)) - - file, err := os.Create(f.path + "/results.edn") - if err != nil { - return err - } - defer file.Close() - - _, err = file.WriteString(string(content)) - if err != nil { - return err - } - - return nil -} diff --git a/policy/storage/gcs.go b/policy/storage/gcs.go deleted file mode 100644 index 8859b93..0000000 --- a/policy/storage/gcs.go +++ /dev/null @@ -1,88 +0,0 @@ -package storage - -import ( - "context" - "net/http" - "os" - - "github.com/atomist-skills/go-skill/environment" - "github.com/atomist-skills/go-skill/policy/goals" - - "cloud.google.com/go/storage" - "github.com/atomist-skills/go-skill" - "google.golang.org/api/googleapi" - "olympos.io/encoding/edn" -) - -func getBucketName() string { - bucket := os.Getenv("POLICY_STORAGE_BUCKET") - if bucket != "" { - return bucket - } - - if environment.IsStaging() { - return "atm-policy-evaluation-results-staging" - } - - return "atm-policy-evaluation-results" -} - -type GcsStorage struct { - client *storage.Client - bucketName string - environment string -} - -func NewGcsStorage(ctx context.Context) (*GcsStorage, error) { - storageClient, err := storage.NewClient(ctx) - if err != nil { - return nil, err - } - - return &GcsStorage{ - client: storageClient, - bucketName: getBucketName(), - }, nil -} - -func (gcs *GcsStorage) Store(ctx context.Context, results []goals.GoalEvaluationQueryResult, storageId string, log skill.Logger) error { - log.Infof("Storing %d results", len(results)) - - content, err := edn.Marshal(results) - if err != nil { - return err - } - log.Infof("Content to store: %s", string(content)) - - environmentBucketName := gcs.bucketName - - if gcs.environment != "" { - environmentBucketName = gcs.environment + "-" + gcs.environment - } - - log.Infof("Storing results in bucket %s", environmentBucketName) - - bucket := gcs.client.Bucket(environmentBucketName) - storageObject := bucket.Object(storageId) - - w := storageObject.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx) - - _, err = w.Write(content) - if err != nil { - return err - } - - if err := w.Close(); err != nil { - switch e := err.(type) { - case *googleapi.Error: - // ignore if object already exists - if e.Code != http.StatusPreconditionFailed { - return err - } - default: - return err - } - } - - return nil -} diff --git a/policy/storage/sbom.go b/policy/storage/sbom.go deleted file mode 100644 index 179027e..0000000 --- a/policy/storage/sbom.go +++ /dev/null @@ -1,71 +0,0 @@ -package storage - -import ( - "context" - "encoding/json" - "net/http" - "strings" - - "github.com/atomist-skills/go-skill/environment" - - "cloud.google.com/go/storage" - "github.com/atomist-skills/go-skill/policy/types" - "google.golang.org/api/googleapi" -) - -const ( - bucketName = "atm-prod-stored-manifests" - stagingBucketName = "atm-staging-stored-manifests" -) - -type ( - Cache struct { - ctx context.Context - client *storage.Client - bucketName string - directory string - } -) - -func (c *Cache) Read(ref, digest string) (*types.SBOM, bool) { - bucket := c.client.Bucket(c.bucketName) - seg := make([]string, 0) - seg = append(seg, c.directory) - seg = append(seg, ref) - seg = append(seg, digest) - - storageObject := bucket.Object(strings.Join(seg, "/")) - - r, err := storageObject.NewReader(c.ctx) - if err != nil { - switch e := err.(type) { - case *googleapi.Error: - if e.Code == http.StatusNotFound { - return nil, false - } - default: - return nil, false - } - } - defer r.Close() - - sb := &types.SBOM{} - if err := json.NewDecoder(r).Decode(sb); err != nil { - return nil, false - } - return sb, true -} - -func NewSBOMStore(ctx context.Context) *Cache { - gcs, _ := storage.NewClient(ctx) - bn := bucketName - if environment.IsStaging() { - bn = stagingBucketName - } - return &Cache{ - ctx: ctx, - client: gcs, - bucketName: bn, - directory: "sbom", - } -} diff --git a/policy/storage/storage.go b/policy/storage/storage.go deleted file mode 100644 index c4f99e5..0000000 --- a/policy/storage/storage.go +++ /dev/null @@ -1,23 +0,0 @@ -package storage - -import ( - "context" - "os" - - "github.com/atomist-skills/go-skill/policy/goals" - - "github.com/atomist-skills/go-skill" -) - -type EvaluationStorage interface { - Store(ctx context.Context, results []goals.GoalEvaluationQueryResult, storageId string, log skill.Logger) error -} - -// NewEvaluationStorage creates a new EvaluationStorage object based on the LOCAL_DEBUG environment variable. -func NewEvaluationStorage(ctx context.Context) (EvaluationStorage, error) { - if os.Getenv("LOCAL_DEBUG") == "true" { - return NewFsStorage(ctx) - } - - return NewGcsStorage(ctx) -} diff --git a/policy/transact/transact.go b/policy/transact/transact.go deleted file mode 100644 index 3d32181..0000000 --- a/policy/transact/transact.go +++ /dev/null @@ -1,85 +0,0 @@ -package transact - -import ( - "context" - "fmt" - "time" - - "github.com/atomist-skills/go-skill" - "github.com/atomist-skills/go-skill/policy/goals" - "github.com/atomist-skills/go-skill/policy/storage" -) - -type PreviousResult struct { - StorageId string - ConfigHash string -} - -func TransactPolicyResult( - ctx context.Context, - evalCtx goals.GoalEvaluationContext, - configuration skill.Configuration, - digest string, - previousResult *PreviousResult, - evaluationTs time.Time, - goalResults []goals.GoalEvaluationQueryResult, - tx int64, - newTransaction func() skill.Transaction, -) (*goals.GoalEvaluationResultEntity, error) { - var previousConfigHash, previousStorageId string - if previousResult == nil { - previousConfigHash = "n/a" - previousStorageId = "n/a" - } else { - previousConfigHash = previousResult.ConfigHash - previousStorageId = previousResult.StorageId - } - - if goalResults == nil { - evalCtx.Log.Infof("returned no data for digest %s", digest) - } - - es, err := storage.NewEvaluationStorage(ctx) - if err != nil { - return nil, fmt.Errorf("Failed to create evaluation storage: %s", err.Error()) - } - - configDiffer, configHash, err := goals.GoalConfigsDiffer(evalCtx.Log, configuration, digest, previousConfigHash) - if err != nil { - evalCtx.Log.Errorf("Failed to check if config hash changed for digest: %s", digest, err) - evalCtx.Log.Warnf("Will continue with the evaluation nonetheless") - configDiffer = true - } - - differ, storageId, err := goals.GoalResultsDiffer(evalCtx.Log, goalResults, digest, previousStorageId) - if err != nil { - evalCtx.Log.Errorf("Failed to check if goal results changed for digest: %s", digest, err) - evalCtx.Log.Warnf("Will continue with the evaluation nonetheless") - differ = true - } - - if differ && goalResults != nil { - if err := es.Store(ctx, goalResults, storageId, evalCtx.Log); err != nil { - return nil, fmt.Errorf("Failed to store evaluation results for digest %s: %s", digest, err.Error()) - } - } - - var resultEntity *goals.GoalEvaluationResultEntity - if differ || configDiffer { - shouldRetract := previousStorageId != "no-data" && previousStorageId != "n/a" && storageId == "no-data" - entity := goals.CreateEntitiesFromResults(goalResults, evalCtx.Goal.Definition, evalCtx.Goal.Configuration, digest, storageId, configHash, evaluationTs, tx, shouldRetract) - resultEntity = &entity - } - - if resultEntity != nil { - err = newTransaction().AddEntities(*resultEntity).Transact() - if err != nil { - return nil, fmt.Errorf("Failed to transact goal results: %s", err.Error()) - } - evalCtx.Log.Info("Goal results transacted") - } else { - evalCtx.Log.Info("No goal results to transact") - } - - return resultEntity, nil -}