Skip to content

Commit

Permalink
feat(go-witness): add vex attestor
Browse files Browse the repository at this point in the history
Signed-off-by: Kris Coleman <kriscodeman@gmail.com>
Co-authored-by: Nick Kane <nkane@testifysec.com>
Signed-off-by: Kris Coleman <kriscodeman@gmail.com>
  • Loading branch information
kriscoleman and Nick Kane committed Jun 15, 2024
1 parent 38dc13c commit 96cb04d
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 9 deletions.
33 changes: 31 additions & 2 deletions attestation/link/link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ func TestAttest(t *testing.T) {

testJson := []byte(testLinkJSON)
if !bytes.Equal(linkJson, testJson) {
t.Errorf("expected \n%s\n, got \n%s\n", testJson, linkJson)
testJson := []byte(testLinkJSONAlternative)
if !bytes.Equal(linkJson, testJson) {
t.Errorf("expected \n%s\n, got \n%s\n", testJson, linkJson)
}
}
}

Expand Down Expand Up @@ -178,6 +181,7 @@ func setupLink(t *testing.T) *Link {

return link
}

func TestRegistration(t *testing.T) {
registrations := attestation.RegistrationEntries()

Expand All @@ -191,7 +195,6 @@ func TestRegistration(t *testing.T) {
if !found {
t.Errorf("expected %s to be registered", Name)
}

}

const testLinkJSON = `{
Expand Down Expand Up @@ -219,3 +222,29 @@ const testLinkJSON = `{
"COLORTERM": "truecolor"
}
}`

const testLinkJSONAlternative = `{
"name": "test",
"command": [
"touch",
"test.txt"
],
"materials": [
{
"name": "test1",
"digest": {
"sha256": "a53d0741798b287c6dd7afa64aee473f305e65d3f49463bb9d7408ec3b12bf5f"
}
},
{
"name": "test2",
"digest": {
"sha256": "a53d0741798b287c6dd7afa64aee473f305e65d3f49463bb9d7408ec3b12bf5f"
}
}
],
"environment": {
"COLORFGBG": "7;0",
"COLORTERM": "truecolor"
}
}`
5 changes: 5 additions & 0 deletions attestation/product/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ func getFileContentType(fileName string) (string, error) {
return bytes.HasPrefix(buf, []byte(`<?xml version="1.0" encoding="UTF-8"?><bom xmlns="http://cyclonedx.org/schema/bom/`))
}, "application/vnd.cyclonedx+xml", ".cdx.xml")

// Add Vex JSON detector
mimetype.Lookup("application/json").Extend(func(buf []byte, limit uint32) bool {
return bytes.HasPrefix(buf, []byte(`{"@context":"https://openvex.dev/ns`))
}, "application/vex+json", ".vex.json")

