Repository: deislabs/ratify
Pull request #118 (2021-12-17 20:15:38)

Add licensechecker plugin and docs
This is a basic implementation of a license checking verifier that utilizes an SPDX tag-value SBoM

sajayantony: @sozercan - you might be interested in this. 
sajayantony: Sharing an offline conversation with @etrexel 

> [1:14 PM] Sajay Antony
>     Nice progress Eric - To make this validate more stuff in the SPDX - should we get this in and refactor this to say validate package version as well?  
> ​[1:16 PM] Eric Trexel
>     Yeah if we wanted to grow this we can expand the config to add additional metadata to validate. I am still browsing the spec to see what would make the most sense to add next
> ​[1:19 PM] Sajay Antony
>     I'm guessing SPDX would be just one binary that supports some options rego inputs. Is that the plan? 
> ​[1:21 PM] Eric Trexel
>     Yes. We could convert tag-value SPDX to json and it could be passed directly to rego evaluation
> 

Patch:
diff --git a/Makefile b/Makefile
index c298a37..f3bd665 100644
--- a/Makefile
+++ b/Makefile
@@ -24,6 +24,7 @@ build-cli:
 .PHONY: build-plugins
 build-plugins: 
 	go build -o ./bin/plugins/ ./plugins/verifier/cosign
+	go build -o ./bin/plugins/ ./plugins/verifier/licensechecker
 	go build -o ./bin/plugins/ ./plugins/verifier/sample
 	go build -o ./bin/plugins/ ./plugins/verifier/sbom
 
diff --git a/README.md b/README.md
index 21b6758..20ea41d 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ artifact ratification.
 by its developers, nor is it "supported" software.
 
 ## Table of Contents
-
+- [Community Meetings](#community-meetings)
 - [Quick Start](#quick-start)
 - [Documents](#documents)
 - [Code of Conduct](#code-of-conduct)
@@ -17,6 +17,11 @@ by its developers, nor is it "supported" software.
 - [Licensing](#licensing)
 - [Trademark](#trademark)
 
+## Community meetings
+
+- Agenda: https://hackmd.io/ABueHjizRz2iFQpWnQrnNA
+- Calendar: https://calendar.google.com/event?action=TEMPLATE&tmeid=MXFuMjM0NTlucHRiZDBwNnU0cGQ2OGlxZ2pfMjAyMjAxMjZUMDAwMDAwWiA5YmN1MXYzdmJkaG5ubWY2YnIwOHNzazA1NEBn&tmsrc=9bcu1v3vbdhnnmf6br08ssk054%40group.calendar.google.com&scp=ALL
+
 ## Quick Start
 
 Try out ratify in Kuberenetes through Gatekeeper as the admission controller.
diff --git a/config/config.json b/config/config.json
index 4aec2eb..d981320 100644
--- a/config/config.json
+++ b/config/config.json
@@ -12,7 +12,8 @@
         "version": "1.0.0",
         "artifactVerificationPolicies": {
             "application/vnd.cncf.notary.v2": "any",
-            "org.example.sbom.v0": "all"
+            "org.example.sbom.v0": "all",
+            "application/vnd.ratify.spdx.v0": "any"
         }
     },
     "verifiers": {
@@ -29,6 +30,11 @@
                 "name":"sbom",
                 "artifactTypes":"org.example.sbom.v0",
                 "nestedReferences": "application/vnd.cncf.notary.v2.signature"
+            },
+            {
+                "name": "licensechecker",
+                "artifactTypes": "application/vnd.ratify.spdx.v0",
+                "allowedLicenses": ["MIT"]
             }
             
         ]
diff --git a/docs/working-with-spdx.md b/docs/working-with-spdx.md
new file mode 100644
index 0000000..2130d60
--- /dev/null
+++ b/docs/working-with-spdx.md
@@ -0,0 +1,158 @@
+# Working with SPDX
+SPDX is a popular specification for representing software bill of material (SBoM) information. Once an SBoM has been
+generated there are a number of ways to validate the contents. This guide will walk through the steps to generate an
+SBoM from a container image and validate some information from the SBoM using the example `licensechecker` plugin. By
+the end of this guide you will have:
+
+- Built a docker image
+- Generated an SBoM for that image in SPDX tag-value format
+- Pushed the SBoM to a registry
+- Configured Ratify to validate the licenses used in the docker image
+- Run the validation via the CLI
+
+For more information on additional metadata that is captured in SPDX visit the [SPDX specification docs](https://spdx.dev/specifications/).
+
+## Setup
+In order to complete this guide you will need some tools.
+
+- [docker](https://www.docker.com/get-started): This tool will be used to build the container image
+- [syft](https://github.com/anchore/syft): This tool will be used to generate an SBoM in SPDX tag-value format
+- [oras](https://github.com/oras-project/oras): This tool will be used to push the generated SBoM to the registry
+- [ratify](https://github.com/deislabs/ratify): This tool will be used to validate the SBoM
+
+You will also need a registry to push your container image and SBoM to. For this guide deploy a local registry with
+oras artifacts support:
+```shell
+docker run --name sbom_demo -d -p 5000:5000 ghcr.io/oras-project/registry:v0.0.3-alpha
+```
+
+## Build the Image
+First we need to build the container image we will be using:
+```shell
+docker build -t localhost:5000/only-spdx:v1 https://github.com/wabbit-networks/net-monitor.git\#main
+```
+
+## Generate the SBoM
+Generate an SPDX SBoM using syft:
+```shell
+syft -o spdx --file sbom.spdx localhost:5000/only-spdx:v1
+```
+
+## Push the image and SBoM
+After building the image and generating the SBoM push them to the registry:
+```shell
+docker push localhost:5000/only-spdx:v1
+
+oras push localhost:5000/only-spdx \
+  --artifact-type application/vnd.ratify.spdx.v0 \
+  --subject localhost:5000/only-spdx:v1 \
+  --plain-http \
+  sbom.spdx:application/text
+```
+
+## Validating the SBoM
+If you have not done so already, build and install ratify:
+```shell
+make build
+make install
+```
+
+Next we will create the config file we will use for validation:
+```shell
+cat <<'EOF' >> spdxconfig.json
+{
+    "stores": {
+        "version": "1.0.0",
+        "plugins": [
+            {
+                "name": "oras",
+                "localCachePath": "./local_oras_cache"
+            }
+        ]
+    },
+    "policies": {
+        "version": "1.0.0",
+        "artifactVerificationPolicies": {
+            "application/vnd.ratify.spdx.v0": "all"
+        }
+    },
+    "verifiers": {
+        "version": "1.0.0",
+        "plugins": [
+            {
+                "name": "licensechecker",
+                "artifactTypes": "application/vnd.ratify.spdx.v0",
+                "allowedLicenses": [
+                    "GPL-2.0-only",
+                    "MIT",
+                    "OpenSSL",
+                    "BSD-2-Clause AND BSD-3-Clause",
+                    "Zlib",
+                    "MPL-2.0 AND MIT",
+                    "ISC"
+                ]
+            }
+
+        ]
+
+    }
+}
+EOF
+```
+
+This config file will only enable the `licensechecker` verifier and configure it to check that only the licenses in the
+`allowedLicenses` list are used. Note here that the `artifactType` of the SBoM is arbitrary and can be whatever you
+want to specify it as so long as you are consistently use it when pushing and validating.
+
+We are now ready to validate our image:
+```shell
+ratify verify -c spdxconfig.json -s localhost:5000/only-spdx:v1
+
+{
+  "isSuccess": true,
+  "verifierReports": [
+    {
+      "subject": "localhost:5000/only-spdx:v1",
+      "isSuccess": true,
+      "name": "licensechecker",
+      "results": [
+        "License Check: SUCCESS",
+        "All packages have allowed licenses"
+      ]
+    }
+  ]
+}
+```
+
+Now remove one of the license lines from the config file, for example `MIT`, and run the validation again:
+```shell
+ratify verify -c spdxconfig.json -s localhost:5000/only-spdx:v1
+
+{
+  "isSuccess": false,
+  "verifierReports": [
+    {
+      "subject": "localhost:5000/only-spdx:v1",
+      "name": "licensechecker",
+      "results": [
+        "License Check: FAILED",
+        "package 'alpine-keys' has unpermitted license 'MIT'",
+        "package 'musl-utils' has unpermitted license 'MIT'",
+        "package 'musl' has unpermitted license 'MIT'"
+      ]
+    }
+  ]
+}
+```
+
+We have successfully used Ratify to validate the contents of an SPDX SBoM!
+
+## Cleaning Up
+Remove the registry container, the guide sbom, and the guide config:
+```shell
+docker container stop sbom_demo
+docker container rm sbom_demo
+
+rm sbom.spdx
+rm spdxconfig.json
+```
diff --git a/go.mod b/go.mod
index 5749a72..719c912 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
 	github.com/sigstore/cosign v1.1.0
 	github.com/sigstore/sigstore v0.0.0-20210729211320-56a91f560f44
 	github.com/sirupsen/logrus v1.8.1
+	github.com/spdx/tools-golang v0.2.0
 	github.com/spf13/cobra v1.2.1
 	github.com/xlab/treeprint v1.1.0
 	oras.land/oras-go v0.5.1-0.20211129233629-3b1e924da9d7
diff --git a/go.sum b/go.sum
index 6235d68..b5a43be 100644
--- a/go.sum
+++ b/go.sum
@@ -1525,6 +1525,9 @@ github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4l
 github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
 github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=
+github.com/spdx/tools-golang v0.2.0 h1:KBNcw7xvVycRWeCWZK/5xQJA+plymW1+rTCs8ekJDro=
+github.com/spdx/tools-golang v0.2.0/go.mod h1:RO4Y3IFROJnz+43JKm1YOrbtgQNljW4gAPpA/sY2eqo=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
diff --git a/httpserver/Dockerfile b/httpserver/Dockerfile
index 4181f69..78a1e4b 100644
--- a/httpserver/Dockerfile
+++ b/httpserver/Dockerfile
@@ -1,29 +1,43 @@
-FROM golang:alpine3.14 as builder
+ARG BUILDPLATFORM="linux/amd64"
+ARG BUILDERIMAGE="golang:1.17"
+ARG BASEIMAGE="gcr.io/distroless/static:nonroot"
 
-WORKDIR /app
+FROM --platform=$BUILDPLATFORM $BUILDERIMAGE as builder
 
-COPY go.mod .
+ARG TARGETPLATFORM
+ARG TARGETOS
+ARG TARGETARCH
+ARG TARGETVARIANT=""
+ARG LDFLAGS
 
-COPY go.sum .
+ENV CGO_ENABLED=0 \
+    GOOS=${TARGETOS} \
+    GOARCH=${TARGETARCH} \
+    GOARM=${TARGETVARIANT}
 
-RUN go mod download
+WORKDIR /app
 
 COPY . .
 
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /app/out/ /app/cmd/ratify 
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /app/out/plugins/ /app/plugins/verifier/sbom
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /app/out/plugins/ /app/plugins/verifier/cosign
+RUN go build -o /app/out/ /app/cmd/ratify 
+RUN go build -o /app/out/plugins/ /app/plugins/verifier/sbom
+RUN go build -o /app/out/plugins/ /app/plugins/verifier/cosign
 
-FROM alpine:3.14
+FROM $BASEIMAGE
 LABEL org.opencontainers.image.source https://github.com/deislabs/ratify
 
-RUN set -ex \
-    && apk add --no-cache ca-certificates apache2-utils
-
 ARG RATIFY_FOLDER=$HOME/.ratify/
+
+WORKDIR /
+
 COPY --from=builder /app/out/ratify /app/
 COPY --from=builder /app/out/plugins ${RATIFY_FOLDER}/plugins
 COPY --from=builder /app/config/config.json ${RATIFY_FOLDER}
+
 ENV RATIFY_CONFIG=${RATIFY_FOLDER}
+
 EXPOSE 6001
-CMD ["./app/ratify", "serve", "--http", ":6001"]
\ No newline at end of file
+
+USER 65532:65532
+
+ENTRYPOINT ["/app/ratify", "serve", "--http", ":6001"]
diff --git a/httpserver/handlers.go b/httpserver/handlers.go
index e195b34..93f469b 100644
--- a/httpserver/handlers.go
+++ b/httpserver/handlers.go
@@ -78,10 +78,10 @@ func (server *Server) verify(ctx context.Context, w http.ResponseWriter, r *http
 			})
 		}
 	}
-	return sendResponse(&results, "", w)
+	return sendResponse(&results, "", w, http.StatusOK)
 }
 
-func sendResponse(results *[]externaldata.Item, systemErr string, w http.ResponseWriter) error {
+func sendResponse(results *[]externaldata.Item, systemErr string, w http.ResponseWriter, respCode int) error {
 	response := externaldata.ProviderResponse{
 		APIVersion: apiVersion,
 		Kind:       "ProviderResponse",
@@ -93,7 +93,7 @@ func sendResponse(results *[]externaldata.Item, systemErr string, w http.Respons
 		response.Response.SystemError = systemErr
 	}
 
-	w.WriteHeader(http.StatusOK)
+	w.WriteHeader(respCode)
 	return json.NewEncoder(w).Encode(response)
 }
 
@@ -118,7 +118,7 @@ func processTimeout(h ContextHandler, duration time.Duration) ContextHandler {
 		}
 
 		if err != nil {
-			return sendResponse(nil, fmt.Sprintf("validate operation failed with error %v", err), w)
+			return sendResponse(nil, fmt.Sprintf("validate operation failed with error %v", err), w, http.StatusInternalServerError)
 		}
 
 		return nil
diff --git a/pkg/executor/core/executor.go b/pkg/executor/core/executor.go
index f667982..beb4274 100644
--- a/pkg/executor/core/executor.go
+++ b/pkg/executor/core/executor.go
@@ -33,7 +33,7 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-const defaultRequestTimeoutSeconds = 8
+const defaultRequestTimeoutMilliseconds = 2800
 
 // Executor describes an execution engine that queries the stores for the supply chain content,
 // runs them through the verifiers as governed by the policy enforcer
@@ -58,11 +58,11 @@ func (executor Executor) VerifySubject(ctx context.Context, verifyParameters e.V
 }
 
 func (executor Executor) GetVerifyRequestTimeout() time.Duration {
-	timeoutSeconds := defaultRequestTimeoutSeconds
+	timeoutMilliSeconds := defaultRequestTimeoutMilliseconds
 	if executor.Config != nil && executor.Config.RequestTimeout != nil {
-		timeoutSeconds = *executor.Config.RequestTimeout
+		timeoutMilliSeconds = *executor.Config.RequestTimeout
 	}
-	return time.Duration(timeoutSeconds) * time.Second
+	return time.Duration(timeoutMilliSeconds) * time.Millisecond
 }
 
 func (executor Executor) verifySubjectInternal(ctx context.Context, verifyParameters e.VerifyParameters) (types.VerifyResult, error) {
diff --git a/plugins/verifier/licensechecker/licensechecker.go b/plugins/verifier/licensechecker/licensechecker.go
new file mode 100644
index 0000000..c04fe14
--- /dev/null
+++ b/plugins/verifier/licensechecker/licensechecker.go
@@ -0,0 +1,103 @@
+/*
+Copyright The Ratify Authors.
+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 main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/deislabs/ratify/plugins/verifier/licensechecker/utils"
+
+	"github.com/deislabs/ratify/pkg/common"
+	"github.com/deislabs/ratify/pkg/ocispecs"
+	"github.com/deislabs/ratify/pkg/referrerstore"
+	_ "github.com/deislabs/ratify/pkg/referrerstore/oras"
+	"github.com/deislabs/ratify/pkg/verifier"
+	"github.com/deislabs/ratify/pkg/verifier/plugin/skel"
+)
+
+type PluginConfig struct {
+	Name            string   `json:"name"`
+	AllowedLicenses []string `json:"allowedLicenses"`
+}
+
+type PluginInputConfig struct {
+	Config PluginConfig `json:"config"`
+}
+
+func main() {
+	skel.PluginMain("licensechecker", "1.0.0", VerifyReference, []string{"1.0.0"})
+}
+
+func parseInput(stdin []byte) (*PluginConfig, error) {
+	conf := PluginInputConfig{}
+
+	if err := json.Unmarshal(stdin, &conf); err != nil {
+		return nil, fmt.Errorf("failed to parse stdin for input: %v", err)
+	}
+
+	return &conf.Config, nil
+}
+
+func VerifyReference(args *skel.CmdArgs, subjectReference common.Reference, descriptor ocispecs.ReferenceDescriptor, store referrerstore.ReferrerStore) (*verifier.VerifierResult, error) {
+	input, err := parseInput(args.StdinData)
+	if err != nil {
+		return nil, err
+	}
+
+	allowedLicenses := utils.LoadAllowedLicenses(input.AllowedLicenses)
+
+	ctx := context.Background()
+	referenceManifest, err := store.GetReferenceManifest(ctx, subjectReference, descriptor)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, blobDesc := range referenceManifest.Blobs {
+		refBlob, err := store.GetBlobContent(ctx, subjectReference, blobDesc.Digest)
+		if err != nil {
+			return nil, err
+		}
+
+		spdxDoc, err := utils.BlobToSPDX(refBlob)
+		if err != nil {
+			return nil, err
+		}
+
+		packageLicenses := utils.GetPackageLicenses(*spdxDoc)
+		disallowedLicenses := utils.FilterPackageLicenses(packageLicenses, allowedLicenses)
+
+		if len(disallowedLicenses) > 0 {
+			resultStrings := []string{"License Check: FAILED"}
+			for _, disallowedLicense := range disallowedLicenses {
+				s := fmt.Sprintf("package '%s' has unpermitted license '%s'", disallowedLicense.PackageName, disallowedLicense.PackageLicense)
+				resultStrings = append(resultStrings, s)
+			}
+			return &verifier.VerifierResult{
+				Name:      input.Name,
+				IsSuccess: false,
+				Results:   resultStrings,
+			}, nil
+		}
+	}
+
+	return &verifier.VerifierResult{
+		Name:      input.Name,
+		IsSuccess: true,
+		Results:   []string{"License Check: SUCCESS", "All packages have allowed licenses"},
+	}, nil
+}
diff --git a/plugins/verifier/licensechecker/utils/spdxutils.go b/plugins/verifier/licensechecker/utils/spdxutils.go
new file mode 100644
index 0000000..13cd9d1
--- /dev/null
+++ b/plugins/verifier/licensechecker/utils/spdxutils.go
@@ -0,0 +1,59 @@
+/*
+Copyright The Ratify Authors.
+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 utils
+
+import (
+	"strings"
+
+	"github.com/spdx/tools-golang/spdx"
+	"github.com/spdx/tools-golang/tvloader"
+)
+
+func BlobToSPDX(bytes []byte) (*spdx.Document2_2, error) {
+	raw := string(bytes)
+	reader := strings.NewReader(raw)
+	return tvloader.Load2_2(reader)
+}
+
+func GetPackageLicenses(doc spdx.Document2_2) []PackageLicense {
+	var output []PackageLicense
+	for _, p := range doc.Packages {
+		output = append(output, PackageLicense{
+			PackageName:    p.PackageName,
+			PackageLicense: p.PackageLicenseConcluded,
+		})
+	}
+	return output
+}
+
+func LoadAllowedLicenses(licenses []string) map[string]struct{} {
+	output := map[string]struct{}{}
+	for _, license := range licenses {
+		output[license] = struct{}{}
+	}
+	return output
+}
+
+func FilterPackageLicenses(packageLicenses []PackageLicense, allowedLicenses map[string]struct{}) []PackageLicense {
+	var output []PackageLicense
+	for _, packageLicense := range packageLicenses {
+		_, ok := allowedLicenses[packageLicense.PackageLicense]
+		if !ok {
+			output = append(output, packageLicense)
+		}
+	}
+	return output
+}
diff --git a/plugins/verifier/licensechecker/utils/types.go b/plugins/verifier/licensechecker/utils/types.go
new file mode 100644
index 0000000..72da7ba
--- /dev/null
+++ b/plugins/verifier/licensechecker/utils/types.go
@@ -0,0 +1,21 @@
+/*
+Copyright The Ratify Authors.
+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 utils
+
+type PackageLicense struct {
+	PackageName    string
+	PackageLicense string
+}


Test patch:
diff --git a/httpserver/server_test.go b/httpserver/server_test.go
new file mode 100644
index 0000000..2ec14e7
--- /dev/null
+++ b/httpserver/server_test.go
@@ -0,0 +1,85 @@
+/*
+Copyright The Ratify Authors.
+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 httpserver
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"github.com/deislabs/ratify/pkg/executor/core"
+	"github.com/deislabs/ratify/pkg/ocispecs"
+	config "github.com/deislabs/ratify/pkg/policyprovider/configpolicy"
+	"github.com/deislabs/ratify/pkg/referrerstore"
+	"github.com/deislabs/ratify/pkg/referrerstore/mocks"
+	"github.com/deislabs/ratify/pkg/verifier"
+	"github.com/open-policy-agent/frameworks/constraint/pkg/externaldata"
+	"github.com/opencontainers/go-digest"
+)
+
+func TestServer_Timeout_Failed(t *testing.T) {
+	timeoutDuration := 6
+	testImageName := "localhost:5000/net-monitor:v1"
+	t.Run("server_timeout_fail", func(t *testing.T) {
+		body := new(bytes.Buffer)
+		json.NewEncoder(body).Encode(externaldata.NewProviderRequest([]string{testImageName}))
+		request := httptest.NewRequest(http.MethodPost, "/ratify/gatekeeper/v1/verify", bytes.NewReader(body.Bytes()))
+		responseRecorder := httptest.NewRecorder()
+
+		testDigest := digest.FromString("test")
+		configPolicy := config.PolicyEnforcer{}
+		store := &mocks.TestStore{References: []ocispecs.ReferenceDescriptor{
+			{
+				ArtifactType: "test-type1",
+			}},
+			ResolveMap: map[string]digest.Digest{
+				"v1": testDigest,
+			},
+		}
+		ver := &core.TestVerifier{
+			CanVerifyFunc: func(at string) bool {
+				return at == "test-type1"
+			},
+			VerifyResult: func(artifactType string) bool {
+				time.Sleep(time.Duration(timeoutDuration) * time.Second)
+				return true
+			},
+		}
+
+		ex := &core.Executor{
+			PolicyEnforcer: configPolicy,
+			ReferrerStores: []referrerstore.ReferrerStore{store},
+			Verifiers:      []verifier.ReferenceVerifier{ver},
+		}
+		server := &Server{
+			Executor: ex,
+			Context:  request.Context(),
+		}
+
+		handler := contextHandler{
+			context: server.Context,
+			handler: processTimeout(server.verify, server.Executor.GetVerifyRequestTimeout()),
+		}
+
+		handler.ServeHTTP(responseRecorder, request)
+		if responseRecorder.Code != http.StatusInternalServerError {
+			t.Errorf("Want status '%d', got '%d'", http.StatusInternalServerError, responseRecorder.Code)
+		}
+	})
+}
diff --git a/pkg/executor/core/executor_test.go b/pkg/executor/core/executor_test.go
index 57f2a1c..96a7cd1 100644
--- a/pkg/executor/core/executor_test.go
+++ b/pkg/executor/core/executor_test.go
@@ -105,10 +105,10 @@ func TestVerifySubject_CanVerify_ExpectedResults(t *testing.T) {
 		},
 	}
 	ver := &TestVerifier{
-		canVerify: func(at string) bool {
+		CanVerifyFunc: func(at string) bool {
 			return at == "test-type1"
 		},
-		verifyResult: func(artifactType string) bool {
+		VerifyResult: func(artifactType string) bool {
 			return true
 		},
 	}
@@ -153,10 +153,10 @@ func TestVerifySubject_VerifyFailures_ExpectedResults(t *testing.T) {
 		},
 	}
 	ver := &TestVerifier{
-		canVerify: func(at string) bool {
+		CanVerifyFunc: func(at string) bool {
 			return true
 		},
-		verifyResult: func(artifactType string) bool {
+		VerifyResult: func(artifactType string) bool {
 			if artifactType == "test-type1" {
 				return false
 			}
@@ -205,10 +205,10 @@ func TestVerifySubject_VerifySuccess_ExpectedResults(t *testing.T) {
 		},
 	}
 	ver := &TestVerifier{
-		canVerify: func(at string) bool {
+		CanVerifyFunc: func(at string) bool {
 			return true
 		},
-		verifyResult: func(artifactType string) bool {
+		VerifyResult: func(artifactType string) bool {
 			return true
 		},
 	}
diff --git a/pkg/executor/core/testtypes.go b/pkg/executor/core/testtypes.go
index 4537657..42f006b 100644
--- a/pkg/executor/core/testtypes.go
+++ b/pkg/executor/core/testtypes.go
@@ -26,8 +26,8 @@ import (
 )
 
 type TestVerifier struct {
-	canVerify    func(artifactType string) bool
-	verifyResult func(artifactType string) bool
+	CanVerifyFunc func(artifactType string) bool
+	VerifyResult  func(artifactType string) bool
 }
 
 func (s *TestVerifier) Name() string {
@@ -35,7 +35,7 @@ func (s *TestVerifier) Name() string {
 }
 
 func (s *TestVerifier) CanVerify(ctx context.Context, referenceDescriptor ocispecs.ReferenceDescriptor) bool {
-	return s.canVerify(referenceDescriptor.ArtifactType)
+	return s.CanVerifyFunc(referenceDescriptor.ArtifactType)
 }
 
 func (s *TestVerifier) Verify(ctx context.Context,
@@ -44,6 +44,6 @@ func (s *TestVerifier) Verify(ctx context.Context,
 	referrerStore referrerstore.ReferrerStore,
 	executor executor.Executor) (verifier.VerifierResult, error) {
 	return verifier.VerifierResult{
-		IsSuccess: s.verifyResult(referenceDescriptor.ArtifactType),
+		IsSuccess: s.VerifyResult(referenceDescriptor.ArtifactType),
 	}, nil
 }
diff --git a/plugins/verifier/licensechecker/utils/spdxutils_test.go b/plugins/verifier/licensechecker/utils/spdxutils_test.go
new file mode 100644
index 0000000..f17130d
--- /dev/null
+++ b/plugins/verifier/licensechecker/utils/spdxutils_test.go
@@ -0,0 +1,95 @@
+/*
+Copyright The Ratify Authors.
+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 utils
+
+import "testing"
+
+var spdxTestBytes = []byte("" +
+	"SPDXVersion: SPDX-2.2\n" +
+	"DataLicense: CC0-1.0\n" +
+	"SPDXID: SPDXRef-DOCUMENT\n" +
+	"DocumentName: localhost-5000/test-v1\n" +
+	"DocumentNamespace: localhost-5000/test-v1-c0e2605b-0d32-45e2-9ed3-530611f8798e" +
+	"\n" +
+	"LicenseListVersion: 3.15\n" +
+	"Creator: Organization: Test\n" +
+	"Creator: Tool: test-0.0.0\n" +
+	"Created: 2021-12-17T20:24:36Z\n" +
+	"\n" +
+	"##### Package: test-baselayout\n" +
+	"\n" +
+	"PackageName: test-baselayout\n" +
+	"SPDXID: SPDXRef-Package-apk-test-baselayout\n" +
+	"PackageVersion: 1.1.1-r1\n" +
+	"PackageDownloadLocation: NOASSERTION\n" +
+	"FilesAnalyzed: false\n" +
+	"PackageLicenseConcluded: GPL-2.0-only\n" +
+	"PackageLicenseDeclared: GPL-2.0-only\n" +
+	"PackageCopyrightText: NOASSERTION\n")
+
+func TestBlobToSPDX(t *testing.T) {
+	spdxDoc, err := BlobToSPDX(spdxTestBytes)
+	if err != nil {
+		t.Fatalf("could not parse SPDX doc from bytes")
+	}
+	expected := "localhost-5000/test-v1"
+	result := spdxDoc.CreationInfo.DocumentName
+	if expected != result {
+		t.Fatalf("expected: %s, got: %s", expected, result)
+	}
+	expectedLen := 1
+	resultLen := len(spdxDoc.Packages)
+	if expectedLen != resultLen {
+		t.Fatalf("expected: %d, got: %d", expectedLen, resultLen)
+	}
+}
+
+func TestGetPackageLicenses(t *testing.T) {
+	spdxDoc, err := BlobToSPDX(spdxTestBytes)
+	if err != nil {
+		t.Fatalf("could not parse SPDX doc from bytes")
+	}
+	expected := "GPL-2.0-only"
+	result := GetPackageLicenses(*spdxDoc)
+	if len(result) != 1 {
+		t.Fatalf("no packages parsed, expected 1")
+	}
+	if result[0].PackageLicense != expected {
+		t.Fatalf("expected: %s, got: %s", expected, result[0].PackageLicense)
+	}
+}
+
+func TestLoadAllowedLicenses(t *testing.T) {
+	license := "GPL-2.0-only"
+	licenses := LoadAllowedLicenses([]string{license})
+	_, ok := licenses[license]
+	if !ok {
+		t.Fatalf("expected license but not present")
+	}
+}
+
+func TestFilterPackageLicenses(t *testing.T) {
+	spdxDoc, err := BlobToSPDX(spdxTestBytes)
+	if err != nil {
+		t.Fatalf("could not parse SPDX doc from bytes")
+	}
+	PackageLicenses := GetPackageLicenses(*spdxDoc)
+	filterLicenses := LoadAllowedLicenses([]string{"GPL-2.0-only"})
+	result := FilterPackageLicenses(PackageLicenses, filterLicenses)
+	if len(result) != 0 {
+		t.Fatalf("License not filtered correctly")
+	}
+}