contentType, err := mimetype.DetectFile(fileName)
if err != nil {
return "", err
Expand Down
14 changes: 7 additions & 7 deletions attestation/slsa/slsa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func TestAttest(t *testing.T) {
// Setup OCI
o := attestors.NewTestOCIAttestor()

var tests = []struct {
tests := []struct {
name string
attestors []attestation.Attestor
expectedJson string
Expand All @@ -140,9 +140,9 @@ func TestAttest(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Logf("Running test %s", test.name)
s := New()
slsaAttestor := New()

ctx, err := attestation.NewContext("test", append(test.attestors, s))
ctx, err := attestation.NewContext("test", append(test.attestors, slsaAttestor))
if err != nil {
t.Errorf("error creating attestation context: %s", err)
}
Expand All @@ -154,17 +154,17 @@ func TestAttest(t *testing.T) {

// TODO: We don't have a way to mock out times on attestor runs
// Set attestor times manually to match testProvenanceJSON
s.PbProvenance.RunDetails.Metadata.StartedOn = &timestamppb.Timestamp{
slsaAttestor.PbProvenance.RunDetails.Metadata.StartedOn = &timestamppb.Timestamp{
Seconds: 1711199861,
Nanos: 560152000,
}
s.PbProvenance.RunDetails.Metadata.FinishedOn = &timestamppb.Timestamp{
slsaAttestor.PbProvenance.RunDetails.Metadata.FinishedOn = &timestamppb.Timestamp{
Seconds: 1711199861,
Nanos: 560152000,
}

var prov []byte
if prov, err = json.MarshalIndent(s, "", " "); err != nil {
if prov, err = json.MarshalIndent(slsaAttestor, "", " "); err != nil {
t.Errorf("unexpected error: %s", err)
}

Expand Down Expand Up @@ -221,6 +221,7 @@ func setupProvenance(t *testing.T) *Provenance {

return provenance
}

func TestRegistration(t *testing.T) {
registrations := attestation.RegistrationEntries()

Expand All @@ -234,7 +235,6 @@ func TestRegistration(t *testing.T) {
if !found {
t.Errorf("expected %s to be registered", Name)
}

}

const testGHProvJSON = `{
Expand Down
122 changes: 122 additions & 0 deletions attestation/vex/vex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2024 The Witness Contributors
//
// 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 vex

import (
"encoding/json"
"fmt"
"io"
"os"

"github.com/in-toto/go-witness/attestation"
"github.com/in-toto/go-witness/cryptoutil"
"github.com/in-toto/go-witness/log"
"github.com/invopop/jsonschema"
vex "github.com/openvex/go-vex/pkg/vex"
)

const (
Name = "vex"
Type = "https://openvex.dev/ns"
RunType = attestation.PostProductRunType
)

// This is a hacky way to create a compile time error in case the attestor
// doesn't implement the expected interfaces.
var (
_ attestation.Attestor = &Attestor{}
)

func init() {
attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor {
return New()
})
}

type Attestor struct {
VEXDocument vex.VEX `json:"vexDocument"`
ReportFile string `json:"reportFileName,omitempty"`
ReportDigestSet cryptoutil.DigestSet `json:"reportDigestSet,omitempty"`
}

func New() *Attestor {
return &Attestor{}
}

func (a *Attestor) Name() string {
return Name
}

func (a *Attestor) Type() string {
return Type
}

func (a *Attestor) RunType() attestation.RunType {
return RunType
}

func (a *Attestor) Schema() *jsonschema.Schema {
return jsonschema.Reflect(&a)
}

func (a *Attestor) Attest(ctx *attestation.AttestationContext) error {
if err := a.getCandidate(ctx); err != nil {
log.Debugf("(attestation/vex) error getting candidate: %w", err)
return err
}

return nil
}

func (a *Attestor) getCandidate(ctx *attestation.AttestationContext) error {
products := ctx.Products()

if len(products) == 0 {
return fmt.Errorf("no products to attest")
}

for path, product := range products {
newDigestSet, err := cryptoutil.CalculateDigestSetFromFile(path, ctx.Hashes())
if newDigestSet == nil || err != nil {
return fmt.Errorf("error calculating digest set from file: %s", path)
}

if !newDigestSet.Equal(product.Digest) {
return fmt.Errorf("integrity error: product digest set does not match candidate digest set")
}

f, err := os.Open(path)
if err != nil {
return fmt.Errorf("error opening file: %s", path)
}

reportBytes, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("error reading file: %s", path)
}

// Check to see if we can unmarshal into VEX type
if err := json.Unmarshal(reportBytes, &a.VEXDocument); err != nil {
log.Debugf("(attestation/vex) error unmarshaling VEX document: %w", err)
continue
}

a.ReportFile = path
a.ReportDigestSet = product.Digest

return nil
}
return fmt.Errorf("no VEX file found")
}
98 changes: 98 additions & 0 deletions attestation/vex/vex_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2024 The Witness Contributors
//
// 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 vex

import (
"bytes"
"encoding/json"
"testing"
"time"

"github.com/in-toto/go-witness/attestation"
vex "github.com/openvex/go-vex/pkg/vex"
)

// NOTE(nick): examples https://github.com/openvex/vexctl/tree/main/examples/openvex

const vexDocumentExpected = `{
"vexDocument": {
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://openvex.dev/docs/public/vex-0f3be8817faafa24e4bfb3d17eaf619efb1fe54923b9c42c57b156a936b91431",
"author": "John Doe",
"role": "Senior Trusted Vex Issuer",
"timestamp": "1970-01-01T00:00:00Z",
"version": 1,
"statements": [
{
"vulnerability": {
"name": "CVE-1234-5678"
},
"products": [
{
"@id": "pkg:apk/wolfi/bash@1.0.0"
}
],
"status": "fixed"
}
]
}
}`

func TestAttest(t *testing.T) {
vexAttestor := New()
vexAttestor.VEXDocument.Context = "https://openvex.dev/ns/v0.2.0"
vexAttestor.VEXDocument.ID = "https://openvex.dev/docs/public/vex-0f3be8817faafa24e4bfb3d17eaf619efb1fe54923b9c42c57b156a936b91431"
vexAttestor.VEXDocument.Author = "John Doe"
vexAttestor.VEXDocument.AuthorRole = "Senior Trusted Vex Issuer"
vexAttestor.VEXDocument.Version = 1
time := time.Date(1970, 1, 1, 0, 0, 0, 0, time.Now().UTC().Location())
vexAttestor.VEXDocument.Timestamp = &time
vexAttestor.VEXDocument.Statements = []vex.Statement{
{
Vulnerability: vex.Vulnerability{
Name: "CVE-1234-5678",
},
Products: []vex.Product{
{
Component: vex.Component{
ID: "pkg:apk/wolfi/bash@1.0.0",
},
},
},
Status: vex.StatusFixed,
},
}

attestorCollection := []attestation.Attestor{vexAttestor}
ctx, err := attestation.NewContext("test", append(attestorCollection, vexAttestor))
if err != nil {
t.Errorf("error creating attestation context: %s", err)
}
err = ctx.RunAttestors()
if err != nil {
t.Errorf("error attesting: %s", err.Error())
}

vexDocJSON, err := json.MarshalIndent(vexAttestor, "", " ")
if err != nil {
t.Errorf("unexpected error: %s", err)
}

expectedJSON := []byte(vexDocumentExpected)

if !bytes.Equal(vexDocJSON, expectedJSON) {
t.Errorf("expected \n%s\n, got \n%s\n", expectedJSON, vexDocJSON)
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/omnibor/omnibor-go v0.0.0-20230521145532-a77de61a16cd // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/package-url/packageurl-go v0.1.1 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
Expand Down Expand Up @@ -137,6 +138,7 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/openvex/go-vex v0.2.5
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,12 @@ github.com/open-policy-agent/opa v0.64.0 h1:2g0JTt78zxhFaoBmZViY4UXvtOlzBjhhrnyr
github.com/open-policy-agent/opa v0.64.0/go.mod h1:j4VeLorVpKipnkQ2TDjWshEuV3cvP/rHzQhYaraUXZY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ=
github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo=
github.com/owenrumney/go-sarif v1.1.1 h1:QNObu6YX1igyFKhdzd7vgzmw7XsWN3/6NMGuDzBgXmE=
github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U=
github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU=
github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
Expand Down

0 comments on commit 96cb04d

Please sign in to comment.