diff --git a/Dockerfile b/Dockerfile index 2251b69..1980375 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,3 @@ # This is used to we scrap the go version and use in CI to get the latest go version # and we use dependabot to keep the go version up to date -FROM golang:1.25.7 +FROM golang:1.25.8 diff --git a/go.mod b/go.mod index 6c66a00..9546bda 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module github.com/slsa-framework/source-tool -go 1.25.7 +go 1.25.8 require ( - github.com/carabiner-dev/attestation v0.2.0 - github.com/carabiner-dev/collector v0.2.8 - github.com/carabiner-dev/signer v0.3.7 + github.com/carabiner-dev/attestation v0.2.1 + github.com/carabiner-dev/collector v0.2.10-0.20260310234513-8d637f10649f + github.com/carabiner-dev/signer v0.3.8-0.20260310160610-a37998585604 github.com/carabiner-dev/vcslocator v0.4.0 github.com/fatih/color v1.18.0 - github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 - github.com/go-git/go-git/v6 v6.0.0-20250711134917-1f24ae85fe16 + github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f + github.com/go-git/go-git/v6 v6.0.0-20260305211659-2083cf940afa github.com/google/go-github/v69 v69.2.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-retryablehttp v0.7.8 @@ -25,9 +25,9 @@ require ( require ( dario.cat/mergo v1.0.2 // indirect - github.com/CycloneDX/cyclonedx-go v0.9.2 // indirect + github.com/CycloneDX/cyclonedx-go v0.10.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/ProtonMail/go-crypto v1.4.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/avast/retry-go/v4 v4.7.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect @@ -36,87 +36,88 @@ require ( github.com/carabiner-dev/github v0.2.2 // indirect github.com/carabiner-dev/hasher v0.2.3 // indirect github.com/carabiner-dev/jsonl v0.2.1 // indirect - github.com/carabiner-dev/openeox v0.0.0-20251126193927-142e907140f5 // indirect + github.com/carabiner-dev/openeox v0.0.0-20260302211234-88fe8a305401 // indirect github.com/carabiner-dev/osv v0.0.0-20250124012120-b8ce4531cd92 // indirect - github.com/carabiner-dev/policy v0.4.1-0.20251211203139-302be2dfaf0d // indirect + github.com/carabiner-dev/policy v0.4.2 // indirect github.com/carabiner-dev/predicates v0.1.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect - github.com/clipperhouse/displaywidth v0.6.0 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.6.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect - github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect - github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect - github.com/docker/cli v29.0.3+incompatible // indirect + github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c // indirect + github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea // indirect + github.com/docker/cli v29.3.0+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg/v2 v2.0.2 // indirect - github.com/go-git/go-billy/v5 v5.7.0 // indirect - github.com/go-git/go-git/v5 v5.16.5 // indirect + github.com/go-git/go-billy/v5 v5.8.0 // indirect + github.com/go-git/go-git/v5 v5.17.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.24.1 // indirect - github.com/go-openapi/errors v0.22.4 // indirect - github.com/go-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.3 // indirect - github.com/go-openapi/loads v0.23.2 // indirect - github.com/go-openapi/runtime v0.29.2 // indirect - github.com/go-openapi/spec v0.22.1 // indirect - github.com/go-openapi/strfmt v0.25.0 // indirect - github.com/go-openapi/swag v0.25.4 // indirect - github.com/go-openapi/swag/cmdutils v0.25.4 // indirect - github.com/go-openapi/swag/conv v0.25.4 // indirect - github.com/go-openapi/swag/fileutils v0.25.4 // indirect - github.com/go-openapi/swag/jsonname v0.25.4 // indirect - github.com/go-openapi/swag/jsonutils v0.25.4 // indirect - github.com/go-openapi/swag/loading v0.25.4 // indirect - github.com/go-openapi/swag/mangling v0.25.4 // indirect - github.com/go-openapi/swag/netutils v0.25.4 // indirect - github.com/go-openapi/swag/stringutils v0.25.4 // indirect - github.com/go-openapi/swag/typeutils v0.25.4 // indirect - github.com/go-openapi/swag/yamlutils v0.25.4 // indirect - github.com/go-openapi/validate v0.25.1 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-openapi/analysis v0.24.3 // indirect + github.com/go-openapi/errors v0.22.7 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/loads v0.23.3 // indirect + github.com/go-openapi/runtime v0.29.3 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/strfmt v0.26.0 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/swag/cmdutils v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/fileutils v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/mangling v0.25.5 // indirect + github.com/go-openapi/swag/netutils v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + github.com/go-openapi/validate v0.25.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/certificate-transparency-go v1.3.2 // indirect + github.com/google/certificate-transparency-go v1.3.3 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-containerregistry v0.20.7 // indirect + github.com/google/go-containerregistry v0.21.2 // indirect github.com/google/go-github/v73 v73.0.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect + github.com/google/go-querystring v1.2.0 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/in-toto/in-toto-golang v0.9.0 // indirect + github.com/in-toto/in-toto-golang v0.10.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 // indirect - github.com/kevinburke/ssh_config v1.4.0 // indirect - github.com/klauspost/compress v1.18.1 // indirect + github.com/kevinburke/ssh_config v1.6.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect - github.com/oklog/ulid v1.3.1 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect - github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.1.3 // indirect - github.com/olekukonko/tablewriter v1.1.2 // indirect + github.com/olekukonko/errors v1.2.0 // indirect + github.com/olekukonko/ll v0.1.7 // indirect + github.com/olekukonko/tablewriter v1.1.3 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/openvex/go-vex v0.2.7 // indirect - github.com/package-url/packageurl-go v0.1.4 // indirect + github.com/package-url/packageurl-go v0.1.5 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect @@ -127,40 +128,39 @@ require ( github.com/sergi/go-diff v1.4.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/protobuf-specs v0.5.0 // indirect - github.com/sigstore/rekor v1.4.3 // indirect - github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect + github.com/sigstore/rekor v1.5.0 // indirect + github.com/sigstore/rekor-tiles/v2 v2.2.1 // indirect github.com/sigstore/sigstore v1.10.4 // indirect - github.com/sigstore/timestamp-authority/v2 v2.0.3 // indirect + github.com/sigstore/timestamp-authority/v2 v2.0.5 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/skeema/knownhosts v1.3.2 // indirect - github.com/spdx/tools-golang v0.5.5 // indirect + github.com/spdx/tools-golang v0.5.7 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/theupdateframework/go-tuf v0.7.0 // indirect github.com/theupdateframework/go-tuf/v2 v2.4.1 // indirect - github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect + github.com/transparency-dev/formats v0.1.0 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - go.mongodb.org/mongo-driver v1.17.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.32.0 // indirect - golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect - google.golang.org/grpc v1.76.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.79.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.140.0 // indirect ) diff --git a/go.sum b/go.sum index fcf4ce7..692fd88 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,25 @@ -cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= -cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= -cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= -cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= +cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k= -cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= -cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= -cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ= +cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= @@ -30,16 +30,15 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfg github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo= -github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= +github.com/CycloneDX/cyclonedx-go v0.10.0 h1:7xyklU7YD+CUyGzSFIARG18NYLsKVn4QFg04qSsu+7Y= +github.com/CycloneDX/cyclonedx-go v0.10.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ= +github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -50,44 +49,48 @@ github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Z github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= -github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= -github.com/aws/aws-sdk-go-v2/config v1.31.20 h1:/jWF4Wu90EhKCgjTdy1DGxcbcbNrjfBHvksEL79tfQc= -github.com/aws/aws-sdk-go-v2/config v1.31.20/go.mod h1:95Hh1Tc5VYKL9NJ7tAkDcqeKt+MCXQB1hQZaRdJIZE0= -github.com/aws/aws-sdk-go-v2/credentials v1.18.24 h1:iJ2FmPT35EaIB0+kMa6TnQ+PwG5A1prEdAw+PsMzfHg= -github.com/aws/aws-sdk-go-v2/credentials v1.18.24/go.mod h1:U91+DrfjAiXPDEGYhh/x29o4p0qHX5HDqG7y5VViv64= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= +github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= -github.com/aws/aws-sdk-go-v2/service/kms v1.48.2 h1:aL8Y/AbB6I+uw0MjLbdo68NQ8t5lNs3CY3S848HpETk= -github.com/aws/aws-sdk-go-v2/service/kms v1.48.2/go.mod h1:VJcNH6BLr+3VJwinRKdotLOMglHO8mIKlD3ea5c7hbw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 h1:NjShtS1t8r5LUfFVtFeI8xLAHQNTa7UI0VawXlrBMFQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.3/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 h1:gTsnx0xXNQ6SBbymoDvcoRHL+q4l/dAFsQuKfDWSaGc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 h1:HK5ON3KmQV2HcAunnx4sKLB9aPf3gKGwVAf7xnx0QT0= -github.com/aws/aws-sdk-go-v2/service/sts v1.40.2/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= -github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= -github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= -github.com/carabiner-dev/attestation v0.2.0 h1:vEqAIapcHjIoEQad9GrKtEx2czeu7t4cun+1bCEtN1o= -github.com/carabiner-dev/attestation v0.2.0/go.mod h1:dLPe3DPL/0YpKJpNDCQJZdtkZIuWTAH1G0N8V5LJ41c= -github.com/carabiner-dev/collector v0.2.8 h1:TQCWbpdM8xIuJuj0seu6z0zNZVNzczqvCKuaist+lbk= -github.com/carabiner-dev/collector v0.2.8/go.mod h1:TDQ75MeBpoIOSGCtTWQrTxwtYcj/9rIJjonCix4F4HQ= +github.com/carabiner-dev/attestation v0.2.1 h1:VhjV5YlO9TsW50Sr/Zd54bdbZhhDAqgxC3kB9z1I+3Q= +github.com/carabiner-dev/attestation v0.2.1/go.mod h1:O84vF84RZG3pJO/6BYrPs718bZviHF5DKajP1HsrDpw= +github.com/carabiner-dev/collector v0.2.10-0.20260309053530-b55bbe428700 h1:S4NFLl3/UJvA4aUuPVY3MECUQS0jhHdT//+excPvWUc= +github.com/carabiner-dev/collector v0.2.10-0.20260309053530-b55bbe428700/go.mod h1:ltqIryrtd+Ev+zAW9Q2h/UHNVMhT2nR1Fmchk+8lbs8= +github.com/carabiner-dev/collector v0.2.10-0.20260310234513-8d637f10649f h1:2Q5VzbSVNKPwhP9rUjwH3ZWajDYwxB+3ZT0MTU/80ho= +github.com/carabiner-dev/collector v0.2.10-0.20260310234513-8d637f10649f/go.mod h1:nqe5n0soDuenKtOt9i4G8D/anZiRTiZywzo3pNqf5As= github.com/carabiner-dev/ghrfs v0.3.4 h1:XJoDXkuw+8KQPTC4oI0da8vLpnx7cfQBGgyjzo+Eqrc= github.com/carabiner-dev/ghrfs v0.3.4/go.mod h1:u9We7molIUX6sCe4ox70juKOnbNAUpDv+B5Cerbqhio= github.com/carabiner-dev/github v0.2.2 h1:Ykrlcct71fRQm4j37LhAz9FyzG4n1nlm2e+V62MIoJM= @@ -96,36 +99,38 @@ github.com/carabiner-dev/hasher v0.2.3 h1:678/FKyUJexzDznoIomnW/Z71hoL6Ehz99W2ZP github.com/carabiner-dev/hasher v0.2.3/go.mod h1:TRannDvJs5eL61xA/K/whtYbZLQPb/CGFXtDnx4h37M= github.com/carabiner-dev/jsonl v0.2.1 h1:2cBv4mGoqWfVkeQO1krIGds9WUm9Jet89QdqWLodlfo= github.com/carabiner-dev/jsonl v0.2.1/go.mod h1:fTypVHMYsAnn6LlW0SqER6XUaI9Wlc/Kv6uyMF+NmKo= -github.com/carabiner-dev/openeox v0.0.0-20251126193927-142e907140f5 h1:yBdk08BZwlHbSZVKHkvWJbu+ugNruYnCWGzCQ/GNQzg= -github.com/carabiner-dev/openeox v0.0.0-20251126193927-142e907140f5/go.mod h1:grnE4r9+/9Cr+Wvh2Lx1Lj7gQ+Mdl2qf0LZA85m8PLM= +github.com/carabiner-dev/openeox v0.0.0-20260302211234-88fe8a305401 h1:fexoMxtBCeYSxNJcd0wTAO3rYyq77wv7AqnNrEyo0JI= +github.com/carabiner-dev/openeox v0.0.0-20260302211234-88fe8a305401/go.mod h1:YDuJw950hcWHl/WWJTpmyAMfRRY3rNXgy4NxnTtRTIY= github.com/carabiner-dev/osv v0.0.0-20250124012120-b8ce4531cd92 h1:BJ9+OCNezZGkU8SrGC3oB7Tj+J0JsonwfZztcgUav6c= github.com/carabiner-dev/osv v0.0.0-20250124012120-b8ce4531cd92/go.mod h1:o7jXwi/fFZ9mQlvVlog0kcvyEkwQT3eWmVQmrorBGpE= -github.com/carabiner-dev/policy v0.4.1-0.20251211203139-302be2dfaf0d h1:nfzLqV09Ray2NvjUxH//o5NE/jP/AnCKOmj/xEixvZk= -github.com/carabiner-dev/policy v0.4.1-0.20251211203139-302be2dfaf0d/go.mod h1:ufVsPpVkUZPxsdI8yxokSSd5dqqDhp1nebbVpjLvvqg= +github.com/carabiner-dev/policy v0.4.2 h1:5yMIqui603UgpUft6wwub7+sErgsCqUJuElZF/McA3U= +github.com/carabiner-dev/policy v0.4.2/go.mod h1:T7ruDEM6F7xWm6OzYRY9s+QiSMsh/VPDufZakCrlYOg= github.com/carabiner-dev/predicates v0.1.0 h1:t6tQF9gFdr6TIccWtuNk3kFasx8eu88INFVGkCUnjL4= github.com/carabiner-dev/predicates v0.1.0/go.mod h1:jL6EAD+LiI6GW/rOdRYAJF4HaA88/V2Q4n7yUGNQ7XM= github.com/carabiner-dev/signer v0.3.7 h1:oEmOg17Szs5+x0oVVGXluPNsVaQyROlXrBPxgwxfOHg= github.com/carabiner-dev/signer v0.3.7/go.mod h1:gReZbCZlINz8Pm/hrD1HtRcGFaO9MGUAu/v+iZp7R6Y= +github.com/carabiner-dev/signer v0.3.8-0.20260310160610-a37998585604 h1:2PeAzIFCqbCF/upSb4Hqj5HwTvuJ6D6KjP2F5yH7WrA= +github.com/carabiner-dev/signer v0.3.8-0.20260310160610-a37998585604/go.mod h1:kqmUAFHKgQXFsIsIFjbk4UlOMXNte7J1IVxIgKAnY/M= github.com/carabiner-dev/vcslocator v0.4.0 h1:HxU8F7FWJatnIhR6NowsPpfki8xw2uNDRLuikcBHFao= github.com/carabiner-dev/vcslocator v0.4.0/go.mod h1:B3JFnwypdrRVQPJKNK3BOlRw6DbgmsiHGmoT3ZELpp8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s= -github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= -github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= -github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= -github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -133,23 +138,23 @@ github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= -github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= -github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= -github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= -github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= -github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= -github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= -github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= -github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c h1:g349iS+CtAvba7i0Ee9EP1TlTZ9w+UncBY6HSmsFZa0= +github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c/go.mod h1:mCGGmWkOQvEuLdIRfPIpXViBfpWto4AhwtJlAvo62SQ= +github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea h1:ALRwvjsSP53QmnN3Bcj0NpR8SsFLnskny/EIMebAk1c= +github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= +github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk= +github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= -github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= +github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= @@ -160,25 +165,24 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= -github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= -github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= -github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= -github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM= -github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= -github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30 h1:4KqVJTL5eanN8Sgg3BV6f2/QzfZEFbCd+rTak1fGRRA= -github.com/go-git/go-billy/v6 v6.0.0-20250627091229-31e2a16eef30/go.mod h1:snwvGrbywVFy2d6KJdQ132zapq4aLyzLMgpo79XdEfM= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= +github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f h1:Uvbx7nITO3Sd1GdXarX0TbyYmOaSNIJP0mm4LocEyyA= +github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git-fixtures/v5 v5.1.0 h1:b8cWxDLTk0s09Ihm9x1HvNGUzxUVlRwIH7EAM0gGDKg= -github.com/go-git/go-git-fixtures/v5 v5.1.0/go.mod h1:CdmU0oQeDuy4Xh8V0i9Ym+vsTkgDDPKEiofBFEVT+aE= -github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= -github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= -github.com/go-git/go-git/v6 v6.0.0-20250711134917-1f24ae85fe16 h1:LGHFWd3pmIuMugWNIQfBd1CY6k6Gy+XuJ/VnNQZCXWg= -github.com/go-git/go-git/v6 v6.0.0-20250711134917-1f24ae85fe16/go.mod h1:gI6xSrrkXH4EKP38iovrsY2EYf2XDU3DrIZRshlNDm0= +github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII= +github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw= +github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= +github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= +github.com/go-git/go-git/v6 v6.0.0-20260305211659-2083cf940afa h1:fIbZ264qSeJ+GRz+5nq6SFonkCanp/6CRXhYutq8GlE= +github.com/go-git/go-git/v6 v6.0.0-20260305211659-2083cf940afa/go.mod h1:V/qoTD4qCYizR+fKFA9++d2APoE8Yheci7dXALaSeuI= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -186,60 +190,62 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM= -github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84= -github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM= -github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= -github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= -github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= -github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= -github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= -github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= -github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0= -github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0= -github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= -github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= -github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= -github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= -github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= -github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= -github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= -github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= -github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= -github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= -github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= -github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= -github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= -github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= -github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= -github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= -github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= -github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= -github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= -github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= -github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= -github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= -github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= -github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= -github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= -github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= -github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= +github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= +github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= +github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU= +github.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= +github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= +github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= +github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= +github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= +github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= +github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -250,21 +256,19 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= -github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/certificate-transparency-go v1.3.3 h1:hq/rSxztSkXN2tx/3jQqF6Xc0O565UQPdHrOWvZwybo= +github.com/google/certificate-transparency-go v1.3.3/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= -github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= +github.com/google/go-containerregistry v0.21.2 h1:vYaMU4nU55JJGFC9JR/s8NZcTjbE9DBBbvusTW9NeS0= +github.com/google/go-containerregistry v0.21.2/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= github.com/google/go-github/v73 v73.0.0 h1:aR+Utnh+Y4mMkS+2qLQwcQ/cF9mOTpdwnzlaw//rG24= github.com/google/go-github/v73 v73.0.0/go.mod h1:fa6w8+/V+edSU0muqdhCVY7Beh1M8F1IlQPZIANKIYw= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -273,16 +277,16 @@ github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81zxNQw05s= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= +github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -311,16 +315,16 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= -github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= -github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= +github.com/in-toto/in-toto-golang v0.10.0 h1:+s2eZQSK3WmWfYV85qXVSBfqgawi/5L02MaqA4o/tpM= +github.com/in-toto/in-toto-golang v0.10.0/go.mod h1:wjT4RiyFlLWCmLUJjwB8oZcjaq7HA390aMJcD3xXgmg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= -github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -331,12 +335,12 @@ github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= -github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= +github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -354,8 +358,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/maxbrunsfeld/counterfeiter/v6 v6.12.1 h1:D4O2wLxB384TS3ohBJMfolnxb4qGmoZ1PnWNtit8LYo= github.com/maxbrunsfeld/counterfeiter/v6 v6.12.1/go.mod h1:RuJdxo0oI6dClIaMzdl3hewq3a065RH65dofJP03h8I= github.com/migueleliasweb/go-github-mock v1.5.0 h1:dIr6vgVz8QY9sDiDopWxk6pDw4d7K/xIcCk/NQe4ajM= @@ -368,16 +372,16 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0 github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= -github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg= -github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= -github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc= -github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg= +github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= +github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.7 h1:WyK1YZwOTUKHEXZz3VydBDT5t3zDqa9yI8iJg5PHon4= +github.com/olekukonko/ll v0.1.7/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= +github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= +github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -386,8 +390,9 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/openvex/go-vex v0.2.7 h1:/pN3bqvS4QOc6WkkL0hbKzJuAtsUD9vmvk9IZkzD3Zc= github.com/openvex/go-vex v0.2.7/go.mod h1:ZyQC3NXl9jjS53JOpBG3LAUXySkW8IlJ/GIhsnf5D54= -github.com/package-url/packageurl-go v0.1.4 h1:RHfiiN1SSY+Kic537DXch6fy593rxGJW6WDzAiOwNdk= -github.com/package-url/packageurl-go v0.1.4/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= +github.com/package-url/packageurl-go v0.1.5 h1:O4efRXja2XQ5CtiiYiCZ22k/m7i5ugLiAghgcC+eDgk= +github.com/package-url/packageurl-go v0.1.5/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -418,47 +423,39 @@ github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= -github.com/sigstore/rekor v1.4.3 h1:2+aw4Gbgumv8vYM/QVg6b+hvr4x4Cukur8stJrVPKU0= -github.com/sigstore/rekor v1.4.3/go.mod h1:o0zgY087Q21YwohVvGwV9vK1/tliat5mfnPiVI3i75o= -github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo= -github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU= +github.com/sigstore/rekor v1.5.0 h1:rL7SghHd5HLCtsCrxw0yQg+NczGvM75EjSPPWuGjaiQ= +github.com/sigstore/rekor v1.5.0/go.mod h1:D7JoVCUkxwQOpPDNYeu+CE8zeBC18Y5uDo6tF8s2rcQ= +github.com/sigstore/rekor-tiles/v2 v2.2.1 h1:UmV1CBQ3SjxxPGpFmwDoOhoIwiKpM2Qm1pU5tPGmvNk= +github.com/sigstore/rekor-tiles/v2 v2.2.1/go.mod h1:z8n6l6oidpaLjjE6rJERuQqY9X38ulnHZCXyL+DEL7U= github.com/sigstore/sigstore v1.10.4 h1:ytOmxMgLdcUed3w1SbbZOgcxqwMG61lh1TmZLN+WeZE= github.com/sigstore/sigstore v1.10.4/go.mod h1:tDiyrdOref3q6qJxm2G+JHghqfmvifB7hw+EReAfnbI= github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.0 h1:UOHpiyezCj5RuixgIvCV3QyuxIGQT+N6nGZEXA7OTTY= -github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.0/go.mod h1:U0CZmA2psabDa8DdiV7yXab0AHODzfKqvD2isH7Hrvw= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.0 h1:fq4+8Y4YadxeF8mzhoMRPZ1mVvDYXmI3BfS0vlkPT7M= -github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.0/go.mod h1:u05nqPWY05lmcdHhv2lPaWTH3FGUhJzO7iW2hbboK3Q= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.0 h1:iUEf5MZYOuXGnXxdF/WrarJrk0DTVHqeIOjYdtpVXtc= -github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.0/go.mod h1:i6vg5JfEQix46R1rhQlrKmUtJoeH91drltyYOJEk1T4= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.0 h1:dUvPv/MP23ZPIXZUW45kvCIgC0ZRfYxEof57AB6bAtU= -github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.0/go.mod h1:fR/gDdPvJWGWL70/NgBBIL1O0/3Wma6JHs3tSSYg3s4= -github.com/sigstore/timestamp-authority/v2 v2.0.3 h1:sRyYNtdED/ttLCMdaYnwpf0zre1A9chvjTnCmWWxN8Y= -github.com/sigstore/timestamp-authority/v2 v2.0.3/go.mod h1:mDaHxkt3HmZYoIlwYj4QWo0RUr7VjYU52aVO5f5Qb3I= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4 h1:VZ+L6SKVWbLPHznIF0tBuO7qKMFdJiJMVwFKu9DlY5o= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4/go.mod h1:Rstj47WpJym25il8j4jTL0BfikzP/9AhVD+DsBcYzZc= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.4 h1:G7yOv8bxk3zIEEZyVCixPxtePIAm+t3ZWSaKRPzVw+o= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.4/go.mod h1:hxJelB/bRItMYOzi6qD9xEKjse2QZcikh4TbysfdDHc= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.4 h1:Qxt6dE4IwhJ6gIXmg2q4S/SeqEDSZ29nmfsv7Zb6LL4= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.4/go.mod h1:hJVeNOwarqfyALjOwsf0OR8YA/A96NABucEaQumPr30= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.4 h1:KVavYMPfSf5NryOl6VrZ9nRG3fXOOJOPp7Czk/YCPkM= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.4/go.mod h1:J7CA1AaBkyK8dYq6EdQANhj+8oEcsA7PrIp088qgPiY= +github.com/sigstore/timestamp-authority/v2 v2.0.5 h1:WT17MU4bNRvjRLlTvTO5gmrSIWJVbzwrNXgwsjB+53U= +github.com/sigstore/timestamp-authority/v2 v2.0.5/go.mod h1:oV+Yy0GsfgNAeDZcv/WJjQE42wFtMTtuD85bPLAQk5M= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= -github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= -github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= -github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= +github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg= +github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= @@ -471,14 +468,14 @@ github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuX github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= -github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0 h1:6nAX1aRGnkg2SEUMwO5toB2tQkP0Jd6cbmZ/K5Le1V0= -github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0/go.mod h1:HOC5NWW1wBI2Vke1FGcRBvDATkEYE7AUDiYbXqi2sBw= -github.com/tink-crypto/tink-go/v2 v2.5.0 h1:B8KLF6AofxdBIE4UJIaFbmoj5/1ehEtt7/MmzfI4Zpw= -github.com/tink-crypto/tink-go/v2 v2.5.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= +github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0 h1:j+S+WKBQ5ya26A5EM/uXoVe+a2IaPQN8KgBJZ22cJ+4= +github.com/tink-crypto/tink-go-hcvault/v2 v2.4.0/go.mod h1:OCKJIujnTzDq7f+73NhVs99oA2c1TR6nsOpuasYM6Yo= +github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4= +github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= -github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= -github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= +github.com/transparency-dev/formats v0.1.0 h1:oL0zUFuYUjg8AbtjPMnIRDmjbaHo5jCjEWU5yaNuz0g= +github.com/transparency-dev/formats v0.1.0/go.mod h1:d2FibUOHfCMdCe/+/rbKt1IPLBbPTDfwj46kt541/mU= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= @@ -505,62 +502,58 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= -go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= -go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.step.sm/crypto v0.74.0 h1:/APBEv45yYR4qQFg47HA8w1nesIGcxh44pGyQNw6JRA= -go.step.sm/crypto v0.74.0/go.mod h1:UoXqCAJjjRgzPte0Llaqen7O9P7XjPmgjgTHQGkKCDk= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4= +go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -572,40 +565,40 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= -google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= -google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= -google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= -google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= -google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU= +google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -616,16 +609,14 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= sigs.k8s.io/release-utils v0.12.3 h1:iNVJY81QfmMCmXxMg8IvvkkeQNk6ZWlLj+iPKSlKyVQ= sigs.k8s.io/release-utils v0.12.3/go.mod h1:BvbNmm1BmM3cnEpBmNHWL3wOSziOdGlsYR8vCFq/Q0o= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= diff --git a/internal/cmd/audit.go b/internal/cmd/audit.go index 6456fe1..06f092b 100644 --- a/internal/cmd/audit.go +++ b/internal/cmd/audit.go @@ -4,15 +4,13 @@ package cmd import ( - "context" "errors" "fmt" "github.com/spf13/cobra" - "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/audit" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" + "github.com/slsa-framework/source-tool/pkg/sourcetool" ) type AuditMode int @@ -74,9 +72,9 @@ type AuditCommitResultJSON struct { VerifiedLevels []string `json:"verified_levels,omitempty"` PrevCommitMatches *bool `json:"prev_commit_matches,omitempty"` ProvControls interface{} `json:"prov_controls,omitempty"` - GhControls interface{} `json:"gh_controls,omitempty"` + Controls interface{} `json:"controls,omitempty"` PrevCommit string `json:"prev_commit,omitempty"` - GhPriorCommit string `json:"gh_prior_commit,omitempty"` + PriorCommit string `json:"prior_commit,omitempty"` Link string `json:"link,omitempty"` Error string `json:"error,omitempty"` } @@ -154,14 +152,105 @@ Future: if err := opts.Validate(); err != nil { return err } - return doAudit(opts) + + authenticator, err := CheckAuth() + if err != nil { + return err + } + + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + ) + if err != nil { + return err + } + + auditor, err := audit.NewAuditor( + audit.WithAttester(srctool.Attester()), + audit.WithBackend(srctool.Backend()), + ) + if err != nil { + return err + } + + // Initialize JSON result structure if needed + var jsonResult *AuditResultJSON + if opts.outputFormatIsJSON() { + jsonResult = &AuditResultJSON{ + Owner: opts.owner, + Repository: opts.repository, + Branch: opts.branch, + CommitResults: []AuditCommitResultJSON{}, + } + } else { + opts.writeTextf("Auditing branch %s\n", opts.branch) + } + + // Single loop for both JSON and text output + count := 0 + passed := 0 + failed := 0 + + for ar, err := range auditor.AuditBranch(cmd.Context(), opts.GetBranch()) { + if ar == nil { + return err + } + + // Process result based on output format + if opts.outputFormatIsJSON() { + commitResult := convertAuditResultToJSON(opts.owner, opts.repository, ar, opts.auditMode) + if err != nil { + commitResult.Error = err.Error() + } + if commitResult.Status == statusPassed { + passed++ + } else { + failed++ + } + jsonResult.CommitResults = append(jsonResult.CommitResults, commitResult) + } else { + // Text output + if err != nil { + opts.writeTextf("\terror: %v\n", err) + } + printResult(opts.owner, opts.repository, ar, opts.auditMode) + } + + // Check for early termination conditions + if opts.endingCommit != "" && opts.endingCommit == ar.Commit { + if !opts.outputFormatIsJSON() { + opts.writeTextf("Found ending commit %s\n", opts.endingCommit) + } + break + } + if opts.auditDepth > 0 && count >= opts.auditDepth { + if !opts.outputFormatIsJSON() { + opts.writeTextf("Reached depth limit %d\n", opts.auditDepth) + } + break + } + count++ + } + + // Write JSON output if needed + if opts.outputFormatIsJSON() { + jsonResult.Summary = &AuditSummary{ + TotalCommits: len(jsonResult.CommitResults), + PassedCommits: passed, + FailedCommits: failed, + } + return opts.writeJSON(jsonResult) + } + + return nil }, } opts.AddFlags(auditCmd) parentCmd.AddCommand(auditCmd) } -func printResult(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCommitResult, mode AuditMode) { +func printResult(owner, repo string, ar *audit.AuditCommitResult, mode AuditMode) { good := ar.IsGood() status := statusPassed if !good { @@ -181,22 +270,22 @@ func printResult(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCommitResult, m if ar.ProvPred != nil { fmt.Print("\tprov:\n") fmt.Printf("\t\tcontrols: %v\n", ar.ProvPred.GetControls()) - if ar.ProvPred.GetPrevCommit() == ar.GhPriorCommit { - fmt.Printf("\t\tPrevCommit matches GH commit: true\n") + if ar.ProvPred.GetPrevCommit() == ar.PriorCommit { + fmt.Printf("\t\tPrevCommit matches prior commit: true\n") } else { - fmt.Printf("\t\tPrevCommit matches GH commit: false: %s != %s\n", ar.ProvPred.GetPrevCommit(), ar.GhPriorCommit) + fmt.Printf("\t\tPrevCommit matches prior commit: false: %s != %s\n", ar.ProvPred.GetPrevCommit(), ar.PriorCommit) } } else { fmt.Printf("\tprov: none\n") } - if ar.GhControlStatus != nil { - fmt.Printf("\tgh controls: %v\n", ar.GhControlStatus.Controls) + if ar.ControlStatus != nil { + fmt.Printf("\tcontrols: %v\n", ar.ControlStatus.Controls) } - fmt.Printf("\tlink: https://github.com/%s/%s/commit/%s\n", ghc.Owner(), ghc.Repo(), ar.GhPriorCommit) + fmt.Printf("\tlink: https://github.com/%s/%s/commit/%s\n", owner, repo, ar.PriorCommit) } -func convertAuditResultToJSON(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCommitResult, mode AuditMode) AuditCommitResultJSON { +func convertAuditResultToJSON(owner, repo string, ar *audit.AuditCommitResult, mode AuditMode) AuditCommitResultJSON { good := ar.IsGood() status := statusPassed if !good { @@ -206,7 +295,7 @@ func convertAuditResultToJSON(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCo result := AuditCommitResultJSON{ Commit: ar.Commit, Status: status, - Link: fmt.Sprintf("https://github.com/%s/%s/commit/%s", ghc.Owner(), ghc.Repo(), ar.GhPriorCommit), + Link: fmt.Sprintf("https://github.com/%s/%s/commit/%s", owner, repo, ar.PriorCommit), } // Only include details if mode is Full or status is failed @@ -218,102 +307,15 @@ func convertAuditResultToJSON(ghc *ghcontrol.GitHubConnection, ar *audit.AuditCo if ar.ProvPred != nil { result.ProvControls = ar.ProvPred.GetControls() result.PrevCommit = ar.ProvPred.GetPrevCommit() - result.GhPriorCommit = ar.GhPriorCommit - matches := ar.ProvPred.GetPrevCommit() == ar.GhPriorCommit + result.PriorCommit = ar.PriorCommit + matches := ar.ProvPred.GetPrevCommit() == ar.PriorCommit result.PrevCommitMatches = &matches } - if ar.GhControlStatus != nil { - result.GhControls = ar.GhControlStatus.Controls + if ar.ControlStatus != nil { + result.Controls = ar.ControlStatus.Controls } } return result } - -func doAudit(auditArgs *auditOpts) error { - ghc := ghcontrol.NewGhConnection(auditArgs.owner, auditArgs.repository, ghcontrol.BranchToFullRef(auditArgs.branch)).WithAuthToken(githubToken) - ctx := context.Background() - verifier := getVerifier(&auditArgs.verifierOptions) - pa := attest.NewProvenanceAttestor(ghc, verifier) - - auditor := audit.NewAuditor(ghc, pa, verifier) - - latestCommit, err := ghc.GetLatestCommit(ctx, auditArgs.branch) - if err != nil { - return fmt.Errorf("could not get latest commit for %s", auditArgs.branch) - } - - // Initialize JSON result structure if needed - var jsonResult *AuditResultJSON - if auditArgs.outputFormatIsJSON() { - jsonResult = &AuditResultJSON{ - Owner: auditArgs.owner, - Repository: auditArgs.repository, - Branch: auditArgs.branch, - LatestCommit: latestCommit, - CommitResults: []AuditCommitResultJSON{}, - } - } else { - // Print header for text output - auditArgs.writeTextf("Auditing branch %s starting from revision %s\n", auditArgs.branch, latestCommit) - } - - // Single loop for both JSON and text output - count := 0 - passed := 0 - failed := 0 - - for ar, err := range auditor.AuditBranch(ctx, auditArgs.branch) { - if ar == nil { - return err - } - - // Process result based on output format - if auditArgs.outputFormatIsJSON() { - commitResult := convertAuditResultToJSON(ghc, ar, auditArgs.auditMode) - if err != nil { - commitResult.Error = err.Error() - } - if commitResult.Status == statusPassed { - passed++ - } else { - failed++ - } - jsonResult.CommitResults = append(jsonResult.CommitResults, commitResult) - } else { - // Text output - if err != nil { - auditArgs.writeTextf("\terror: %v\n", err) - } - printResult(ghc, ar, auditArgs.auditMode) - } - - // Check for early termination conditions - if auditArgs.endingCommit != "" && auditArgs.endingCommit == ar.Commit { - if !auditArgs.outputFormatIsJSON() { - auditArgs.writeTextf("Found ending commit %s\n", auditArgs.endingCommit) - } - break - } - if auditArgs.auditDepth > 0 && count >= auditArgs.auditDepth { - if !auditArgs.outputFormatIsJSON() { - auditArgs.writeTextf("Reached depth limit %d\n", auditArgs.auditDepth) - } - break - } - count++ - } - - // Write JSON output if needed - if auditArgs.outputFormatIsJSON() { - jsonResult.Summary = &AuditSummary{ - TotalCommits: len(jsonResult.CommitResults), - PassedCommits: passed, - FailedCommits: failed, - } - return auditArgs.writeJSON(jsonResult) - } - - return nil -} diff --git a/internal/cmd/audit_test.go b/internal/cmd/audit_test.go index d0c9a81..e6c1633 100644 --- a/internal/cmd/audit_test.go +++ b/internal/cmd/audit_test.go @@ -11,7 +11,6 @@ import ( vpb "github.com/in-toto/attestation/go/predicates/vsa/v1" "github.com/slsa-framework/source-tool/pkg/audit" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" ) @@ -140,8 +139,6 @@ func TestAuditResultJSON_JSONMarshaling(t *testing.T) { } func TestConvertAuditResultToJSON(t *testing.T) { - ghc := ghcontrol.NewGhConnection("test-owner", "test-repo", "refs/heads/main") - tests := []struct { name string result *audit.AuditCommitResult @@ -158,7 +155,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { ProvPred: &provenance.SourceProvenancePred{ PrevCommit: "def456", }, - GhPriorCommit: "def456", + PriorCommit: "def456", }, mode: AuditModeBasic, want: AuditCommitResultJSON{ @@ -178,10 +175,8 @@ func TestConvertAuditResultToJSON(t *testing.T) { PrevCommit: "def456", Controls: []*provenance.Control{{Name: "test_control"}}, }, - GhPriorCommit: "def456", - GhControlStatus: &ghcontrol.GhControlStatus{ - Controls: slsa.Controls{}, - }, + PriorCommit: "def456", + ControlStatus: &slsa.ControlSet{}, }, mode: AuditModeFull, want: func() AuditCommitResultJSON { @@ -192,9 +187,9 @@ func TestConvertAuditResultToJSON(t *testing.T) { VerifiedLevels: []string{"SLSA_SOURCE_LEVEL_3"}, PrevCommitMatches: &matches, ProvControls: []*provenance.Control{{Name: "test_control"}}, - GhControls: slsa.Controls{}, + Controls: slsa.ControlSet{}, PrevCommit: "def456", - GhPriorCommit: "def456", + PriorCommit: "def456", Link: "https://github.com/test-owner/test-repo/commit/def456", } }(), @@ -202,10 +197,10 @@ func TestConvertAuditResultToJSON(t *testing.T) { { name: "failed audit shows details even in basic mode", result: &audit.AuditCommitResult{ - Commit: "abc123", - VsaPred: nil, - ProvPred: nil, - GhPriorCommit: "def456", + Commit: "abc123", + VsaPred: nil, + ProvPred: nil, + PriorCommit: "def456", }, mode: AuditModeBasic, want: AuditCommitResultJSON{ @@ -225,7 +220,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { PrevCommit: "wrong123", Controls: []*provenance.Control{{Name: "test_control"}}, }, - GhPriorCommit: "def456", + PriorCommit: "def456", }, mode: AuditModeFull, want: func() AuditCommitResultJSON { @@ -237,7 +232,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { PrevCommitMatches: &matches, ProvControls: []*provenance.Control{{Name: "test_control"}}, PrevCommit: "wrong123", - GhPriorCommit: "def456", + PriorCommit: "def456", Link: "https://github.com/test-owner/test-repo/commit/def456", } }(), @@ -246,7 +241,7 @@ func TestConvertAuditResultToJSON(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := convertAuditResultToJSON(ghc, tt.result, tt.mode) + got := convertAuditResultToJSON("test-owner", "test-repo", tt.result, tt.mode) if got.Commit != tt.want.Commit { t.Errorf("Commit = %v, want %v", got.Commit, tt.want.Commit) diff --git a/internal/cmd/checklevel.go b/internal/cmd/checklevel.go index 9e845aa..a6ea4de 100644 --- a/internal/cmd/checklevel.go +++ b/internal/cmd/checklevel.go @@ -4,7 +4,6 @@ package cmd import ( - "context" "errors" "fmt" "os" @@ -12,19 +11,21 @@ import ( "github.com/spf13/cobra" "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/policy" + "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type checkLevelOpts struct { - commitOptions + revisionOpts + allowMergeCommitsOptions outputVsa, outputUnsignedVsa, useLocalPolicy string - allowMergeCommits bool } func (clo *checkLevelOpts) Validate() error { errs := []error{ - clo.commitOptions.Validate(), + clo.revisionOpts.Validate(), } return errors.Join(errs...) @@ -32,10 +33,10 @@ func (clo *checkLevelOpts) Validate() error { func (clo *checkLevelOpts) AddFlags(cmd *cobra.Command) { clo.commitOptions.AddFlags(cmd) + clo.allowMergeCommitsOptions.AddFlags(cmd) cmd.PersistentFlags().StringVar(&clo.outputVsa, "output_vsa", "", "The path to write a signed VSA with the determined level.") cmd.PersistentFlags().StringVar(&clo.outputUnsignedVsa, "output_unsigned_vsa", "", "The path to write an unsigned vsa with the determined level.") cmd.PersistentFlags().StringVar(&clo.useLocalPolicy, "use_local_policy", "", "UNSAFE: Use the policy at this local path instead of the official one.") - cmd.PersistentFlags().BoolVar(&clo.allowMergeCommits, "allow-merge-commits", false, "[EXPERIMENTAL] Allow merge commits in branch.") } func addCheckLevel(parentCmd *cobra.Command) { @@ -71,51 +72,65 @@ This is meant to be run within the corresponding GitHub Actions workflow.`, return err } - return doCheckLevel(&opts) - }, - } - opts.AddFlags(checklevelCmd) - parentCmd.AddCommand(checklevelCmd) -} + authenticator, err := CheckAuth() + if err != nil { + return err + } -func doCheckLevel(cla *checkLevelOpts) error { - ghconnection := ghcontrol.NewGhConnection(cla.owner, cla.repository, ghcontrol.BranchToFullRef(cla.branch)).WithAuthToken(githubToken) - ghconnection.Options.AllowMergeCommits = cla.allowMergeCommits + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), + ) + if err != nil { + return err + } - ctx := context.Background() - controlStatus, err := ghconnection.GetBranchControlsAtCommit(ctx, cla.commit, ghconnection.GetFullRef()) - if err != nil { - return err - } - pe := policy.NewPolicyEvaluator() - pe.UseLocalPolicy = cla.useLocalPolicy - verifiedLevels, policyPath, err := pe.EvaluateControl(ctx, cla.GetRepository(), cla.GetBranch(), controlStatus) - if err != nil { - return err - } - fmt.Print(verifiedLevels) + controlStatus, err := srctool.GetBranchControlsAtCommit(cmd.Context(), opts.GetBranch(), &models.Commit{SHA: opts.commit}) + if err != nil { + return err + } - unsignedVsa, err := attest.CreateUnsignedSourceVsa(ghconnection.GetRepoUri(), ghconnection.GetFullRef(), cla.commit, verifiedLevels, policyPath) - if err != nil { - return err - } - if cla.outputUnsignedVsa != "" { - if err := os.WriteFile(cla.outputUnsignedVsa, []byte(unsignedVsa), 0o644); err != nil { //nolint:gosec - return err - } - } + pe := policy.NewPolicyEvaluator() + pe.UseLocalPolicy = opts.useLocalPolicy + verifiedLevels, policyPath, err := pe.EvaluateControl(cmd.Context(), opts.GetRepository(), opts.GetBranch(), controlStatus) + if err != nil { + return err + } + for _, level := range verifiedLevels { + if slsa.IsSlsaSourceLevel(level) { + fmt.Print(level) + break + } + } - if cla.outputVsa != "" { - // This will output in the sigstore bundle format. - signedVsa, err := attest.Sign(unsignedVsa) - if err != nil { - return err - } - err = os.WriteFile(cla.outputVsa, []byte(signedVsa), 0o644) //nolint:gosec - if err != nil { - return err - } - } + unsignedVsa, err := attest.CreateUnsignedSourceVsa( + opts.GetBranch(), opts.GetCommit(), verifiedLevels, policyPath, + ) + if err != nil { + return err + } + if opts.outputUnsignedVsa != "" { + if err := os.WriteFile(opts.outputUnsignedVsa, []byte(unsignedVsa), 0o644); err != nil { //nolint:gosec + return err + } + } + + if opts.outputVsa != "" { + // This will output in the sigstore bundle format. + signedVsa, err := attest.Sign(unsignedVsa) + if err != nil { + return err + } + err = os.WriteFile(opts.outputVsa, []byte(signedVsa), 0o644) //nolint:gosec + if err != nil { + return err + } + } - return nil + return nil + }, + } + opts.AddFlags(checklevelCmd) + parentCmd.AddCommand(checklevelCmd) } diff --git a/internal/cmd/checklevelprov.go b/internal/cmd/checklevelprov.go index 0408ccc..d47bf17 100644 --- a/internal/cmd/checklevelprov.go +++ b/internal/cmd/checklevelprov.go @@ -4,30 +4,19 @@ package cmd import ( - "context" "errors" "fmt" - "log" - "os" "slices" "strings" - "github.com/carabiner-dev/attestation" - "github.com/carabiner-dev/collector" - "github.com/carabiner-dev/collector/envelope" - "github.com/carabiner-dev/collector/repository/github" - "github.com/carabiner-dev/collector/repository/note" "github.com/spf13/cobra" - "google.golang.org/protobuf/encoding/protojson" - "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/auth" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" - "github.com/slsa-framework/source-tool/pkg/policy" + "github.com/slsa-framework/source-tool/pkg/sourcetool" ) type pushOptions struct { - pushLocation []string + pushLocation []string + pushRepositories []string } // Support repository types to push @@ -61,89 +50,34 @@ func (po *pushOptions) AddFlags(cmd *cobra.Command) { cmd.PersistentFlags().StringSliceVar(&po.pushLocation, "push", []string{}, fmt.Sprintf("Push signed attestations to storage %v", supportedPushRepos)) } -func (po *pushOptions) GetCollectorAgent(opts commitOptions, token string) (*collector.Agent, error) { - if len(po.pushLocation) == 0 { - return nil, nil - } - - // Create the attestation storage repositories - agent, err := collector.New() - if err != nil { - return nil, err - } - - for _, uri := range po.pushLocation { - var repo attestation.Repository - var err error - - // Translate just "note" or "github" to the full repo spec acting on - // the sepcified commit - switch uri { - case note.TypeMoniker: - uri = fmt.Sprintf( - "note:git+https://github.com/%s/%s@%s", - opts.owner, opts.repository, opts.commit, - ) - case github.TypeMoniker: - uri = fmt.Sprintf("github:%s/%s", opts.owner, opts.repository) - } - switch { - case strings.HasPrefix(uri, "github:"): - repo, err = github.New( - // Initialize the github repository - github.WithInit(uri), - // We pass the token to use in the githu client - github.WithToken(token), - ) - case strings.HasPrefix(uri, "note:"): - repo, err = note.New( - // Initialize the notes repository - note.WithInit(uri), - // Push is enabled as we will append the note to the remote - note.WithPush(true), - // Push via http, using the GH access token - note.WithHttpAuth("x-access-token", token), - ) - default: - return nil, fmt.Errorf("repository type not supported") - } - if err != nil { - return nil, fmt.Errorf("creating storage repository: %w", err) - } - agent.AddRepository(repo) //nolint:errcheck,gosec // always returns nil - } - - return agent, nil -} - type checkLevelProvOpts struct { - commitOptions + revisionOpts verifierOptions pushOptions + allowMergeCommitsOptions prevBundlePath string prevCommit string outputUnsignedBundle string outputSignedBundle string useLocalPolicy string - allowMergeCommits bool } func (clp *checkLevelProvOpts) Validate() error { return errors.Join([]error{ - clp.commitOptions.Validate(), + clp.revisionOpts.Validate(), clp.verifierOptions.Validate(), }...) } func (clp *checkLevelProvOpts) AddFlags(cmd *cobra.Command) { - clp.commitOptions.AddFlags(cmd) + clp.revisionOpts.AddFlags(cmd) clp.pushOptions.AddFlags(cmd) + clp.allowMergeCommitsOptions.AddFlags(cmd) cmd.PersistentFlags().StringVar(&clp.prevBundlePath, "prev_bundle_path", "", "Path to the file with the attestations for the previous commit (as an in-toto bundle).") cmd.PersistentFlags().StringVar(&clp.prevCommit, "prev_commit", "", "The commit to check.") cmd.PersistentFlags().StringVar(&clp.outputUnsignedBundle, "output_unsigned_bundle", "", "The path to write a bundle of unsigned attestations.") cmd.PersistentFlags().StringVar(&clp.outputSignedBundle, "output_signed_bundle", "", "The path to write a bundle of signed attestations.") cmd.PersistentFlags().StringVar(&clp.useLocalPolicy, "use_local_policy", "", "UNSAFE: Use the policy at this local path instead of the official one.") - cmd.PersistentFlags().BoolVar(&clp.allowMergeCommits, "allow-merge-commits", false, "[EXPERIMENTAL] Allow merge commits in branch") } func addCheckLevelProv(parentCmd *cobra.Command) { @@ -184,130 +118,67 @@ and pushed to its remote (--push=note). return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return doCheckLevelProv(opts) - }, - } - - opts.AddFlags(checklevelprovCmd) - parentCmd.AddCommand(checklevelprovCmd) -} - -func doCheckLevelProv(checkLevelProvArgs *checkLevelProvOpts) error { - t := githubToken - var err error - if t == "" { - t, err = auth.New().ReadToken() - if err != nil { - return err - } - } - ghconnection := ghcontrol.NewGhConnection(checkLevelProvArgs.owner, checkLevelProvArgs.repository, ghcontrol.BranchToFullRef(checkLevelProvArgs.branch)).WithAuthToken(t) - ghconnection.Options.AllowMergeCommits = checkLevelProvArgs.allowMergeCommits - ctx := context.Background() - - prevCommit := checkLevelProvArgs.prevCommit - if prevCommit == "" { - prevCommit, err = ghconnection.GetPriorCommit(ctx, checkLevelProvArgs.commit) - if err != nil { - return err - } - } - - pa := attest.NewProvenanceAttestor(ghconnection, getVerifier(&checkLevelProvArgs.verifierOptions)) - prov, err := pa.CreateSourceProvenance(ctx, checkLevelProvArgs.prevBundlePath, checkLevelProvArgs.commit, prevCommit, ghconnection.GetFullRef()) - if err != nil { - return err - } - - // check p against policy - pe := policy.NewPolicyEvaluator() - pe.UseLocalPolicy = checkLevelProvArgs.useLocalPolicy - verifiedLevels, policyPath, err := pe.EvaluateSourceProv(ctx, checkLevelProvArgs.GetRepository(), checkLevelProvArgs.GetBranch(), prov) - if err != nil { - return err - } - - // create vsa - unsignedVsa, err := attest.CreateUnsignedSourceVsa(ghconnection.GetRepoUri(), ghconnection.GetFullRef(), checkLevelProvArgs.commit, verifiedLevels, policyPath) - if err != nil { - return err - } - - unsignedProv, err := protojson.Marshal(prov) - if err != nil { - return err - } + // Here we need to translate the CLI options to the sourcetool + // options. Some of these will be deprecated as some point for + // a more concise options set. + signAttestation := false + outputPath := "" + + switch { + case opts.outputSignedBundle != "": + outputPath = opts.outputSignedBundle + signAttestation = true + case opts.outputUnsignedBundle != "": + outputPath = opts.outputUnsignedBundle + } - // Store both the unsigned provenance and vsa - switch { - case checkLevelProvArgs.outputUnsignedBundle != "": - f, err := os.OpenFile(checkLevelProvArgs.outputUnsignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec - if err != nil { - return err - } - defer f.Close() //nolint:errcheck + var githubStorer, notesStorer, pushAttestations bool + switch { + case slices.Contains(opts.pushLocation, "github"): + pushAttestations = true + githubStorer = true + case slices.Contains(opts.pushLocation, "notes"): + pushAttestations = true + notesStorer = true + case len(opts.pushRepositories) > 0: + pushAttestations = true + } - if _, err := f.WriteString(string(unsignedProv) + "\n" + unsignedVsa + "\n"); err != nil { - return fmt.Errorf("writing signed bundle: %w", err) - } - case checkLevelProvArgs.outputSignedBundle != "" || len(checkLevelProvArgs.pushLocation) > 0: - var f *os.File - if checkLevelProvArgs.outputSignedBundle != "" { - f, err = os.OpenFile(checkLevelProvArgs.outputSignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec + // Create the authenticator + authenticator, err := CheckAuth() if err != nil { return err } - } - defer func() { - if f != nil { - f.Close() //nolint:errcheck,gosec - } - }() - - signedProv, err := attest.Sign(string(unsignedProv)) - if err != nil { - return err - } - - signedVsa, err := attest.Sign(unsignedVsa) - if err != nil { - return err - } - // If a file was specified, write the attestations - if f != nil { - if _, err := f.WriteString(signedProv + "\n" + signedVsa + "\n"); err != nil { - return fmt.Errorf("writing bundle data: %w", err) - } - } - - cl, err := checkLevelProvArgs.GetCollectorAgent(checkLevelProvArgs.commitOptions, t) - if err != nil { - return fmt.Errorf("creating storage repositories: %w", err) - } - - // If there are any storage repositories configured, push the attestations - if cl != nil { - // Parse the attestations into envelopes - envProv, err := envelope.Parsers.Parse(strings.NewReader(signedProv)) - if err != nil || len(envProv) == 0 { - return fmt.Errorf("parsing provenance: %w", err) - } - envVsa, err := envelope.Parsers.Parse(strings.NewReader(signedVsa)) - if err != nil || len(envVsa) == 0 { - return fmt.Errorf("parsing VSA: %w", err) + // Initialize sourcetool + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), + sourcetool.WithNotesStorer(notesStorer), + sourcetool.WithGithubStorer(githubStorer), + ) + if err != nil { + return fmt.Errorf("creating sourcetool: %w", err) } - // And store them - err = cl.Store(ctx, []attestation.Envelope{envProv[0], envVsa[0]}) + // Attest the commit passing the options + verifiedLevels, err := srctool.AttestRevision( + cmd.Context(), opts.GetBranch(), opts.GetRevision(), + sourcetool.WithLocalPolicy(opts.useLocalPolicy), + sourcetool.WithOutputPath(outputPath), + sourcetool.WithSign(signAttestation), + sourcetool.WithUseStdout(true), + sourcetool.WithPush(pushAttestations), + ) if err != nil { - return fmt.Errorf("storing attestations: %w", err) + return fmt.Errorf("attesting commit: %w", err) } - } - default: - log.Printf("unsigned prov: %s\n", unsignedProv) - log.Printf("unsigned vsa: %s\n", unsignedVsa) + + fmt.Print(verifiedLevels.Levels()) + return nil + }, } - fmt.Print(verifiedLevels) - return nil + + opts.AddFlags(checklevelprovCmd) + parentCmd.AddCommand(checklevelprovCmd) } diff --git a/internal/cmd/checktag.go b/internal/cmd/checktag.go index cd6b0b5..057e03c 100644 --- a/internal/cmd/checktag.go +++ b/internal/cmd/checktag.go @@ -4,46 +4,44 @@ package cmd import ( - "context" "errors" "fmt" - "log" - "os" + "slices" "github.com/spf13/cobra" - "google.golang.org/protobuf/encoding/protojson" - "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" - "github.com/slsa-framework/source-tool/pkg/policy" + "github.com/slsa-framework/source-tool/pkg/sourcetool" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type checkTagOptions struct { - repoOptions verifierOptions - commit string - tagName string - actor string - outputSignedBundle string - useLocalPolicy string - vsaRetries uint8 + revisionOpts + pushOptions + allowMergeCommitsOptions + actor string + outputSignedBundle string + outputUnsignedBundle string + useLocalPolicy string + vsaRetries uint8 } func (cto *checkTagOptions) Validate() error { errs := []error{ - cto.repoOptions.Validate(), + cto.revisionOpts.Validate(), cto.verifierOptions.Validate(), } return errors.Join(errs...) } func (cto *checkTagOptions) AddFlags(cmd *cobra.Command) { - cto.repoOptions.AddFlags(cmd) + cto.revisionOpts.AddFlags(cmd) cto.verifierOptions.AddFlags(cmd) - cmd.PersistentFlags().StringVar(&cto.commit, "commit", "", "The commit to check - required.") - cmd.PersistentFlags().StringVar(&cto.tagName, "tag_name", "", "The name of the new tag - required.") + cto.pushOptions.AddFlags(cmd) + cto.allowMergeCommitsOptions.AddFlags(cmd) cmd.PersistentFlags().StringVar(&cto.actor, "actor", "", "The username of the actor that pushed the tag.") cmd.PersistentFlags().StringVar(&cto.outputSignedBundle, "output_signed_bundle", "", "The path to write a bundle of signed attestations.") + cmd.PersistentFlags().StringVar(&cto.outputUnsignedBundle, "output_unsigned_bundle", "", "The path to write a bundle of unsigned attestations.") cmd.PersistentFlags().StringVar(&cto.useLocalPolicy, "use_local_policy", "", "UNSAFE: Use the policy at this local path instead of the official one.") cmd.PersistentFlags().Uint8Var(&cto.vsaRetries, "retries", 3, "Number of times to retry fetching the commit's VSA") } @@ -55,72 +53,92 @@ func addCheckTag(parentCmd *cobra.Command) { Use: "checktag", GroupID: "assessment", Short: "Checks to see if the tag operation should be allowed and issues a VSA", + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + if err := opts.ParseLocator(args[0]); err != nil { + return err + } + } + + if err := opts.repoOptions.Validate(); err != nil { + return err + } + + if err := opts.EnsureDefaults(); err != nil { + return err + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { - return doCheckTag(opts) + // Here we need to translate the CLI options to the sourcetool + // options. Some of these will be deprecated as some point for + // a more concise options set. + signAttestation := false + outputPath := "" + + switch { + case opts.outputSignedBundle != "": + outputPath = opts.outputSignedBundle + signAttestation = true + case opts.outputUnsignedBundle != "": + outputPath = opts.outputUnsignedBundle + } + + var githubStorer, notesStorer, pushAttestations bool + switch { + case slices.Contains(opts.pushLocation, "github"): + pushAttestations = true + githubStorer = true + case slices.Contains(opts.pushLocation, "notes"): + pushAttestations = true + notesStorer = true + case len(opts.pushRepositories) > 0: + pushAttestations = true + } + + rev := opts.GetRevision() + if rev == nil { + return errors.New("unable to get revision from configured options") + } + if _, ok := rev.(*models.Tag); !ok { + return errors.New("revision is not a tag") + } + + // Create the authenticator + authenticator, err := CheckAuth() + if err != nil { + return err + } + + // Initialize sourcetool + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), + sourcetool.WithNotesStorer(notesStorer), + sourcetool.WithGithubStorer(githubStorer), + ) + if err != nil { + return fmt.Errorf("creating sourcetool: %w", err) + } + + // Attest the commit passing the options + verifiedLevels, err := srctool.AttestRevision( + cmd.Context(), opts.GetBranch(), opts.GetRevision(), + sourcetool.WithLocalPolicy(opts.useLocalPolicy), + sourcetool.WithOutputPath(outputPath), + sourcetool.WithSign(signAttestation), + sourcetool.WithUseStdout(true), + sourcetool.WithPush(pushAttestations), + ) + if err != nil { + return fmt.Errorf("attesting commit: %w", err) + } + + fmt.Print(verifiedLevels.Levels()) + return nil }, } opts.AddFlags(checktagCmd) parentCmd.AddCommand(checktagCmd) } - -func doCheckTag(args *checkTagOptions) error { - ghconnection := ghcontrol.NewGhConnection(args.owner, args.repository, ghcontrol.TagToFullRef(args.tagName)).WithAuthToken(githubToken) - ctx := context.Background() - verifier := getVerifier(&args.verifierOptions) - - // Create tag provenance. - pa := attest.NewProvenanceAttestor(ghconnection, verifier) - pa.Options.VsaRetries = args.vsaRetries // Retry fetching the commit's VSA - - prov, err := pa.CreateTagProvenance(ctx, args.commit, ghcontrol.TagToFullRef(args.tagName), args.actor) - if err != nil { - return fmt.Errorf("creating tag provenance metadata: %w", err) - } - - // check p against policy - pe := policy.NewPolicyEvaluator() - pe.UseLocalPolicy = args.useLocalPolicy - verifiedLevels, policyPath, err := pe.EvaluateTagProv(ctx, args.GetRepository(), prov) - if err != nil { - return fmt.Errorf("evaluating the tag provenance metadata: %w", err) - } - - // create vsa - unsignedVsa, err := attest.CreateUnsignedSourceVsa(ghconnection.GetRepoUri(), ghconnection.GetFullRef(), args.commit, verifiedLevels, policyPath) - if err != nil { - return err - } - - unsignedProv, err := protojson.Marshal(prov) - if err != nil { - return err - } - - if args.outputSignedBundle != "" { - f, err := os.OpenFile(args.outputSignedBundle, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644) //nolint:gosec - if err != nil { - return err - } - defer f.Close() //nolint:errcheck - - signedProv, err := attest.Sign(string(unsignedProv)) - if err != nil { - return err - } - - signedVsa, err := attest.Sign(unsignedVsa) - if err != nil { - return err - } - - if _, err := f.WriteString(signedProv + "\n" + signedVsa + "\n"); err != nil { - return fmt.Errorf("writing bundledata: %w", err) - } - } else { - log.Printf("unsigned prov: %s\n", unsignedProv) - log.Printf("unsigned vsa: %s\n", unsignedVsa) - } - fmt.Print(verifiedLevels) - return nil -} diff --git a/internal/cmd/options.go b/internal/cmd/options.go index 93f7cf2..d902607 100644 --- a/internal/cmd/options.go +++ b/internal/cmd/options.go @@ -14,6 +14,7 @@ import ( "github.com/slsa-framework/source-tool/pkg/auth" "github.com/slsa-framework/source-tool/pkg/ghcontrol" + "github.com/slsa-framework/source-tool/pkg/sourcetool" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) @@ -123,21 +124,19 @@ func (bo *branchOptions) EnsureDefaults() error { return nil } - t := githubToken - var err error - if t == "" { - t, err = auth.New().ReadToken() - if err != nil { - return err - } + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(auth.New()), + ) + if err != nil { + return err } - gcx := ghcontrol.NewGhConnection(bo.owner, bo.repository, "").WithAuthToken(t) - branch, err := gcx.GetDefaultBranch(context.Background()) + branch, err := srctool.Backend().GetDefaultBranch(context.Background(), bo.GetRepository()) if err != nil { - return fmt.Errorf("reading repository default branch: %w", err) + return fmt.Errorf("getting default branch: %w", err) } - bo.branch = branch + bo.branch = branch.GetName() return nil } @@ -210,6 +209,12 @@ func (co *commitOptions) EnsureDefaults() error { return nil } +func (co *commitOptions) GetCommit() *models.Commit { + return &models.Commit{ + SHA: co.commit, + } +} + type verifierOptions struct { expectedIssuer string expectedSan string @@ -223,3 +228,143 @@ func (vo *verifierOptions) AddFlags(cmd *cobra.Command) { cmd.PersistentFlags().StringVar(&vo.expectedIssuer, "expected_issuer", "", "The expected issuer of the attestation signer certificate") cmd.PersistentFlags().StringVar(&vo.expectedSan, "expected_san", "", "The expected SAN string in the attestation signer certificate") } + +type tagOptions struct { + branchOptions + tag string +} + +func (to *tagOptions) Validate() error { + return nil +} + +func (to *tagOptions) AddFlags(cmd *cobra.Command) { + cmd.PersistentFlags().StringVar(&to.tag, "tag", "", "Git tag within the repository") +} + +type revisionOpts struct { + repoOptions + tagOptions + branchOptions + commitOptions +} + +func (ro *revisionOpts) Validate() error { + errs := []error{} + if ro.tag == "" && ro.commit == "" { + errs = append(errs, errors.New("unable to compute revision: no commit or tag set")) + } + return errors.Join(errs...) +} + +func (ro *revisionOpts) AddFlags(cmd *cobra.Command) { + // branchOptions.AddFlags already registers repo, owner, and branch flags + ro.branchOptions.AddFlags(cmd) + // Only register tag; tagOptions.AddFlags doesn't chain to branchOptions + ro.tagOptions.AddFlags(cmd) + // Register commit flag directly to avoid re-registering branch/repo flags + cmd.PersistentFlags().StringVarP( + &ro.commit, "commit", "c", "", "commit digest (sha1)", + ) +} + +func (ro *revisionOpts) GetRevision() models.Revision { + if ro.tag != "" { + return &models.Tag{ + Name: ro.tag, + Commit: ro.GetCommit(), + Repository: ro.GetRepository(), + } + } else if ro.commit != "" { + return ro.GetCommit() + } + return nil +} + +func (ro *revisionOpts) ParseLocator(lString string) error { + if err := ro.branchOptions.ParseLocator(lString); err != nil { + return err + } + components, err := vcslocator.Locator(lString).Parse() + if err != nil { + return fmt.Errorf("parsing repository slug: %w", err) + } + + if err := ro.ParseSlug(components.RepoPath); err != nil { + return err + } + + if components.Commit != "" { + ro.commit = components.Commit + } + + if components.Tag != "" { + ro.tag = components.Tag + } + + if components.Branch != "" { + ro.tag = components.Branch + } + + return nil +} + +func (ro *revisionOpts) EnsureDefaults() error { + if err := ro.branchOptions.EnsureDefaults(); err != nil { + return err + } + + if ro.tag != "" { + ro.tagOptions.branchOptions = ro.branchOptions + + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(auth.New()), + ) + if err != nil { + return err + } + + commit, err := srctool.Backend().GetRevisionCommit(context.Background(), ro.GetRepository(), &models.Tag{ + Name: ro.tag, + }) + if err != nil { + return err + } + + ro.commit = commit.SHA + } + + if ro.commit != "" { + ro.commitOptions.branchOptions = ro.branchOptions + if err := ro.commitOptions.EnsureDefaults(); err != nil { + return err + } + } + + // If no tag and not commit where specified, get the commit at the branch tip + if ro.commit == "" && ro.tag == "" { + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(auth.New()), + ) + if err != nil { + return err + } + + commit, err := srctool.Backend().GetLatestCommit(context.Background(), ro.GetRepository(), ro.GetBranch()) + if err != nil { + return err + } + ro.commit = commit.SHA + } + + return nil +} + +type allowMergeCommitsOptions struct { + allowMergeCommits bool +} + +// AddFlags adds the subcommands flags +func (o *allowMergeCommitsOptions) AddFlags(cmd *cobra.Command) { + cmd.PersistentFlags().BoolVar(&o.allowMergeCommits, "allow-merge-commits", false, "[EXPERIMENTAL] Allow merge commits in branch.") +} diff --git a/internal/cmd/prov.go b/internal/cmd/prov.go index 79bc534..0bd1e47 100644 --- a/internal/cmd/prov.go +++ b/internal/cmd/prov.go @@ -4,39 +4,37 @@ package cmd import ( - "context" "errors" "fmt" "github.com/spf13/cobra" "google.golang.org/protobuf/encoding/protojson" - "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" + "github.com/slsa-framework/source-tool/pkg/sourcetool" ) type provOptions struct { - commitOptions + revisionOpts verifierOptions + allowMergeCommitsOptions prevAttPath, prevCommit string } func (po *provOptions) Validate() error { return errors.Join([]error{ - po.commitOptions.Validate(), + po.revisionOpts.Validate(), po.verifierOptions.Validate(), }...) } func (po *provOptions) AddFlags(cmd *cobra.Command) { - po.commitOptions.AddFlags(cmd) + po.revisionOpts.AddFlags(cmd) po.verifierOptions.AddFlags(cmd) - + po.allowMergeCommitsOptions.AddFlags(cmd) cmd.PersistentFlags().StringVar(&po.prevAttPath, "prev_att_path", "", "Path to the file with the attestations for the previous commit (as an in-toto bundle).") cmd.PersistentFlags().StringVar(&po.prevCommit, "prev_commit", "", "The commit prior to 'commit'.") } -//nolint:dupl func addProv(parentCmd *cobra.Command) { opts := provOptions{} provCmd := &cobra.Command{ @@ -67,25 +65,33 @@ func addProv(parentCmd *cobra.Command) { if err := opts.Validate(); err != nil { return fmt.Errorf("validating options: %w", err) } - return doProv(&opts) + + authenticator, err := CheckAuth() + if err != nil { + return err + } + + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + sourcetool.WithAllowMergeCommits(opts.allowMergeCommits), + ) + if err != nil { + return err + } + + newProv, err := srctool.Attester().CreateSourceProvenance(cmd.Context(), opts.GetBranch(), opts.GetCommit()) + if err != nil { + return err + } + provStr, err := protojson.Marshal(newProv) + if err != nil { + return err + } + fmt.Println(string(provStr)) + return nil }, } opts.AddFlags(provCmd) parentCmd.AddCommand(provCmd) } - -func doProv(opts *provOptions) error { - ghconnection := ghcontrol.NewGhConnection(opts.owner, opts.repository, ghcontrol.BranchToFullRef(opts.branch)).WithAuthToken(githubToken) - ctx := context.Background() - pa := attest.NewProvenanceAttestor(ghconnection, getVerifier(&opts.verifierOptions)) - newProv, err := pa.CreateSourceProvenance(ctx, opts.prevAttPath, opts.commit, opts.prevCommit, ghconnection.GetFullRef()) - if err != nil { - return err - } - provStr, err := protojson.Marshal(newProv) - if err != nil { - return err - } - fmt.Println(string(provStr)) - return nil -} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 6934e19..3e74233 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -10,22 +10,21 @@ import ( "github.com/spf13/cobra" - "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/auth" ) var githubToken string -func getVerifier(vo *verifierOptions) attest.Verifier { - options := attest.DefaultVerifierOptions - if vo.expectedIssuer != "" { - options.ExpectedIssuer = vo.expectedIssuer - } - if vo.expectedSan != "" { - options.ExpectedSan = vo.expectedSan - } - return attest.NewBndVerifier(options) -} +// func getVerifier(vo *verifierOptions) attest.Verifier { +// options := attest.DefaultVerifierOptions +// if vo.expectedIssuer != "" { +// options.ExpectedIssuer = vo.expectedIssuer +// } +// if vo.expectedSan != "" { +// options.ExpectedSan = vo.expectedSan +// } +// return attest.NewBndVerifier(options) +// } func buildRootCommand() *cobra.Command { // rootCmd represents the base command when called without any subcommands diff --git a/internal/cmd/status.go b/internal/cmd/status.go index 8adc923..ff080ae 100644 --- a/internal/cmd/status.go +++ b/internal/cmd/status.go @@ -110,13 +110,13 @@ sourcetool status myorg/myrepo@mybranch } // Get the active repository controls - controls, err := srctool.GetBranchControls(cmd.Context(), opts.GetRepository(), opts.GetBranch()) + controls, err := srctool.GetBranchControls(cmd.Context(), opts.GetBranch()) if err != nil { return fmt.Errorf("fetching active controls: %w", err) } // Compute the maximum level possible: - toplevel := policy.ComputeEligibleSlsaLevel(*controls.GetActiveControls()) + toplevel := policy.ComputeEligibleSlsaLevel(controls.GetActiveControls()) title := fmt.Sprintf( "\nSLSA Source Status for %s/%s@%s", opts.owner, opts.repository, @@ -126,10 +126,10 @@ sourcetool status myorg/myrepo@mybranch fmt.Println(w(title)) fmt.Println(strings.Repeat("=", len(title))) - var policyControlStatus *slsa.ControlStatus + var policyControlStatus *slsa.Control for _, c := range controls.Controls { if c.Name == slsa.PolicyAvailable { - policyControlStatus = &c + policyControlStatus = c continue } fmt.Printf("%-35s ", c.Name) diff --git a/internal/cmd/verifycommit.go b/internal/cmd/verifycommit.go index b23d48b..2342f20 100644 --- a/internal/cmd/verifycommit.go +++ b/internal/cmd/verifycommit.go @@ -4,21 +4,18 @@ package cmd import ( - "context" "errors" "fmt" "github.com/spf13/cobra" - "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" + "github.com/slsa-framework/source-tool/pkg/sourcetool" ) type verifyCommitOptions struct { - commitOptions verifierOptions outputOptions - tag string + revisionOpts } // VerifyCommitResult represents the result of a commit verification @@ -43,7 +40,7 @@ func (v VerifyCommitResult) String() string { func (vco *verifyCommitOptions) Validate() error { errs := []error{ - vco.commitOptions.Validate(), + vco.revisionOpts.Validate(), vco.verifierOptions.Validate(), vco.outputOptions.Validate(), } @@ -51,15 +48,12 @@ func (vco *verifyCommitOptions) Validate() error { } func (vco *verifyCommitOptions) AddFlags(cmd *cobra.Command) { + vco.tagOptions.AddFlags(cmd) vco.commitOptions.AddFlags(cmd) vco.verifierOptions.AddFlags(cmd) vco.outputOptions.AddFlags(cmd) - cmd.PersistentFlags().StringVar( - &vco.tag, "tag", "", "The tag within the repository", - ) } -//nolint:dupl func addVerifyCommit(cmd *cobra.Command) { opts := verifyCommitOptions{} verifyCommitCmd := &cobra.Command{ @@ -89,55 +83,58 @@ func addVerifyCommit(cmd *cobra.Command) { if err := opts.Validate(); err != nil { return fmt.Errorf("validating options: %w", err) } - return doVerifyCommit(&opts) - }, - } - opts.AddFlags(cmd) - cmd.AddCommand(verifyCommitCmd) -} + authenticator, err := CheckAuth() + if err != nil { + return err + } -func doVerifyCommit(opts *verifyCommitOptions) error { - var ref string - var refType string - var refName string - switch { - case opts.branch != "": - ref = ghcontrol.BranchToFullRef(opts.branch) - refType = "branch" - refName = opts.branch - case opts.tag != "": - ref = ghcontrol.TagToFullRef(opts.tag) - refType = "tag" - refName = opts.tag - default: - return fmt.Errorf("must specify either branch or tag") - } + // Create a new sourcetool object + srctool, err := sourcetool.New( + sourcetool.WithAuthenticator(authenticator), + ) + if err != nil { + return err + } + + var refType string + var refName string + switch { + case opts.branch != "": + refType = "branch" + refName = opts.branch + case opts.tag != "": + refType = "tag" + refName = opts.tag + default: + return fmt.Errorf("must specify either branch or tag") + } - ghconnection := ghcontrol.NewGhConnection(opts.owner, opts.repository, ref).WithAuthToken(githubToken) - ctx := context.Background() + _, vsaPred, err := srctool.Attester().GetRevisionVSA(cmd.Context(), opts.GetBranch(), opts.GetCommit()) + if err != nil { + return err + } - _, vsaPred, err := attest.GetVsa(ctx, ghconnection, getVerifier(&opts.verifierOptions), opts.commit, ghconnection.GetFullRef()) - if err != nil { - return err - } + result := VerifyCommitResult{ + Success: vsaPred != nil, + Commit: opts.commit, + Ref: refName, + RefType: refType, + Owner: opts.owner, + Repository: opts.repository, + } - result := VerifyCommitResult{ - Success: vsaPred != nil, - Commit: opts.commit, - Ref: refName, - RefType: refType, - Owner: opts.owner, - Repository: opts.repository, - } + if vsaPred == nil { + result.Message = fmt.Sprintf( + "no VSA matching commit '%s' on %s '%s' found in github.com/%s/%s", + opts.commit, refType, refName, opts.owner, opts.repository, + ) + return opts.writeResult(result) + } - if vsaPred == nil { - result.Message = fmt.Sprintf( - "no VSA matching commit '%s' on %s '%s' found in github.com/%s/%s", - opts.commit, refType, refName, opts.owner, opts.repository, - ) - return opts.writeResult(result) + result.VerifiedLevels = vsaPred.GetVerifiedLevels() + return opts.writeResult(result) + }, } - - result.VerifiedLevels = vsaPred.GetVerifiedLevels() - return opts.writeResult(result) + opts.AddFlags(cmd) + cmd.AddCommand(verifyCommitCmd) } diff --git a/pkg/attest/attester.go b/pkg/attest/attester.go new file mode 100644 index 0000000..8fef6a8 --- /dev/null +++ b/pkg/attest/attester.go @@ -0,0 +1,216 @@ +// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors +// SPDX-License-Identifier: Apache-2.0 + +package attest + +import ( + "context" + "errors" + "fmt" + "slices" + "time" + + intoto "github.com/in-toto/attestation/go/v1" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/slsa-framework/source-tool/pkg/auth" + "github.com/slsa-framework/source-tool/pkg/provenance" + "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" +) + +type AttesterOptions struct { + // Initialize dynamic notes fetcher and storer + InitNotesCollector bool + + // Initialize attestations store collector and storer + InitGHCollector bool + + // Additional read repositories + Repos []string + + // Times to retry fetching attestations + Retries uint8 +} + +var defaultAttesterOptions = AttesterOptions{ + InitNotesCollector: true, + Retries: 3, +} + +type Attester struct { + verifier Verifier + backend models.VcsBackend + Options AttesterOptions + authenticator *auth.Authenticator +} + +type optFn func(*Attester) error + +func WithRepository(repos ...string) optFn { + return func(a *Attester) error { + // TODO(puerco): Validate the repository strings + for _, s := range repos { + if !slices.Contains(a.Options.Repos, s) { + a.Options.Repos = append(a.Options.Repos, s) + } + } + return nil + } +} + +func WithRetries(r uint8) optFn { + return func(a *Attester) error { + a.Options.Retries = r + return nil + } +} + +func WithAuthenticator(athn *auth.Authenticator) optFn { + return func(a *Attester) error { + a.authenticator = athn + return nil + } +} + +func WithBackend(b models.VcsBackend) optFn { + return func(a *Attester) error { + a.backend = b + return nil + } +} + +func WithVerifier(vf Verifier) optFn { + return func(a *Attester) error { + a.verifier = vf + return nil + } +} + +func WithGithubCollector(yesno bool) optFn { + return func(a *Attester) error { + a.Options.InitGHCollector = yesno + return nil + } +} + +func WithNotesCollector(yesno bool) optFn { + return func(a *Attester) error { + a.Options.InitNotesCollector = yesno + return nil + } +} + +// Validate checks that the attester configuration is complete +func (a *Attester) Validate() error { + errs := []error{} + + // Check a backend is configured + if a.backend == nil { + errs = append(errs, errors.New("attester has no backend defined")) + } + + // Check we have attestation repos to read + if len(a.Options.Repos) == 0 && !a.Options.InitGHCollector && !a.Options.InitNotesCollector { + errs = append(errs, errors.New("no attestation repository configured")) + } + + // Check we have a signature verifier + if a.verifier == nil { + errs = append(errs, errors.New("attester has no verifier")) + } + + return errors.Join(errs...) +} + +// NewAttester creates a new attester +func NewAttester(fn ...optFn) (*Attester, error) { + attester := &Attester{ + Options: defaultAttesterOptions, + } + + for _, f := range fn { + if err := f(attester); err != nil { + return nil, err + } + } + + // Check attester configuration + if err := attester.Validate(); err != nil { + return nil, err + } + + return attester, nil +} + +// createCurrentProvenance creates the provenance statement for the specified commit +// without any context from the previous provenance (if any). +func (a *Attester) createCurrentProvenance(ctx context.Context, branch *models.Branch, commit, prevCommit *models.Commit) (*intoto.Statement, error) { + // Get the active controls + controlStatus, err := a.backend.GetBranchControlsAtCommit(ctx, branch, commit) + if err != nil { + return nil, err + } + + if controlStatus == nil { + return nil, errors.New("VCS backend returned a nil controlset") + } + + // Build the provenance predicate + curProvPred := provenance.SourceProvenancePred{ + PrevCommit: prevCommit.SHA, + RepoUri: branch.Repository.GetHttpURL(), + ActivityType: controlStatus.ActivityType, + Actor: controlStatus.ActorLogin, + Branch: branch.FullRef(), + CreatedOn: timestamppb.New(time.Now()), + Controls: controlStatus.ToProvenanceControls(), + } + + // At the very least provenance is available starting now. :) + // ... indeed, but don't set the `since`` date because doing so breaks + // checking against policies. + // See https://github.com/slsa-framework/source-tool/issues/272 + if curProvPred.GetControl(slsa.SLSA_SOURCE_SCS_PROVENANCE.String()) == nil { + curProvPred.AddControl( + &provenance.Control{ + Name: slsa.SLSA_SOURCE_SCS_PROVENANCE.String(), + }, + ) + } + + return addPredToStatement(&curProvPred, provenance.SourceProvPredicateType, commit.SHA) +} + +// addPredToStatement generates a new statement and adds the provenance predicate +func addPredToStatement(provPred proto.Message, predicateType, commit string) (*intoto.Statement, error) { + predJson, err := protojson.MarshalOptions{ + Multiline: true, + Indent: " ", + }.Marshal(provPred) + if err != nil { + return nil, fmt.Errorf("marshaling predicate proto: %w", err) + } + + sub := []*intoto.ResourceDescriptor{{ + Digest: map[string]string{"gitCommit": commit}, + }} + + var predPb structpb.Struct + err = protojson.Unmarshal(predJson, &predPb) + if err != nil { + return nil, err + } + + statementPb := &intoto.Statement{ + Type: intoto.StatementTypeUri, + Subject: sub, + PredicateType: predicateType, + Predicate: &predPb, + } + + return statementPb, nil +} diff --git a/pkg/attest/log.go b/pkg/attest/log.go index 7d10a16..977fe37 100644 --- a/pkg/attest/log.go +++ b/pkg/attest/log.go @@ -9,6 +9,5 @@ import ( ) func Debugf(format string, args ...any) { - //nolint:gosec // G706 This is feneral purpose logger slog.Debug(fmt.Sprintf(format, args...)) } diff --git a/pkg/attest/provenance.go b/pkg/attest/provenance.go index d0f074f..2ffefb5 100644 --- a/pkg/attest/provenance.go +++ b/pkg/attest/provenance.go @@ -1,45 +1,31 @@ -// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors +// SPDX-FileCopyrightText: Copyright 2026 The SLSA Authors // SPDX-License-Identifier: Apache-2.0 package attest import ( - "bufio" "context" "errors" "fmt" - "log" - "os" "strings" "time" - v1 "github.com/in-toto/attestation/go/predicates/vsa/v1" - spb "github.com/in-toto/attestation/go/v1" + "github.com/carabiner-dev/attestation" + "github.com/carabiner-dev/collector" + "github.com/carabiner-dev/collector/filters" + "github.com/carabiner-dev/collector/repository/github" + "github.com/carabiner-dev/collector/repository/note" + vsa "github.com/in-toto/attestation/go/predicates/vsa/v1" + intoto "github.com/in-toto/attestation/go/v1" "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) -type ProvenanceAttestorOptions struct { - VsaRetries uint8 -} - -type ProvenanceAttestor struct { - verifier Verifier - gh_connection *ghcontrol.GitHubConnection - Options ProvenanceAttestorOptions -} - -func NewProvenanceAttestor(gh_connection *ghcontrol.GitHubConnection, verifier Verifier) *ProvenanceAttestor { - return &ProvenanceAttestor{verifier: verifier, gh_connection: gh_connection} -} - -func GetSourceProvPred(statement *spb.Statement) (*provenance.SourceProvenancePred, error) { +func GetSourceProvPred(statement *intoto.Statement) (*provenance.SourceProvenancePred, error) { if statement == nil { return nil, errors.New("nil statement") } @@ -66,7 +52,7 @@ func GetSourceProvPred(statement *spb.Statement) (*provenance.SourceProvenancePr return &predStruct, nil } -func GetTagProvPred(statement *spb.Statement) (*provenance.TagProvenancePred, error) { +func GetTagProvPred(statement *intoto.Statement) (*provenance.TagProvenancePred, error) { if statement == nil { return nil, errors.New("nil statement") } @@ -93,148 +79,207 @@ func GetTagProvPred(statement *spb.Statement) (*provenance.TagProvenancePred, er return &predStruct, nil } -func addPredToStatement(provPred any, predicateType, commit string) (*spb.Statement, error) { - msg, ok := provPred.(proto.Message) - if !ok { - return nil, fmt.Errorf("unable to serialize predicate as proto message") +func (a *Attester) getCollector(branch *models.Branch) (*collector.Agent, error) { + if err := collector.LoadDefaultRepositoryTypes(); err != nil { + return nil, err } - predJson, err := protojson.MarshalOptions{ - Multiline: true, - Indent: " ", - }.Marshal(msg) + agent, err := collector.New() if err != nil { - return nil, fmt.Errorf("marshaling predicate proto: %w", err) + return nil, err } - sub := []*spb.ResourceDescriptor{{ - Digest: map[string]string{"gitCommit": commit}, - }} + var token string + if a.Options.InitNotesCollector || a.Options.InitGHCollector { + token, err = a.authenticator.ReadToken() + if err != nil { + return nil, fmt.Errorf("fetching token from authenticator: %w", err) + } + } - var predPb structpb.Struct - err = protojson.Unmarshal(predJson, &predPb) - if err != nil { - return nil, err + // These two require the authenticator token + if a.Options.InitNotesCollector { + repo, err := note.NewDynamic( + note.DynamicRepoURL(branch.Repository.GetHttpURL()), + note.WithHttpAuth("github", token), + ) + if err != nil { + return nil, fmt.Errorf("creating notes collector: %w", err) + } + + if err := agent.AddRepository(repo); err != nil { + return nil, err + } } - statementPb := spb.Statement{ - Type: spb.StatementTypeUri, - Subject: sub, - PredicateType: predicateType, - Predicate: &predPb, + if a.Options.InitGHCollector { + repo, err := github.New( + github.WithRepo(branch.Repository.Path), + github.WithToken(token), + ) + if err != nil { + return nil, fmt.Errorf("creating github collector: %w", err) + } + if err := agent.AddRepository(repo); err != nil { + return nil, err + } } - return &statementPb, nil + for _, initString := range a.Options.Repos { + if err := agent.AddRepositoryFromString(initString); err != nil { + return nil, err + } + } + + return agent, nil } -// Create provenance for the current commit without any context from the previous provenance (if any). -func (pa ProvenanceAttestor) createCurrentProvenance(ctx context.Context, commit, prevCommit, ref string) (*spb.Statement, error) { - controlStatus, err := pa.gh_connection.GetBranchControlsAtCommit(ctx, commit, ref) +// GetRevisionVSA returns a revision's VSA attestation +func (a *Attester) GetRevisionVSA(ctx context.Context, branch *models.Branch, revision models.Revision) (attestation.Envelope, *vsa.VerificationSummary, error) { + if revision.GetCommit() == nil { + return nil, nil, errors.New("commit is nil") + } + c, err := a.getCollector(branch) if err != nil { - return nil, err + return nil, nil, fmt.Errorf("unable to get collector: %w", err) } - curTime := time.Now() - - var curProvPred provenance.SourceProvenancePred - curProvPred.PrevCommit = prevCommit - curProvPred.RepoUri = pa.gh_connection.GetRepoUri() - curProvPred.Actor = controlStatus.ActorLogin - curProvPred.ActivityType = controlStatus.ActivityType - curProvPred.Branch = ref - curProvPred.CreatedOn = timestamppb.New(curTime) - curProvPred.Controls = controlStatus.Controls - - // At the very least provenance is available starting now. :) - // ... indeed, but don't set the `since`` date because doing so breaks - // checking against policies. - // See https://github.com/slsa-framework/source-tool/issues/272 - curProvPred.AddControl( - &provenance.Control{ - Name: slsa.ProvenanceAvailable.String(), + // Configure the matcher to filter all the predicate types of the provenance + matcher := &filters.PredicateTypeMatcher{ + PredicateTypes: map[attestation.PredicateType]struct{}{ + attestation.PredicateType("https://slsa.dev/verification_summary/v1"): {}, }, - ) - - return addPredToStatement(&curProvPred, provenance.SourceProvPredicateType, commit) -} - -// Gets provenance for the commit from git notes. -func (pa ProvenanceAttestor) GetProvenance(ctx context.Context, commit, ref string) (*spb.Statement, *provenance.SourceProvenancePred, error) { - notes, err := pa.gh_connection.GetNotesForCommit(ctx, commit) - if notes == "" { - Debugf("didn't find notes for commit %s", commit) - return nil, nil, nil } - if err != nil { - log.Fatal(err) + var attErr error + var atts []attestation.Envelope + for i := 0; i <= int(a.Options.Retries); i++ { + // Fetch the attestations from the configured repos + atts, attErr = c.FetchAttestationsBySubject( + ctx, []attestation.Subject{revision.GetCommit().ToResourceDescriptor()}, + collector.WithQuery(attestation.NewQuery().WithFilter(matcher)), + ) + if attErr == nil { + break + } + time.Sleep(time.Duration(i*5) * time.Second) + } + if attErr != nil { + return nil, nil, fmt.Errorf("fetching attestations: %w", attErr) } - bundleReader := NewBundleReader(bufio.NewReader(strings.NewReader(notes)), pa.verifier) - - return pa.getProvFromReader(bundleReader, commit, ref) -} + if len(atts) == 0 { + Debugf("no attestations returned from collector") + return nil, nil, nil + } -func (pa ProvenanceAttestor) getProvFromReader(reader *BundleReader, commit, ref string) (*spb.Statement, *provenance.SourceProvenancePred, error) { - for { - stmt, err := reader.ReadStatement(MatchesTypeAndCommit(provenance.SourceProvPredicateType, commit)) - if err != nil { - // Ignore errors, we want to check all the lines. - Debugf("error while processing line: %v", err) + // Range the attestations and find and validate that it matches our repo + // and expected resources + for _, att := range atts { + predicate := att.GetPredicate().GetParsed() + vsaPred, ok := predicate.(*vsa.VerificationSummary) + if !ok { + // This should not happen continue } - if stmt == nil { - // No statements left. - break + // Check the verifier ID matches + if vsaPred.GetVerifier().GetId() != VsaVerifierId { + Debugf("VSA verfier ID does not match %s", VsaVerifierId) + continue } - // We know the statement is good, what about the predicate? - provPred, err := GetSourceProvPred(stmt) - if err != nil { - return nil, nil, err + // Check the VSA resource to ensure it is our repo + cleanResourceUri := strings.TrimPrefix(vsaPred.GetResourceUri(), "git+") + if branch.Repository.GetHttpURL() != "" && cleanResourceUri != branch.Repository.GetHttpURL() { + Debugf("ResourceUri is %s but we want %s", cleanResourceUri, branch.Repository.GetHttpURL()) + continue } - if pa.gh_connection.GetRepoUri() == provPred.GetRepoUri() && (ref == ghcontrol.AnyReference || provPred.GetBranch() == ref) { - // Should be good! - return stmt, provPred, nil - } else { - Debugf("prov '%v' does not reference commit '%s' for branch '%s', skipping", stmt, commit, ref) + + // And the verification is passed + // // Is the result PASSED? + if vsaPred.GetVerificationResult() != "PASSED" { + Debugf("verificationResult is %s but must be 'PASSED'", vsaPred.GetVerificationResult()) + continue } + + return att, vsaPred, nil } - Debugf("didn't find commit %s for ref %s", commit, ref) + // None of the collected attestations are valid return nil, nil, nil } -func (pa ProvenanceAttestor) getPrevProvenance(ctx context.Context, prevAttPath, prevCommit, ref string) (*spb.Statement, *provenance.SourceProvenancePred, error) { - if prevAttPath != "" { - f, err := os.Open(prevAttPath) - if err != nil { - return nil, nil, err +// GetRevisionProvenance returns the provenance attestation for a commit by querying +// the configured collectors. +func (a *Attester) GetRevisionProvenance(ctx context.Context, branch *models.Branch, commit *models.Commit) (*provenance.SourceProvenancePred, error) { + c, err := a.getCollector(branch) + if err != nil { + return nil, fmt.Errorf("unable to get collector: %w", err) + } + + // Configure the matcher to filter all the predicate types of the provenance + matcher := &filters.PredicateTypeMatcher{ + PredicateTypes: map[attestation.PredicateType]struct{}{ + attestation.PredicateType(provenance.SourceProvPredicateType): {}, + }, + } + + var attErr error + var atts []attestation.Envelope + for i := 0; i <= int(a.Options.Retries); i++ { + // Fetch the attestations from the configured repos + atts, attErr = c.FetchAttestationsBySubject( + ctx, []attestation.Subject{commit.ToResourceDescriptor()}, + collector.WithQuery(attestation.NewQuery().WithFilter(matcher)), + ) + if attErr == nil { + break } - return pa.getProvFromReader(NewBundleReader(bufio.NewReader(f), pa.verifier), prevCommit, ref) + time.Sleep(time.Duration(i*5) * time.Second) + } + if attErr != nil { + return nil, fmt.Errorf("fetching attestations: %w", attErr) } - // Try to get the previous bundle ourselves... - return pa.GetProvenance(ctx, prevCommit, ref) + if len(atts) == 0 { + Debugf("no attestations returned from collector") + return nil, nil + } + + // Now extract the provenance + for _, att := range atts { + pred := &provenance.SourceProvenancePred{} + if err = protojson.Unmarshal(att.GetPredicate().GetData(), pred); err == nil { + return pred, nil + } + } + return nil, fmt.Errorf("unable to parse predicate: %w", err) } -func (pa ProvenanceAttestor) CreateSourceProvenance(ctx context.Context, prevAttPath, commit, prevCommit, ref string) (*spb.Statement, error) { +// prevAttPath string +func (a *Attester) CreateSourceProvenance(ctx context.Context, branch *models.Branch, commit *models.Commit) (*intoto.Statement, error) { + // Get the previous commit + prevCommit, err := a.backend.GetPreviousCommit(ctx, branch, commit) + if err != nil { + return nil, fmt.Errorf("getting previous commit: %w", err) + } + // Source provenance is based on // 1. The current control situation (we assume 'commit' has _just_ occurred). // 2. How long the properties have been enforced according to the previous provenance. - - curProv, err := pa.createCurrentProvenance(ctx, commit, prevCommit, ref) + curProv, err := a.createCurrentProvenance(ctx, branch, commit, prevCommit) if err != nil { - return nil, err + return nil, fmt.Errorf("creating provenance predicate: %w", err) } - prevProvStmt, prevProvPred, err := pa.getPrevProvenance(ctx, prevAttPath, prevCommit, ref) + prevProvPred, err := a.GetRevisionProvenance(ctx, branch, prevCommit) if err != nil { return nil, err } // No prior provenance found, so we just go with current. - if prevProvStmt == nil || prevProvPred == nil { + // if prevProvStmt == nil || prevProvPred == nil { + if prevProvPred == nil { Debugf("No previous provenance found, have to bootstrap\n") return curProv, nil } @@ -257,54 +302,47 @@ func (pa ProvenanceAttestor) CreateSourceProvenance(ctx context.Context, prevAtt curProvPred.Controls[i] = curControl } - return addPredToStatement(curProvPred, provenance.SourceProvPredicateType, commit) + return addPredToStatement(curProvPred, provenance.SourceProvPredicateType, commit.SHA) } -func (pa ProvenanceAttestor) CreateTagProvenance(ctx context.Context, commit, ref, actor string) (*spb.Statement, error) { +// CreateTagProvenance creates a provenance statement for a tag. +func (a *Attester) CreateTagProvenance(ctx context.Context, branch *models.Branch, tag *models.Tag, actor string) (*intoto.Statement, error) { + if tag.Commit == nil { + return nil, fmt.Errorf("tag does not have its commit set") + } + // 1. Check that the tag hygiene control is still enabled and how long it's been enabled, store it in the prov. // 2. Get a VSA associated with this commit, if any. // 3. Record the levels and branches covered by that VSA in the provenance. - - controlStatus, err := pa.gh_connection.GetTagControls(ctx, commit, ref) + controls, err := a.backend.GetTagControls(ctx, branch, tag) if err != nil { - return nil, err + return nil, fmt.Errorf("getting tag controls: %w", err) } // Find the most recent VSA for this commit. Any reference is OK. // TODO: in the future get all of them. // TODO: we should actually verify this vsa: https://github.com/slsa-framework/source-tool/issues/148 - var tries uint8 - var vsaStatement *spb.Statement - var vsaPred *v1.VerificationSummary - for { - vsaStatement, vsaPred, err = GetVsa(ctx, pa.gh_connection, pa.verifier, commit, ghcontrol.AnyReference) - if err != nil { - return nil, fmt.Errorf("error fetching VSA when creating tag provenance %w", err) - } - - tries++ - if tries >= pa.Options.VsaRetries || vsaPred != nil { - break - } - time.Sleep(time.Duration(tries*5) * time.Second) + vsaAtt, vsaPred, err := a.GetRevisionVSA(ctx, branch, tag.Commit) // Aqui antes era: ghcontrol.AnyReference + if err != nil { + return nil, fmt.Errorf("error fetching VSA when creating tag provenance %w", err) } - if vsaPred == nil { - // TODO: If there's not a VSA should we still issue provenance? + if vsaAtt == nil { + // TODO: If there's not a VSA, should we still issue provenance? return nil, nil } - vsaRefs, err := GetSourceRefsForCommit(vsaStatement, commit) + vsaRefs, err := GetSourceRefsForCommit(vsaAtt, tag.Commit) if err != nil { return nil, fmt.Errorf("error getting source refs from vsa %w", err) } curProvPred := provenance.TagProvenancePred{ - RepoUri: pa.gh_connection.GetRepoUri(), + RepoUri: branch.Repository.GetHttpURL(), Actor: actor, - Tag: ref, + Tag: tag.Name, CreatedOn: timestamppb.Now(), - Controls: controlStatus.Controls, + Controls: controls.ToProvenanceControls(), VsaSummaries: []*provenance.VsaSummary{ { SourceRefs: vsaRefs, @@ -313,5 +351,5 @@ func (pa ProvenanceAttestor) CreateTagProvenance(ctx context.Context, commit, re }, } - return addPredToStatement(&curProvPred, provenance.TagProvPredicateType, commit) + return addPredToStatement(&curProvPred, provenance.TagProvPredicateType, tag.Commit.SHA) } diff --git a/pkg/attest/provenance_test.go b/pkg/attest/provenance_test.go deleted file mode 100644 index e030a4c..0000000 --- a/pkg/attest/provenance_test.go +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors -// SPDX-License-Identifier: Apache-2.0 - -package attest - -import ( - "encoding/json" - "testing" - "time" - - "github.com/google/go-github/v69/github" - "github.com/migueleliasweb/go-github-mock/src/mock" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/slsa-framework/source-tool/pkg/ghcontrol" - "github.com/slsa-framework/source-tool/pkg/provenance" - "github.com/slsa-framework/source-tool/pkg/slsa" - "github.com/slsa-framework/source-tool/pkg/testsupport" -) - -var rulesetOldTime = time.Now().Add(-time.Hour) - -func rulesForTagImmutability() *github.RepositoryRulesetRules { - return &github.RepositoryRulesetRules{ - Update: &github.UpdateRuleParameters{}, - Deletion: &github.EmptyRuleParameters{}, - NonFastForward: &github.EmptyRuleParameters{}, - } -} - -func conditionsForTagImmutability() *github.RepositoryRulesetConditions { - return &github.RepositoryRulesetConditions{ - RefName: &github.RepositoryRulesetRefConditionParameters{ - Include: []string{"~ALL"}, - }, - } -} - -func createTestProv(t *testing.T, repoUri, ref, commit string) string { - provPred := provenance.SourceProvenancePred{RepoUri: repoUri, Branch: ref, ActivityType: "pr_merge", Actor: "test actor"} - stmt, err := addPredToStatement(&provPred, provenance.SourceProvPredicateType, commit) - if err != nil { - t.Fatalf("failure creating test prov: %v", err) - } - - statementBytes, err := json.Marshal(&stmt) - require.NoError(t, err, "failure marshalling statement: %v", err) - - return string(statementBytes) -} - -func newNotesContent(content string) *github.RepositoryContent { - return &github.RepositoryContent{ - Content: github.Ptr(content), - } -} - -func newTagHygieneRulesetsResponse(id int64, target github.RulesetTarget, enforcement github.RulesetEnforcement, - updatedAt time.Time, -) *github.RepositoryRuleset { - return &github.RepositoryRuleset{ - ID: github.Ptr(id), - Target: github.Ptr(target), - Enforcement: enforcement, - UpdatedAt: github.Ptr(github.Timestamp{Time: updatedAt}), - Rules: rulesForTagImmutability(), - Conditions: conditionsForTagImmutability(), - } -} - -func newMockedGitHubClient(rulesetResponse *github.RepositoryRuleset, notesContent *github.RepositoryContent) *github.Client { - return github.NewClient(mock.NewMockedHTTPClient( - mock.WithRequestMatch( - mock.GetReposRulesetsByOwnerByRepo, - []*github.RepositoryRuleset{ - rulesetResponse, - }, - ), - mock.WithRequestMatch( - mock.GetReposRulesetsByOwnerByRepoByRulesetId, - *rulesetResponse, - ), - mock.WithRequestMatch( - mock.GetReposContentsByOwnerByRepoByPath, - *notesContent, - ), - )) -} - -// Helper to create a test GH Branch connection with no client. -func newTestGhConnection(owner, repo, branch string, rulesetResponse *github.RepositoryRuleset, notesContent *github.RepositoryContent) *ghcontrol.GitHubConnection { - return ghcontrol.NewGhConnectionWithClient( - owner, repo, ghcontrol.BranchToFullRef(branch), - newMockedGitHubClient(rulesetResponse, notesContent)) -} - -func timesEqualWithinMargin(t1, t2 time.Time, margin time.Duration) bool { - diff := t1.Sub(t2).Abs() - return diff <= margin -} - -func assertTagProvPredsEqual(t *testing.T, actual, expected *provenance.TagProvenancePred) { - if actual.GetActor() != expected.GetActor() { - t.Errorf("Actor %v does not match expected value %v", actual.GetActor(), expected.GetActor()) - } - - if actual.GetRepoUri() != expected.GetRepoUri() { - t.Errorf("RepoUri %v does not match expected value %v", actual.GetRepoUri(), expected.GetRepoUri()) - } - - if actual.GetTag() != expected.GetTag() { - t.Errorf("Tag %v does not match expected value %v", actual.GetTag(), expected.GetTag()) - } - - if timesEqualWithinMargin(actual.GetCreatedOn().AsTime(), expected.GetCreatedOn().AsTime(), 5*time.Second) { - t.Errorf("CreatedOn %v does not match expected value %v", actual.GetCreatedOn(), expected.GetCreatedOn()) - } - - if len(actual.GetControls()) != len(expected.GetControls()) { - t.Errorf("Control %v does not match expected value %v", actual.GetControls(), expected.GetControls()) - } else { - for ci := range actual.GetControls() { - if !timesEqualWithinMargin(actual.GetControls()[ci].GetSince().AsTime(), expected.GetControls()[ci].GetSince().AsTime(), time.Second) { - t.Errorf("control at [%d]'s time %v does not match expected time %v", ci, - actual.GetControls()[ci].GetSince(), expected.GetControls()[ci].GetSince()) - } - } - } - - require.Len(t, actual.GetVsaSummaries(), len(expected.GetVsaSummaries())) - for i := range actual.GetVsaSummaries() { - if !proto.Equal(actual.GetVsaSummaries()[i], expected.GetVsaSummaries()[i]) { - t.Errorf("VsaSummaries %v does not match expected value %v", actual.GetVsaSummaries(), expected.GetVsaSummaries()) - } - } -} - -func TestReadProvSuccess(t *testing.T) { - testProv := createTestProv(t, "https://github.com/owner/repo", "main", "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") - ghc := newTestGhConnection("owner", "repo", "branch", - // We just need _some_ rulesets response, we don't care what. - newTagHygieneRulesetsResponse(123, github.RulesetTargetTag, - github.RulesetEnforcementActive, rulesetOldTime), - newNotesContent(testProv)) - verifier := testsupport.NewMockVerifier() - - pa := NewProvenanceAttestor(ghc, verifier) - readStmt, readPred, err := pa.GetProvenance(t.Context(), "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa", "main") - if err != nil { - t.Fatalf("error finding prov: %v", err) - } - if readStmt == nil || readPred == nil { - t.Errorf("could not find provenance") - } -} - -func TestReadProvFailure(t *testing.T) { - testProv := createTestProv(t, "foo", "main", "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") - ghc := newTestGhConnection("owner", "repo", "branch", - // We just need _some_ rulesets response, we don't care what. - newTagHygieneRulesetsResponse(456, github.RulesetTargetBranch, - github.RulesetEnforcementEvaluate, rulesetOldTime), - newNotesContent(testProv)) - verifier := testsupport.NewMockVerifier() - - pa := NewProvenanceAttestor(ghc, verifier) - _, readPred, err := pa.GetProvenance(t.Context(), "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa", "main") - if err != nil { - t.Fatalf("error finding prov: %v", err) - } - if readPred != nil { - t.Errorf("should not have gotten provenance: %+v", readPred) - } -} - -func TestCreateTagProvenance(t *testing.T) { - testVsa := createTestVsa(t, "https://github.com/owner/repo", "refs/some/ref", "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa", slsa.SourceVerifiedLevels{"TEST_LEVEL"}) - - ghc := newTestGhConnection("owner", "repo", "branch", - newTagHygieneRulesetsResponse(123, github.RulesetTargetTag, - github.RulesetEnforcementActive, rulesetOldTime), - newNotesContent(testVsa)) - verifier := testsupport.NewMockVerifier() - - pa := NewProvenanceAttestor(ghc, verifier) - - stmt, err := pa.CreateTagProvenance(t.Context(), "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa", "refs/tags/v1", "the-tag-pusher") - if err != nil { - t.Fatalf("error creating tag prov %v", err) - } - - if stmt == nil { - t.Fatalf("returned statement is nil") - } - - if stmt.GetPredicateType() != provenance.TagProvPredicateType { - t.Errorf("statement pred type %v does not match expected %v", stmt.GetPredicateType(), provenance.TagProvPredicateType) - } - - if !DoesSubjectIncludeCommit(stmt, "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") { - t.Errorf("statement subject %v does not match expected %v", stmt.GetSubject(), "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") - } - - tagPred, err := GetTagProvPred(stmt) - if err != nil { - t.Fatalf("error getting tag prov %v", err) - } - - expectedPred := provenance.TagProvenancePred{ - RepoUri: "https://github.com/owner/repo", - Actor: "the-tag-pusher", - Tag: "refs/tags/v1", - CreatedOn: timestamppb.New(rulesetOldTime), - Controls: []*provenance.Control{ - { - Name: "TAG_HYGIENE", - Since: timestamppb.New(rulesetOldTime), - }, - }, - VsaSummaries: []*provenance.VsaSummary{ - { - SourceRefs: []string{"refs/some/ref"}, - VerifiedLevels: []string{"TEST_LEVEL"}, - }, - }, - } - - assertTagProvPredsEqual(t, tagPred, &expectedPred) -} diff --git a/pkg/attest/statement.go b/pkg/attest/statement.go index 8ab57b8..bad0315 100644 --- a/pkg/attest/statement.go +++ b/pkg/attest/statement.go @@ -5,15 +5,14 @@ package attest import ( "bufio" - "errors" "fmt" - "io" - "strings" - spb "github.com/in-toto/attestation/go/v1" + "github.com/carabiner-dev/attestation" + intoto "github.com/in-toto/attestation/go/v1" "google.golang.org/protobuf/encoding/protojson" "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type BundleReader struct { @@ -25,43 +24,44 @@ func NewBundleReader(reader *bufio.Reader, verifier Verifier) *BundleReader { return &BundleReader{reader: reader, verifier: verifier} } -func (br *BundleReader) convertLineToStatement(line string) (*spb.Statement, error) { - // Is this a sigstore bundle with a statement? - // Verify will check the signature, but nothing else. - vr, err := br.verifier.Verify(line) - if err == nil { - // This is it. - return vr.Statement, nil - } - - // Compatibility hack bridging identities for repository migration - // See here for more info and when to drop: - // - // https://github.com/slsa-framework/source-tool/issues/255 - if strings.Contains(err.Error(), "no matching CertificateIdentity") && strings.Contains(err.Error(), OldExpectedSan) { - ver, err := (&BndVerifier{ - Options: VerificationOptions{ - ExpectedIssuer: ExpectedIssuer, - ExpectedSan: OldExpectedSan, - }, - }).Verify(line) - if err == nil { - Debugf("found statement signed with old identity") - return ver.Statement, nil - } - } - - Debugf("Line '%s' failed verification: %v", line, err) - - // TODO: add support for 'regular' DSSEs. - - return nil, fmt.Errorf("could not convert line to statement: '%s': %w", line, err) -} - -func GetSourceRefsForCommit(vsaStatement *spb.Statement, commit string) ([]string, error) { - subject := GetSubjectForCommit(vsaStatement, commit) +// func (br *BundleReader) convertLineToStatement(line string) (*intoto.Statement, error) { +// // Is this a sigstore bundle with a statement? +// // Verify will check the signature, but nothing else. +// vr, err := br.verifier.Verify(line) +// if err == nil { +// // This is it. +// return vr.Statement, nil +// } + +// // Compatibility hack bridging identities for repository migration +// // See here for more info and when to drop: +// // +// // https://github.com/slsa-framework/source-tool/issues/255 +// if strings.Contains(err.Error(), "no matching CertificateIdentity") && strings.Contains(err.Error(), OldExpectedSan) { +// ver, err := (&BndVerifier{ +// Options: VerificationOptions{ +// ExpectedIssuer: ExpectedIssuer, +// ExpectedSan: OldExpectedSan, +// }, +// }).Verify(line) +// if err == nil { +// Debugf("found statement signed with old identity") +// return ver.Statement, nil +// } +// } + +// Debugf("Line '%s' failed verification: %v", line, err) + +// // TODO: add support for 'regular' DSSEs. + +// return nil, fmt.Errorf("could not convert line to statement: '%s': %w", line, err) +// } + +// GetSourceRefsForCommit returns the source branch annotations from the subject +func GetSourceRefsForCommit(att attestation.Envelope, commit *models.Commit) ([]string, error) { + subject := GetSubjectForCommit(att, commit) if subject == nil { - return []string{}, fmt.Errorf("statement \n%v\n does not match commit %s", StatementToString(vsaStatement), commit) + return []string{}, fmt.Errorf("statement \n%v\n does not match commit %s", string(att.GetPredicate().GetData()), commit) } annotations := subject.GetAnnotations() sourceRefs, ok := annotations.GetFields()[slsa.SourceRefsAnnotation] @@ -82,78 +82,91 @@ func GetSourceRefsForCommit(vsaStatement *spb.Statement, commit string) ([]strin return stringRefs, nil } -type StatementMatcher func(*spb.Statement) bool - -func MatchesTypeAndCommit(predicateType, commit string) StatementMatcher { - return func(statement *spb.Statement) bool { - if statement.GetPredicateType() != predicateType { - Debugf("statement predicate type (%s) doesn't match %s", statement.GetPredicateType(), predicateType) - return false - } - if !DoesSubjectIncludeCommit(statement, commit) { - Debugf("statement \n%v\n does not match commit %s", StatementToString(statement), commit) - return false - } - return true - } -} +// type StatementMatcher func(*intoto.Statement) bool + +// func MatchesTypeAndCommit(predicateType, commit string) StatementMatcher { +// return func(statement *intoto.Statement) bool { +// if statement.GetPredicateType() != predicateType { +// Debugf("statement predicate type (%s) doesn't match %s", statement.GetPredicateType(), predicateType) +// return false +// } +// if !DoesSubjectIncludeCommit(statement, commit) { +// Debugf("statement \n%v\n does not match commit %s", StatementToString(att.GetStatement()), commit) +// return false +// } +// return true +// } +// } // Reads all the statements that: // 1. Have a valid signature // 2. Have the specified predicate type. // 3. Have a subject that matches the specified git commit. -func (br *BundleReader) ReadStatement(matcher StatementMatcher) (*spb.Statement, error) { - // Read until we get a statement or end of file. - for { - line, err := br.reader.ReadString('\n') - if err != nil { - // Handle end of file gracefully - if !errors.Is(err, io.EOF) { - return nil, err - } - if line == "" { - // Nothing to see here. - break - } - } - if line == "\n" { - // skip empties - continue - } - statement, err := br.convertLineToStatement(line) - if err != nil { - // Ignore errors, the next line could be fine. - Debugf("problem converting line to statement line: '%s', error: %v", line, err) - } - if statement == nil { - // Not sure what this is, just continue +// func (br *BundleReader) ReadStatement(matcher StatementMatcher) (*intoto.Statement, error) { +// // Read until we get a statement or end of file. +// for { +// line, err := br.reader.ReadString('\n') +// if err != nil { +// // Handle end of file gracefully +// if !errors.Is(err, io.EOF) { +// return nil, err +// } +// if line == "" { +// // Nothing to see here. +// break +// } +// } +// if line == "\n" { +// // skip empties +// continue +// } +// statement, err := br.convertLineToStatement(line) +// if err != nil { +// // Ignore errors, the next line could be fine. +// Debugf("problem converting line to statement line: '%s', error: %v", line, err) +// } +// if statement == nil { +// // Not sure what this is, just continue +// continue +// } +// if matcher(statement) { +// return statement, nil +// } +// // If we loop again it's because that line didn't have a matching statement +// } +// return nil, nil +// } + +// func DoesSubjectIncludeCommit(statement *intoto.Statement, commit string) bool { +// return GetSubjectForCommit(statement, commit) != nil +// } + +// Returns the _first_ subject that includes the commit. +// TODO: add support for multiple subjects... +func GetSubjectForCommit(att attestation.Envelope, commit *models.Commit) *intoto.ResourceDescriptor { + var fromSha *intoto.ResourceDescriptor + for _, subject := range att.GetStatement().GetSubjects() { + rd, ok := subject.(*intoto.ResourceDescriptor) + if !ok { continue } - if matcher(statement) { - return statement, nil + val, ok := rd.GetDigest()["gitCommit"] + if ok && val == commit.SHA { + return rd } - // If we loop again it's because that line didn't have a matching statement - } - return nil, nil -} - -func DoesSubjectIncludeCommit(statement *spb.Statement, commit string) bool { - return GetSubjectForCommit(statement, commit) != nil -} -// Returns the _first_ subject that includes the commit. -// TODO: add support for multiple subjects... -func GetSubjectForCommit(statement *spb.Statement, commit string) *spb.ResourceDescriptor { - for _, subject := range statement.GetSubject() { - if subject.GetDigest()["gitCommit"] == commit { - return subject + if val, ok = rd.GetDigest()["sha1"]; ok && val == commit.SHA { + fromSha = rd } } - return nil + + // Prefer the gitCommit version, but if we saw a sha1 of the commit + // sha return the backup resource descriptor. + return fromSha } // Just make this easy for logging... -func StatementToString(stmt *spb.Statement) string { +func StatementToString(stmt *intoto.Statement) string { if stmt == nil { return "" } diff --git a/pkg/attest/statement_test.go b/pkg/attest/statement_test.go index 4a77d68..b119739 100644 --- a/pkg/attest/statement_test.go +++ b/pkg/attest/statement_test.go @@ -8,30 +8,151 @@ import ( "slices" "testing" + "github.com/carabiner-dev/attestation" spb "github.com/in-toto/attestation/go/v1" "google.golang.org/protobuf/types/known/structpb" + + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) -func newStatement(commit string, annotation map[string]any) (*spb.Statement, error) { - var sub []*spb.ResourceDescriptor +// mockEnvelope implements attestation.Envelope for testing. +type mockEnvelope struct { + statement *mockStatement + predicate *mockPredicate +} + +func (m *mockEnvelope) GetStatement() attestation.Statement { return m.statement } +func (m *mockEnvelope) GetPredicate() attestation.Predicate { return m.predicate } +func (m *mockEnvelope) GetSignatures() []attestation.Signature { return nil } +func (m *mockEnvelope) GetCertificate() attestation.Certificate { return nil } +func (m *mockEnvelope) GetVerification() attestation.Verification { return nil } +func (m *mockEnvelope) Verify(...any) error { return nil } + +type mockStatement struct { + subjects []attestation.Subject +} + +func (m *mockStatement) GetSubjects() []attestation.Subject { return m.subjects } +func (m *mockStatement) GetPredicate() attestation.Predicate { return nil } +func (m *mockStatement) GetPredicateType() attestation.PredicateType { return "" } +func (m *mockStatement) GetType() string { return "" } +func (m *mockStatement) GetVerification() attestation.Verification { return nil } + +type mockPredicate struct { + data []byte +} + +func (m *mockPredicate) GetType() attestation.PredicateType { return "" } +func (m *mockPredicate) SetType(attestation.PredicateType) error { return nil } +func (m *mockPredicate) GetParsed() any { return nil } +func (m *mockPredicate) GetData() []byte { return m.data } +func (m *mockPredicate) GetVerification() attestation.Verification { return nil } +func (m *mockPredicate) GetOrigin() attestation.Subject { return nil } +func (m *mockPredicate) SetOrigin(attestation.Subject) {} +func (m *mockPredicate) SetVerification(attestation.Verification) {} + +func newMockEnvelope(commit string, annotation map[string]any) (attestation.Envelope, error) { + var subjects []attestation.Subject if annotation != nil { annotationStruct, err := structpb.NewStruct(annotation) if err != nil { return nil, fmt.Errorf("creating struct from map: %w", err) } - sub = []*spb.ResourceDescriptor{{ + subjects = []attestation.Subject{&spb.ResourceDescriptor{ Digest: map[string]string{"gitCommit": commit}, Annotations: annotationStruct, }} } - statementPb := spb.Statement{ - Type: spb.StatementTypeUri, - Subject: sub, - PredicateType: "test", - Predicate: &structpb.Struct{}, + return &mockEnvelope{ + statement: &mockStatement{subjects: subjects}, + predicate: &mockPredicate{data: []byte("{}")}, + }, nil +} + +func newMockEnvelopeWithSubjects(subjects []attestation.Subject) attestation.Envelope { + return &mockEnvelope{ + statement: &mockStatement{subjects: subjects}, + predicate: &mockPredicate{data: []byte("{}")}, + } +} + +func TestGetSubjectForCommit(t *testing.T) { + commitSHA := "abc123" + commit := &models.Commit{SHA: commitSHA} + + tests := []struct { + name string + subjects []attestation.Subject + wantNil bool + }{ + { + name: "match by gitCommit", + subjects: []attestation.Subject{ + &spb.ResourceDescriptor{ + Digest: map[string]string{"gitCommit": commitSHA}, + }, + }, + }, + { + name: "match by sha1 fallback", + subjects: []attestation.Subject{ + &spb.ResourceDescriptor{ + Digest: map[string]string{"sha1": commitSHA}, + }, + }, + }, + { + name: "gitCommit preferred over sha1", + subjects: []attestation.Subject{ + &spb.ResourceDescriptor{ + Digest: map[string]string{"sha1": commitSHA}, + Name: "sha1-subject", + }, + &spb.ResourceDescriptor{ + Digest: map[string]string{"gitCommit": commitSHA}, + Name: "gitCommit-subject", + }, + }, + }, + { + name: "no match", + subjects: []attestation.Subject{ + &spb.ResourceDescriptor{ + Digest: map[string]string{"gitCommit": "other"}, + }, + }, + wantNil: true, + }, + { + name: "empty subjects", + subjects: nil, + wantNil: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := newMockEnvelopeWithSubjects(tt.subjects) + got := GetSubjectForCommit(env, commit) + + if tt.wantNil { + if got != nil { + t.Errorf("expected nil, got %v", got) + } + return + } + if got == nil { + t.Fatal("expected non-nil subject") + } + + // For the "preferred" test, verify we got the gitCommit subject + if tt.name == "gitCommit preferred over sha1" { + if got.GetName() != "gitCommit-subject" { + t.Errorf("expected gitCommit-subject, got %s", got.GetName()) + } + } + }) } - return &statementPb, nil } func stringToAnyArray(valArray []string) []any { @@ -46,7 +167,6 @@ func TestGetSourceRefsForCommit(t *testing.T) { tests := []struct { name string annotationName string - stmt *spb.Statement refs []string expectedRefs []string expectErr bool @@ -89,12 +209,13 @@ func TestGetSourceRefsForCommit(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - stmt, err := newStatement("abc123", map[string]any{tt.annotationName: stringToAnyArray(tt.refs)}) + env, err := newMockEnvelope("abc123", map[string]any{tt.annotationName: stringToAnyArray(tt.refs)}) if err != nil { - t.Fatalf("error creating statement: %v", err) + t.Fatalf("error creating envelope: %v", err) } - gotRefs, err := GetSourceRefsForCommit(stmt, "abc123") + commit := &models.Commit{SHA: "abc123"} + gotRefs, err := GetSourceRefsForCommit(env, commit) if err != nil && !tt.expectErr { t.Errorf("did not expect error, got %v", err) diff --git a/pkg/attest/vsa.go b/pkg/attest/vsa.go index 59e60aa..bdff404 100644 --- a/pkg/attest/vsa.go +++ b/pkg/attest/vsa.go @@ -4,10 +4,7 @@ package attest import ( - "bufio" - "context" "fmt" - "strings" vpb "github.com/in-toto/attestation/go/predicates/vsa/v1" spb "github.com/in-toto/attestation/go/v1" @@ -15,8 +12,8 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) const ( @@ -24,12 +21,14 @@ const ( VsaVerifierId = "https://github.com/slsa-framework/source-actions" ) -func CreateUnsignedSourceVsa(repoUri, ref, commit string, verifiedLevels slsa.SourceVerifiedLevels, policy string) (string, error) { - return createUnsignedSourceVsaAllParams(repoUri, ref, commit, verifiedLevels, policy, VsaVerifierId, "PASSED") +func CreateUnsignedSourceVsa(branch *models.Branch, commit *models.Commit, verifiedLevels slsa.SourceVerifiedLevels, policy string) (string, error) { + return createUnsignedSourceVsaAllParams(branch, commit, verifiedLevels, policy, VsaVerifierId, "PASSED") } -func createUnsignedSourceVsaAllParams(repoUri, ref, commit string, verifiedLevels slsa.SourceVerifiedLevels, policy, verifiedId, result string) (string, error) { - resourceUri := fmt.Sprintf("git+%s", repoUri) +// createUnsignedSourceVsaAllParams generates a VSA +func createUnsignedSourceVsaAllParams(branch *models.Branch, commit *models.Commit, verifiedLevels slsa.SourceVerifiedLevels, policy, verifiedId, result string) (string, error) { + // The attestation records a VCS locator + resourceUri := fmt.Sprintf("git+%s", branch.Repository.GetHttpURL()) vsaPred := &vpb.VerificationSummary{ Verifier: &vpb.VerificationSummary_Verifier{ Id: verifiedId, @@ -46,13 +45,13 @@ func createUnsignedSourceVsaAllParams(repoUri, ref, commit string, verifiedLevel return "", err } - branchAnnotation := map[string]any{slsa.SourceRefsAnnotation: []any{ref}} + branchAnnotation := map[string]any{slsa.SourceRefsAnnotation: []any{branch.FullRef()}} annotationStruct, err := structpb.NewStruct(branchAnnotation) if err != nil { return "", fmt.Errorf("creating struct from map: %w", err) } sub := []*spb.ResourceDescriptor{{ - Digest: map[string]string{"gitCommit": commit}, + Digest: map[string]string{"gitCommit": commit.SHA}, Annotations: annotationStruct, }} @@ -75,105 +74,3 @@ func createUnsignedSourceVsaAllParams(repoUri, ref, commit string, verifiedLevel } return string(statement), nil } - -// Gets a VSA for the commit from git notes. -func GetVsa(ctx context.Context, ghc *ghcontrol.GitHubConnection, verifier Verifier, commit, ref string) (*spb.Statement, *vpb.VerificationSummary, error) { - notes, err := ghc.GetNotesForCommit(ctx, commit) - if err != nil { - return nil, nil, fmt.Errorf("fetching commit note: %w", err) - } - - if notes == "" { - Debugf("didn't find notes for commit %s", commit) - return nil, nil, nil - } - - return getVsaFromReader(NewBundleReader(bufio.NewReader(strings.NewReader(notes)), verifier), commit, ref, ghc.GetRepoUri()) -} - -func getVsaPred(statement *spb.Statement) (*vpb.VerificationSummary, error) { - predJson, err := protojson.Marshal(statement.GetPredicate()) - if err != nil { - return nil, err - } - - var predStruct vpb.VerificationSummary - // Using regular json.Unmarshal because this is just a regular struct. - err = protojson.Unmarshal(predJson, &predStruct) - if err != nil { - return nil, err - } - return &predStruct, nil -} - -func MatchesTypeCommitAndRef(predicateType, commit, targetRef string) StatementMatcher { - return func(statement *spb.Statement) bool { - if statement.GetPredicateType() != predicateType { - Debugf("statement predicate type (%s) doesn't match %s", statement.GetPredicateType(), predicateType) - return false - } - refs, err := GetSourceRefsForCommit(statement, commit) - if err != nil { - Debugf("statement \n%v\n does not match commit %s: %v", StatementToString(statement), commit, err) - return false - } - for _, ref := range refs { - if targetRef == ghcontrol.AnyReference || ref == targetRef { - Debugf("statement \n%v\n matches commit '%s' on ref '%s'", StatementToString(statement), commit, targetRef) - return true - } - } - Debugf("source_refs (%v) in VSA does not contain %s", refs, targetRef) - return false - } -} - -func getVsaFromReader(reader *BundleReader, commit, ref, repoUri string) (*spb.Statement, *vpb.VerificationSummary, error) { - // We want to return the first valid VSA. - // We should follow instructions from - // https://slsa.dev/spec/draft/verifying-source#how-to-verify-slsa-a-source-revision - - for { - stmt, err := reader.ReadStatement(MatchesTypeCommitAndRef(VsaPredicateType, commit, ref)) - if err != nil { - // Ignore errors, we want to check all the lines. - Debugf("error while processing line: %v", err) - continue - } - - if stmt == nil { - // No statements left. - break - } - - vsaPred, err := getVsaPred(stmt) - if err != nil { - return nil, nil, err - } - - // Is it the verifier ID we expect? - if vsaPred.GetVerifier().GetId() != VsaVerifierId { - Debugf("we do not accept Verifier.Id %s", vsaPred.GetVerifier().GetId()) - continue - } - - // Does repo match? - // remove git+http:// from resourceUri - cleanResourceUri := strings.TrimPrefix(vsaPred.GetResourceUri(), "git+") - if cleanResourceUri != repoUri { - Debugf("ResourceUri is %s but we want %s", cleanResourceUri, repoUri) - continue - } - - // Is the result PASSED? - if vsaPred.GetVerificationResult() != "PASSED" { - Debugf("verificationResult is %s but must be PASSED", vsaPred.GetVerificationResult()) - continue - } - - return stmt, vsaPred, nil - } - - Debugf("didn't find commit %s for ref %s", commit, ref) - return nil, nil, nil -} diff --git a/pkg/attest/vsa_test.go b/pkg/attest/vsa_test.go index be110f3..5695ab1 100644 --- a/pkg/attest/vsa_test.go +++ b/pkg/attest/vsa_test.go @@ -3,104 +3,130 @@ package attest -// TODO: This test package uses some functions that live in provenance_test.go -// that seems ugly and maybe we should fix it. - import ( - "slices" "testing" - "github.com/google/go-github/v69/github" + spb "github.com/in-toto/attestation/go/v1" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" "github.com/slsa-framework/source-tool/pkg/slsa" - "github.com/slsa-framework/source-tool/pkg/testsupport" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) -func createTestVsa(t *testing.T, repoUri, ref, commit string, verifiedLevels slsa.SourceVerifiedLevels) string { - vsa, err := CreateUnsignedSourceVsa(repoUri, ref, commit, verifiedLevels, "test-policy") - if err != nil { - t.Fatalf("failure creating test vsa: %v", err) +//nolint:unparam +func newTestBranch(hostname, path, branchName string) *models.Branch { + return &models.Branch{ + Name: branchName, + Repository: &models.Repository{ + Hostname: hostname, + Path: path, + }, } - return vsa } -func createTestVsaWithIdAndResult(t *testing.T, repoUri, ref, commit, id, result string, verifiedLevels slsa.SourceVerifiedLevels) string { - vsa, err := createUnsignedSourceVsaAllParams(repoUri, ref, commit, verifiedLevels, "test-policy", id, result) - if err != nil { - t.Fatalf("failure creating test vsa: %v", err) - } - return vsa +func newTestCommit(sha string) *models.Commit { + return &models.Commit{SHA: sha} } -func TestReadVsaSuccess(t *testing.T) { - testVsa := createTestVsa(t, "https://github.com/owner/repo", "refs/some/ref", "de9395302d14b24c0a42685cf27315d93c88ff79", slsa.SourceVerifiedLevels{"TEST_LEVEL"}) - ghc := newTestGhConnection("owner", "repo", "branch", - // We just need _some_ rulesets response, we don't care what. - newTagHygieneRulesetsResponse(123, github.RulesetTargetTag, - github.RulesetEnforcementActive, rulesetOldTime), - newNotesContent(testVsa)) - verifier := testsupport.NewMockVerifier() - - readStmt, readPred, err := GetVsa(t.Context(), ghc, verifier, "de9395302d14b24c0a42685cf27315d93c88ff79", "refs/some/ref") - if err != nil { - t.Fatalf("error finding vsa: %v", err) - } - if readStmt == nil || readPred == nil { - t.Errorf("could not find vsa") - } +func TestCreateUnsignedSourceVsa(t *testing.T) { + branch := newTestBranch("github.com", "owner/repo", "main") + commit := newTestCommit("de9395302d14b24c0a42685cf27315d93c88ff79") - if !slices.Contains(readPred.GetVerifiedLevels(), "TEST_LEVEL") { - t.Errorf("expected VSA to contain TEST_LEVEL, but it just contains %v", readPred.GetVerifiedLevels()) - } + vsaJSON, err := CreateUnsignedSourceVsa(branch, commit, slsa.SourceVerifiedLevels{"TEST_LEVEL"}, "test-policy") + require.NoError(t, err) + require.NotEmpty(t, vsaJSON) + + // Parse the statement back + var stmt spb.Statement + err = protojson.Unmarshal([]byte(vsaJSON), &stmt) + require.NoError(t, err) + + require.Equal(t, VsaPredicateType, stmt.GetPredicateType()) + require.Len(t, stmt.GetSubject(), 1) + require.Equal(t, commit.SHA, stmt.GetSubject()[0].GetDigest()["gitCommit"]) + + // Verify the predicate contains the verifier ID and result + predFields := stmt.GetPredicate().GetFields() + require.Equal(t, VsaVerifierId, predFields["verifier"].GetStructValue().GetFields()["id"].GetStringValue()) + require.Equal(t, "PASSED", predFields["verificationResult"].GetStringValue()) + require.Equal(t, "test-policy", predFields["policy"].GetStructValue().GetFields()["uri"].GetStringValue()) + + // Check verified levels + levels := predFields["verifiedLevels"].GetListValue().GetValues() + require.Len(t, levels, 1) + require.Equal(t, "TEST_LEVEL", levels[0].GetStringValue()) + + // Check resource URI includes the repo URL + require.Contains(t, predFields["resourceUri"].GetStringValue(), branch.Repository.GetHttpURL()) } -func TestReadVsaInvalidVsas(t *testing.T) { - goodRepo := "https://github.com/org/foo" - goodRef := "refs/heads/main" - goodCommit := "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa" +func TestCreateUnsignedSourceVsaMultipleLevels(t *testing.T) { + branch := newTestBranch("github.com", "owner/repo", "main") + commit := newTestCommit("73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") + + vsaJSON, err := CreateUnsignedSourceVsa(branch, commit, slsa.SourceVerifiedLevels{"LEVEL_1", "LEVEL_2", "LEVEL_3"}, "test-policy") + require.NoError(t, err) + + var stmt spb.Statement + err = protojson.Unmarshal([]byte(vsaJSON), &stmt) + require.NoError(t, err) + + levels := stmt.GetPredicate().GetFields()["verifiedLevels"].GetListValue().GetValues() + require.Len(t, levels, 3) +} + +func TestCreateUnsignedSourceVsaAllParams(t *testing.T) { + branch := newTestBranch("github.com", "owner/repo", "main") + commit := newTestCommit("73f0a864c2c9af12e03dae433a6ff5f5e719d7aa") - // We want to make sure invalid VSAs aren't returned. tests := []struct { - name string - vsa string + name string + verifierID string + result string }{ { - name: "wrong commit", - vsa: createTestVsa(t, goodRepo, goodRef, "def456", slsa.SourceVerifiedLevels{}), - }, - { - name: "wrong repo uri", - vsa: createTestVsa(t, "https://github.com/foo/bar", goodRef, goodCommit, slsa.SourceVerifiedLevels{}), - }, - { - name: "wrong ref", - vsa: createTestVsa(t, goodRepo, "refs/heads/bad", goodCommit, slsa.SourceVerifiedLevels{}), - }, - { - name: "wrong verifier ID", - vsa: createTestVsaWithIdAndResult(t, goodRepo, goodRef, goodCommit, "bad id", "PASSED", slsa.SourceVerifiedLevels{}), + name: "custom verifier and PASSED", + verifierID: "custom-verifier", + result: "PASSED", }, { - name: "bad result", - vsa: createTestVsaWithIdAndResult(t, goodRepo, goodRef, goodCommit, VsaVerifierId, "FAILED", slsa.SourceVerifiedLevels{}), + name: "default verifier and FAILED", + verifierID: VsaVerifierId, + result: "FAILED", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ghc := newTestGhConnection("org", "foo", "main", - // We just need _some_ rulesets response, we don't care what. - newTagHygieneRulesetsResponse(123, github.RulesetTargetTag, - github.RulesetEnforcementActive, rulesetOldTime), - newNotesContent(tt.vsa)) - verifier := testsupport.NewMockVerifier() - - _, readPred, err := GetVsa(t.Context(), ghc, verifier, "73f0a864c2c9af12e03dae433a6ff5f5e719d7aa", "refs/heads/main") - if err != nil { - t.Fatalf("error finding vsa: %v", err) - } - if readPred != nil { - t.Errorf("should not have gotten vsa: %+v", readPred) - } + vsaJSON, err := createUnsignedSourceVsaAllParams(branch, commit, slsa.SourceVerifiedLevels{}, "test-policy", tt.verifierID, tt.result) + require.NoError(t, err) + + var stmt spb.Statement + err = protojson.Unmarshal([]byte(vsaJSON), &stmt) + require.NoError(t, err) + + predFields := stmt.GetPredicate().GetFields() + require.Equal(t, tt.verifierID, predFields["verifier"].GetStructValue().GetFields()["id"].GetStringValue()) + require.Equal(t, tt.result, predFields["verificationResult"].GetStringValue()) }) } } + +func TestCreateUnsignedSourceVsaSubjectAnnotation(t *testing.T) { + branch := newTestBranch("github.com", "owner/repo", "main") + commit := newTestCommit("abc123") + + vsaJSON, err := CreateUnsignedSourceVsa(branch, commit, slsa.SourceVerifiedLevels{}, "test-policy") + require.NoError(t, err) + + var stmt spb.Statement + err = protojson.Unmarshal([]byte(vsaJSON), &stmt) + require.NoError(t, err) + + // The subject should have a source_refs annotation with the branch ref + annotations := stmt.GetSubject()[0].GetAnnotations() + require.NotNil(t, annotations) + refs := annotations.GetFields()[slsa.SourceRefsAnnotation].GetListValue().GetValues() + require.Len(t, refs, 1) + require.Equal(t, branch.FullRef(), refs[0].GetStringValue()) +} diff --git a/pkg/audit/audit.go b/pkg/audit/audit.go index 28ac99d..0fc2f35 100644 --- a/pkg/audit/audit.go +++ b/pkg/audit/audit.go @@ -5,30 +5,47 @@ package audit import ( "context" + "errors" "fmt" "iter" + "os" vpb "github.com/in-toto/attestation/go/predicates/vsa/v1" "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/provenance" + "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type Auditor struct { - ghc *ghcontrol.GitHubConnection - // TODO: This should probably be turned into a pointer. - verifier attest.Verifier - pa *attest.ProvenanceAttestor + attester *attest.Attester + backend models.VcsBackend +} + +type optFn func(*Auditor) error + +func WithAttester(a *attest.Attester) optFn { + return func(au *Auditor) error { + au.attester = a + return nil + } +} + +func WithBackend(b models.VcsBackend) optFn { + return func(au *Auditor) error { + au.backend = b + return nil + } } type AuditCommitResult struct { Commit string VsaPred *vpb.VerificationSummary ProvPred *provenance.SourceProvenancePred - // The previous commit reported by GH. - GhPriorCommit string - GhControlStatus *ghcontrol.GhControlStatus + // The previous commit reported by the VCS backend. + PriorCommit string + ControlStatus *slsa.ControlSet } func (ar *AuditCommitResult) IsGood() bool { @@ -38,7 +55,7 @@ func (ar *AuditCommitResult) IsGood() bool { // Have to have provenance if ar.ProvPred == nil { good = false - } else if ar.ProvPred.GetPrevCommit() != ar.GhPriorCommit { + } else if ar.ProvPred.GetPrevCommit() != ar.PriorCommit { // Commits need to be the same. good = false } @@ -46,53 +63,69 @@ func (ar *AuditCommitResult) IsGood() bool { return good } -func NewAuditor(ghc *ghcontrol.GitHubConnection, pa *attest.ProvenanceAttestor, verifier attest.Verifier) *Auditor { - return &Auditor{ - ghc: ghc, - verifier: verifier, - pa: pa, +func NewAuditor(fn ...optFn) (*Auditor, error) { + a := &Auditor{} + for _, f := range fn { + if err := f(a); err != nil { + return nil, err + } + } + + errs := []error{} + if a.attester == nil { + errs = append(errs, errors.New("auditor has no attester")) + } + if a.backend == nil { + errs = append(errs, errors.New("auditor has no backend")) } + if err := errors.Join(errs...); err != nil { + return nil, err + } + return a, nil } -func (a *Auditor) AuditCommit(ctx context.Context, commit string) (ar *AuditCommitResult, err error) { - ar = &AuditCommitResult{Commit: commit} +func (a *Auditor) AuditCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (ar *AuditCommitResult, err error) { + ar = &AuditCommitResult{Commit: commit.SHA} - _, vsa, err := attest.GetVsa(ctx, a.ghc, a.verifier, commit, a.ghc.GetFullRef()) + _, vsa, err := a.attester.GetRevisionVSA(ctx, branch, commit) if err != nil { return nil, fmt.Errorf("getting vsa for revision %s: %w", commit, err) } ar.VsaPred = vsa - _, prov, err := a.pa.GetProvenance(ctx, commit, a.ghc.GetFullRef()) + prov, err := a.attester.GetRevisionProvenance(ctx, branch, commit) if err != nil { return nil, fmt.Errorf("getting prov for revision %s: %w", commit, err) } ar.ProvPred = prov - ghPrior, err := a.ghc.GetPriorCommit(ctx, commit) + prior, err := a.backend.GetPreviousCommit(ctx, branch, commit) if err != nil { return nil, fmt.Errorf("could not get prior commit for revision %s: %w", commit, err) } - ar.GhPriorCommit = ghPrior + ar.PriorCommit = prior.SHA - var controlStatus *ghcontrol.GhControlStatus if prov == nil { - // If there's no provenance, let's check the controls to see how they're looking. + // If there's no provenance, check the controls to see how they're looking. // It could be that provenance generation failed, but the controls were still // in place. - controlStatus, err = a.ghc.GetBranchControlsAtCommit(ctx, commit, a.ghc.GetFullRef()) + controlStatus, err := a.backend.GetBranchControlsAtCommit(ctx, branch, commit) if err != nil { - // Let's still return ar so they can continue if they want. - return ar, fmt.Errorf("could not get controls for %s on %s: %w", commit, a.ghc.GetFullRef(), err) + // Still return ar so callers can continue if they want. + return ar, fmt.Errorf("could not get controls for %s on %s: %w", commit.SHA, branch.FullRef(), err) } + ar.ControlStatus = controlStatus } - ar.GhControlStatus = controlStatus return ar, nil } -func (a *Auditor) AuditBranch(ctx context.Context, branch string) iter.Seq2[*AuditCommitResult, error] { - latestCommit, err := a.ghc.GetLatestCommit(ctx, branch) +func (a *Auditor) AuditBranch(ctx context.Context, branch *models.Branch) iter.Seq2[*AuditCommitResult, error] { + latestCommit, err := a.backend.GetLatestCommit(ctx, branch.Repository, branch) + if err != nil { + fmt.Fprintf(os.Stderr, "error fetching latest commit: %v", err) + return nil + } return func(yield func(*AuditCommitResult, error) bool) { if err != nil { @@ -100,12 +133,12 @@ func (a *Auditor) AuditBranch(ctx context.Context, branch string) iter.Seq2[*Aud return } nextCommit := latestCommit - for ok := true; ok; ok = (nextCommit != "") { - ar, err := a.AuditCommit(ctx, nextCommit) + for ok := true; ok; ok = (nextCommit.SHA != "") { + ar, err := a.AuditCommit(ctx, branch, nextCommit) if !yield(ar, err) { return } - nextCommit = ar.GhPriorCommit + nextCommit = &models.Commit{SHA: ar.PriorCommit} } } } diff --git a/pkg/auth/authenticator.go b/pkg/auth/authenticator.go index 3d50684..f2888e2 100644 --- a/pkg/auth/authenticator.go +++ b/pkg/auth/authenticator.go @@ -54,7 +54,7 @@ type DeviceCodeResponse struct { // TokenResponse is the data structure returned when exchanging tokens type TokenResponse struct { - AccessToken string `json:"access_token"` //nolint:gosec // G117 This is the github struct + AccessToken string `json:"access_token"` TokenType string `json:"token_type"` Scope string `json:"scope"` Error string `json:"error"` diff --git a/pkg/auth/implementation.go b/pkg/auth/implementation.go index faae556..2c6d309 100644 --- a/pkg/auth/implementation.go +++ b/pkg/auth/implementation.go @@ -100,7 +100,7 @@ func (di *defaultImplementation) checkTokenStatus(ctx context.Context, deviceCod // device authorization webpage. func (di *defaultImplementation) openBrowser(authURL string) error { var cmd string - var args []string + var args []string //nolint:prealloc switch runtime.GOOS { case "windows": diff --git a/pkg/ghcontrol/checklevel.go b/pkg/ghcontrol/checklevel.go index c718f0d..2b62235 100644 --- a/pkg/ghcontrol/checklevel.go +++ b/pkg/ghcontrol/checklevel.go @@ -12,9 +12,7 @@ import ( "time" "github.com/google/go-github/v69/github" - "google.golang.org/protobuf/types/known/timestamppb" - "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) @@ -81,13 +79,13 @@ type GhControlStatus struct { ActivityType string // The controls that are enabled according to the GitHub API. // May not include other controls like if we have provenance. - Controls slsa.Controls + Controls *slsa.ControlSet } // Adds the control, but only if it existed when the commit was pushed. -func (cs *GhControlStatus) AddControl(newControls ...*provenance.Control) { +func (cs *GhControlStatus) AddControl(newControls ...*slsa.Control) { for _, newControl := range newControls { - if newControl != nil && cs.CommitPushTime.After(newControl.GetSince().AsTime()) { + if cs.CommitPushTime.After(*newControl.GetSince()) { cs.Controls.AddControl(newControl) } } @@ -101,7 +99,7 @@ func (ghc *GitHubConnection) ruleMeetsRequiresReview(rule *github.PullRequestBra } // Computes the continuity control returning nil if it's not enabled. -func (ghc *GitHubConnection) computeContinuityControl(ctx context.Context, rules *github.BranchRules) (*provenance.Control, error) { +func (ghc *GitHubConnection) computeRepoContinuityControl(ctx context.Context, rules *github.BranchRules) (*slsa.Control, error) { oldestDeletion, err := ghc.getOldestActiveRule(ctx, rules.Deletion) if err != nil { return nil, fmt.Errorf("looking for oldest branch delete protection: %w", err) @@ -122,9 +120,11 @@ func (ghc *GitHubConnection) computeContinuityControl(ctx context.Context, rules newestRule = oldestNoFf } - return &provenance.Control{Name: string(slsa.ContinuityEnforced), Since: timestamppb.New(newestRule.UpdatedAt.Time)}, nil + return &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, Since: &newestRule.UpdatedAt.Time}, nil } +// enforcesTagHygiene checks a repository ruleset to understand if it is +// configured to protect the branch func enforcesTagHygiene(ruleset *github.RepositoryRuleset) bool { if ruleset.Rules != nil && ruleset.Rules.Update != nil && @@ -138,7 +138,8 @@ func enforcesTagHygiene(ruleset *github.RepositoryRuleset) bool { return false } -func (ghc *GitHubConnection) computeTagHygieneControl(ctx context.Context, allRulesets []*github.RepositoryRuleset) (*provenance.Control, error) { +// computeTagHygieneControl +func (ghc *GitHubConnection) computeTagHygieneControl(ctx context.Context, allRulesets []*github.RepositoryRuleset) (*slsa.Control, error) { var validRuleset *github.RepositoryRuleset for _, ruleset := range allRulesets { if *ruleset.Target != github.RulesetTargetTag { @@ -168,11 +169,14 @@ func (ghc *GitHubConnection) computeTagHygieneControl(ctx context.Context, allRu return nil, nil } - return &provenance.Control{Name: slsa.TagHygiene.String(), Since: timestamppb.New(validRuleset.UpdatedAt.Time)}, nil + return &slsa.Control{ + Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, + Since: &validRuleset.UpdatedAt.Time, + }, nil } // Computes the review control returning nil if it's not enabled. -func (ghc *GitHubConnection) computeReviewControl(ctx context.Context, rules []*github.PullRequestBranchRule) (*provenance.Control, error) { +func (ghc *GitHubConnection) computeReviewControl(ctx context.Context, rules []*github.PullRequestBranchRule) (*slsa.Control, error) { var oldestActive *github.RepositoryRuleset for _, rule := range rules { if ghc.ruleMeetsRequiresReview(rule) { @@ -189,16 +193,19 @@ func (ghc *GitHubConnection) computeReviewControl(ctx context.Context, rules []* } if oldestActive != nil { - return &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: timestamppb.New(oldestActive.UpdatedAt.Time)}, nil + return &slsa.Control{ + Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, + Since: &oldestActive.UpdatedAt.Time, + }, nil } return nil, nil } -func (ghc *GitHubConnection) computeRequiredChecks(ctx context.Context, ghCheckRules []*github.RequiredStatusChecksBranchRule) ([]*provenance.Control, error) { +func (ghc *GitHubConnection) computeRequiredChecks(ctx context.Context, ghCheckRules []*github.RequiredStatusChecksBranchRule) ([]*slsa.Control, error) { // Only return the checks we're happy about. // For now that's only stuff from the GitHub Actions app. - requiredChecks := []*provenance.Control{} + requiredChecks := []*slsa.Control{} for _, ghCheckRule := range ghCheckRules { ruleset, _, err := ghc.Client().Repositories.GetRuleset(ctx, ghc.Owner(), ghc.Repo(), ghCheckRule.RulesetID, false) if err != nil { @@ -214,9 +221,9 @@ func (ghc *GitHubConnection) computeRequiredChecks(ctx context.Context, ghCheckR // Ignore untrusted integration id. continue } - requiredChecks = append(requiredChecks, &provenance.Control{ - Name: CheckNameToControlName(check.Context).String(), - Since: timestamppb.New(ruleset.UpdatedAt.Time), + requiredChecks = append(requiredChecks, &slsa.Control{ + Name: CheckNameToControlName(check.Context), + Since: &ruleset.UpdatedAt.Time, }) } } @@ -341,13 +348,13 @@ func (ghc *GitHubConnection) getOldestActiveRule(ctx context.Context, rules []*g // GetBranchControls returns a list of the controls enabled at present for a branch. // This function does not take into account a commit date, it just returns those controls // that are active when called. -func (ghc *GitHubConnection) GetBranchControls(ctx context.Context, ref string) (*slsa.Controls, error) { +func (ghc *GitHubConnection) GetBranchControls(ctx context.Context, ref string) (*slsa.ControlSet, error) { branch := GetBranchFromRef(ref) if branch == "" { return nil, fmt.Errorf("ref %s is not a branch", ref) } - controls := &slsa.Controls{} + controls := &slsa.ControlSet{} // Do the branch specific stuff. branchRules, _, err := ghc.Client().Repositories.GetRulesForBranch(ctx, ghc.Owner(), ghc.Repo(), branch) @@ -356,7 +363,7 @@ func (ghc *GitHubConnection) GetBranchControls(ctx context.Context, ref string) } // Compute the controls enforced. - continuityControl, err := ghc.computeContinuityControl(ctx, branchRules) + continuityControl, err := ghc.computeRepoContinuityControl(ctx, branchRules) if err != nil { return nil, fmt.Errorf("could not populate ContinuityControl: %w", err) } @@ -402,7 +409,7 @@ func (ghc *GitHubConnection) GetBranchControlsAtCommit(ctx context.Context, comm CommitPushTime: activity.Timestamp, ActivityType: activity.ActivityType, ActorLogin: activity.Actor.Login, - Controls: slsa.Controls{}, + Controls: &slsa.ControlSet{}, } activeControls, err := ghc.GetBranchControls(ctx, ref) @@ -412,7 +419,7 @@ func (ghc *GitHubConnection) GetBranchControlsAtCommit(ctx context.Context, comm // Add the controls to the control status object. This will // discard any that were not active when the commit merged. - for _, c := range *activeControls { + for _, c := range activeControls.Controls { controlStatus.AddControl(c) } @@ -422,7 +429,7 @@ func (ghc *GitHubConnection) GetBranchControlsAtCommit(ctx context.Context, comm func (ghc *GitHubConnection) GetTagControls(ctx context.Context, commit, ref string) (*GhControlStatus, error) { controlStatus := GhControlStatus{ CommitPushTime: time.Now(), - Controls: slsa.Controls{}, + Controls: &slsa.ControlSet{}, } allRulesets, _, err := ghc.Client().Repositories.GetAllRulesets(ctx, ghc.Owner(), ghc.Repo(), true) diff --git a/pkg/ghcontrol/checklevel_test.go b/pkg/ghcontrol/checklevel_test.go index 2e0f8a3..44c7fbe 100644 --- a/pkg/ghcontrol/checklevel_test.go +++ b/pkg/ghcontrol/checklevel_test.go @@ -241,17 +241,17 @@ func TestBuiltinBranchControls(t *testing.T) { { branchRules: createContinuityBranchRules(), rulesetRules: rulesForBranchContinuity(), - expectedControl: slsa.ContinuityEnforced, + expectedControl: slsa.SLSA_SOURCE_SCS_CONTINUITY, }, { branchRules: createReviewBranchRules(), rulesetRules: rulesForReviewEnforced(), - expectedControl: slsa.ReviewEnforced, + expectedControl: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, }, { branchRules: createTagHygieneRules(), rulesetRules: rulesForTagHygiene(), - expectedControl: slsa.TagHygiene, + expectedControl: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, }, } for _, tt := range tests { @@ -269,7 +269,7 @@ func TestBuiltinBranchControls(t *testing.T) { control := controlStatus.Controls.GetControl(tt.expectedControl) if control == nil { t.Fatalf("expected controls to contain %v, got %+v", tt.expectedControl, controlStatus.Controls) - } else if !control.GetSince().AsTime().Equal(priorTime) { + } else if !control.GetSince().Equal(priorTime) { t.Fatalf("expected control.Since %v, got %v", priorTime, control.GetSince()) } }) @@ -285,17 +285,17 @@ func TestBuiltinBranchControlsEnabledLater(t *testing.T) { { branchRules: createContinuityBranchRules(), rulesetRules: rulesForBranchContinuity(), - name: slsa.ContinuityEnforced, + name: slsa.SLSA_SOURCE_SCS_CONTINUITY, }, { branchRules: createReviewBranchRules(), rulesetRules: rulesForReviewEnforced(), - name: slsa.ReviewEnforced, + name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, }, { branchRules: createTagHygieneRules(), rulesetRules: rulesForTagHygiene(), - name: slsa.TagHygiene, + name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, }, } for _, tt := range tests { @@ -310,7 +310,7 @@ func TestBuiltinBranchControlsEnabledLater(t *testing.T) { t.Fatalf("Error getting branch controls: %v", err) } - if len(controlStatus.Controls) != 0 { + if len(controlStatus.Controls.Controls) != 0 { t.Errorf("execpted no controls, got: %+v", controlStatus.Controls) } }) @@ -350,10 +350,10 @@ func TestGetBranchControlsRequiredChecks(t *testing.T) { t.Fatalf("Error getting branch controls: %v", err) } - controlNames := make([]slsa.ControlName, 0, len(controlStatus.Controls)) - for _, control := range controlStatus.Controls { - controlNames = append(controlNames, slsa.ControlName(control.GetName())) - if !control.GetSince().AsTime().Equal(priorTime) { + controlNames := make([]slsa.ControlName, 0, len(controlStatus.Controls.Controls)) + for _, control := range controlStatus.Controls.Controls { + controlNames = append(controlNames, control.GetName()) + if !control.GetSince().Equal(priorTime) { t.Errorf("Expected control.Since %v, got %v", priorTime, control.GetSince()) } } diff --git a/pkg/ghcontrol/connection.go b/pkg/ghcontrol/connection.go index 7a19b84..8d66dba 100644 --- a/pkg/ghcontrol/connection.go +++ b/pkg/ghcontrol/connection.go @@ -10,6 +10,8 @@ import ( "github.com/google/go-github/v69/github" "github.com/hashicorp/go-retryablehttp" + + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) const tokenEnvVar = "GITHUB_TOKEN" //nolint:gosec // These are not credentials @@ -117,3 +119,33 @@ func (ghc *GitHubConnection) GetDefaultBranch(ctx context.Context) (string, erro return repo.GetDefaultBranch(), nil } + +func (ghc *GitHubConnection) GetTagCommit(ctx context.Context, tagName string) (*models.Commit, error) { + ref, _, err := ghc.Client().Git.GetRef(ctx, ghc.owner, ghc.repo, "tags/"+tagName) + if err != nil { + return nil, fmt.Errorf("fetching ref %s: %w", ghc.ref, err) + } + + obj := ref.GetObject() + + // If the ref points directly to a commit, return it + if obj.GetType() == "commit" { + return &models.Commit{ + SHA: obj.GetSHA(), + }, nil + } + + // For annotated tags, the ref points to a tag object. + // Dereference it to get the commit. + tag, _, err := ghc.Client().Git.GetTag(ctx, ghc.owner, ghc.repo, obj.GetSHA()) + if err != nil { + return nil, fmt.Errorf("fetching tag object %s: %w", obj.GetSHA(), err) + } + + return &models.Commit{ + SHA: tag.GetObject().GetSHA(), + Author: tag.GetTagger().GetLogin(), + Time: tag.GetTagger().Date.GetTime(), + Message: tag.GetMessage(), + }, nil +} diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index bb283a8..584a915 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -27,7 +27,6 @@ import ( "github.com/slsa-framework/source-tool/pkg/ghcontrol" "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" - "github.com/slsa-framework/source-tool/pkg/sourcetool/backends/attestation/notes" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) @@ -52,7 +51,7 @@ func createDefaultBranchPolicy(branch *models.Branch) *ProtectedBranch { return &ProtectedBranch{ Name: branch.Name, Since: timestamppb.Now(), - TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), + TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel0), RequireReview: false, } } @@ -258,8 +257,8 @@ func (pe *PolicyEvaluator) CreateLocalPolicy(ctx context.Context, repo *models.R // Unless there is previous provenance metadata, then we can compute // a higher level if provPred != nil { - eligibleLevel = ComputeEligibleSlsaLevel(provPred.GetControls()) - eligibleSince, err = ComputeEligibleSince(provPred.GetControls(), eligibleLevel) + eligibleLevel = ComputeEligibleSlsaLevel(slsa.NewControlSetFromProvanenaceControls(provPred.GetControls())) + eligibleSince, err = ComputeEligibleSince(slsa.NewControlSetFromProvanenaceControls(provPred.GetControls()), eligibleLevel) if err != nil { return "", fmt.Errorf("could not compute eligible since: %w", err) } @@ -278,11 +277,11 @@ func (pe *PolicyEvaluator) CreateLocalPolicy(ctx context.Context, repo *models.R } // If the controls returned - controls := slsa.Controls(provPred.GetControls()) - tagHygiene := controls.GetControl(slsa.TagHygiene) + controls := slsa.NewControlSetFromProvanenaceControls(provPred.GetControls()) + tagHygiene := controls.GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS) if tagHygiene != nil { p.ProtectedTag = &ProtectedTag{ - Since: tagHygiene.GetSince(), + Since: timestamppb.New(*tagHygiene.GetSince()), TagHygiene: true, } } @@ -302,14 +301,17 @@ func (pe *PolicyEvaluator) CreateLocalPolicy(ctx context.Context, repo *models.R return path, nil } -func computeEligibleForLevel(controls slsa.Controls, level slsa.SlsaSourceLevel) bool { +func computeEligibleForLevel(controls *slsa.ControlSet, level slsa.SlsaSourceLevel) bool { requiredControls := slsa.GetRequiredControlsForLevel(level) return controls.AreControlsAvailable(requiredControls) } // Computes the eligible SLSA level, and when they started being eligible for it, // if only they had a policy. Also returns a rationale for why it's eligible for this level. -func ComputeEligibleSlsaLevel(controls slsa.Controls) slsa.SlsaSourceLevel { +func ComputeEligibleSlsaLevel(controls *slsa.ControlSet) slsa.SlsaSourceLevel { + if controls == nil { + return slsa.SlsaSourceLevel1 + } // Go from highest to lowest. for _, level := range []slsa.SlsaSourceLevel{ slsa.SlsaSourceLevel4, slsa.SlsaSourceLevel3, slsa.SlsaSourceLevel2, @@ -335,27 +337,40 @@ func laterTime(time1, time2 time.Time) time.Time { } // Computes the time since these controls have been eligible for the level, nil if not eligible. -func ComputeEligibleSince(controls slsa.Controls, level slsa.SlsaSourceLevel) (*time.Time, error) { +func ComputeEligibleSince(controls *slsa.ControlSet, level slsa.SlsaSourceLevel) (*time.Time, error) { + // Get the required controls for the taget SLSA level requiredControls := slsa.GetRequiredControlsForLevel(level) var newestTime time.Time + // Range the controls and get the latest time. This is the time when + // the repo started being elegible for the target level for _, rc := range requiredControls { ac := controls.GetControl(rc) if ac == nil { + // TODO(puerco): Here we should report which controls are missing + // to inform the user somehow. return nil, nil } + + // If a control is missing it since date, then ignore it for "ElegibleSince" + // computation. Here we have a problem on how we compute since for provenance. + // See https://github.com/slsa-framework/source-tool/issues/365 + since := ac.GetSince() + if since == nil { + continue + } if newestTime.Equal(time.Time{}) { - newestTime = ac.GetSince().AsTime() + newestTime = *since } else { - newestTime = laterTime(newestTime, ac.GetSince().AsTime()) + newestTime = laterTime(newestTime, *since) } } return &newestTime, nil } // Every function that determines properties to include in the result & VSA implements this interface. -type computePolicyResult func(*ProtectedBranch, *ProtectedTag, slsa.Controls) ([]slsa.ControlName, error) +type computePolicyResult func(*ProtectedBranch, *ProtectedTag, *slsa.ControlSet) ([]slsa.ControlName, error) -func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls slsa.Controls) ([]slsa.ControlName, error) { +func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSet) ([]slsa.ControlName, error) { eligibleLevel := ComputeEligibleSlsaLevel(controls) if !slsa.IsLevelHigherOrEqualTo(eligibleLevel, slsa.SlsaSourceLevel(branchPolicy.GetTargetSlsaSourceLevel())) { @@ -363,7 +378,9 @@ func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls s "policy sets target level %s which requires %v, but branch is only eligible for %s because it only has %v", branchPolicy.GetTargetSlsaSourceLevel(), slsa.GetRequiredControlsForLevel(slsa.SlsaSourceLevel(branchPolicy.GetTargetSlsaSourceLevel())), - eligibleLevel, controls.Names()) + eligibleLevel, + controls.Names(), + ) } // Check to see when this branch became eligible for the current target level. @@ -382,46 +399,53 @@ func computeSlsaLevel(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls s return []slsa.ControlName{slsa.ControlName(branchPolicy.GetTargetSlsaSourceLevel())}, nil } -func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls slsa.Controls) ([]slsa.ControlName, error) { +func computeReviewEnforced(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSet) ([]slsa.ControlName, error) { if !branchPolicy.GetRequireReview() { return []slsa.ControlName{}, nil } - reviewControl := controls.GetControl(slsa.ReviewEnforced) + reviewControl := controls.GetControl(slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW) if reviewControl == nil { return []slsa.ControlName{}, fmt.Errorf("policy requires review, but that control is not enabled") } - if branchPolicy.GetSince().AsTime().Before(reviewControl.GetSince().AsTime()) { + if reviewControl.GetSince() != nil && branchPolicy.GetSince().AsTime().Before(*reviewControl.GetSince()) { return []slsa.ControlName{}, fmt.Errorf("policy requires review since %v, but that control has only been enabled since %v", branchPolicy.GetSince(), reviewControl.GetSince()) } - return []slsa.ControlName{slsa.ReviewEnforced}, nil + return []slsa.ControlName{slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, nil } -func computeTagHygiene(_ *ProtectedBranch, tagPolicy *ProtectedTag, controls slsa.Controls) ([]slsa.ControlName, error) { +// computeTagHygiene checks if the current state of the protected refs +// matches what we see in the policy policy has SLSA_SOURCE_SCS_PROTECTED_REFS +func computeTagHygiene(_ *ProtectedBranch, tagPolicy *ProtectedTag, controls *slsa.ControlSet) ([]slsa.ControlName, error) { if tagPolicy == nil { // There is no tag policy, so the control isn't met, but it's not an error. return []slsa.ControlName{}, nil } + // The tag entry in the policy does not have tag_hygiene if !tagPolicy.GetTagHygiene() { return []slsa.ControlName{}, nil } - tagHygiene := controls.GetControl(slsa.TagHygiene) + // Get the current state of protected refs + tagHygiene := controls.GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS) + + // Tags are not protected. Policy fails if tagHygiene == nil { return []slsa.ControlName{}, fmt.Errorf("policy requires tag hygiene, but that control is not enabled") } - if tagPolicy.GetSince().AsTime().Before(tagHygiene.GetSince().AsTime()) { + // Tags were protected later than the policy date. Fail. mmmh.. + if tagHygiene.GetSince() != nil && tagPolicy.GetSince().AsTime().Before(*tagHygiene.GetSince()) { return []slsa.ControlName{}, fmt.Errorf("policy requires tag hygiene since %v, but that control has only been enabled since %v", tagPolicy.GetSince(), tagHygiene.GetSince()) } - return []slsa.ControlName{slsa.TagHygiene}, nil + return []slsa.ControlName{slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, nil } -func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls slsa.Controls) ([]slsa.ControlName, error) { +func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls *slsa.ControlSet) ([]slsa.ControlName, error) { controlNames := []slsa.ControlName{} for _, rc := range branchPolicy.GetOrgStatusCheckControls() { if !strings.HasPrefix(rc.GetPropertyName(), slsa.AllowedOrgPropPrefix) { @@ -430,7 +454,7 @@ func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls control := controls.GetControl(ghcontrol.CheckNameToControlName(rc.GetCheckName())) if control != nil { - if rc.GetSince().AsTime().Before(control.GetSince().AsTime()) { + if control.GetSince() != nil && rc.GetSince().AsTime().Before(*control.GetSince()) { return []slsa.ControlName{}, fmt.Errorf("policy requires check '%v' since %v, but that control has only been enabled since %v", rc.GetCheckName(), rc.GetSince(), control.GetSince()) } controlNames = append(controlNames, slsa.ControlName(rc.GetPropertyName())) @@ -442,8 +466,13 @@ func computeOrgControls(branchPolicy *ProtectedBranch, _ *ProtectedTag, controls } // Returns a list of controls to include in the vsa's 'verifiedLevels' field when creating a VSA for a branch. -func evaluateBranchControls(branchPolicy *ProtectedBranch, tagPolicy *ProtectedTag, controls slsa.Controls) (slsa.SourceVerifiedLevels, error) { - policyComputers := []computePolicyResult{computeSlsaLevel, computeReviewEnforced, computeTagHygiene, computeOrgControls} +func evaluateBranchControls(branchPolicy *ProtectedBranch, tagPolicy *ProtectedTag, controls *slsa.ControlSet) (slsa.SourceVerifiedLevels, error) { + policyComputers := []computePolicyResult{ + computeSlsaLevel, // Add the SLSA Level to the VSA + computeReviewEnforced, // Stamp if reviews are enforced + computeTagHygiene, // Stamp the tag hygiene + computeOrgControls, // Add other organizational controls + } verifiedLevels := slsa.SourceVerifiedLevels{} @@ -465,7 +494,7 @@ func evaluateTagProv(tagPolicy *ProtectedTag, tagProvPred *provenance.TagProvena // As long as all the controls for tag protection are currently in force then we'll // include the verifiedLevels. - computedControls, err := computeTagHygiene(nil, tagPolicy, tagProvPred.GetControls()) + computedControls, err := computeTagHygiene(nil, tagPolicy, slsa.NewControlSetFromProvanenaceControls(tagProvPred.GetControls())) if err != nil { return slsa.SourceVerifiedLevels{}, fmt.Errorf("error computing tag immutability enforced: %w", err) } @@ -516,10 +545,6 @@ type PolicyEvaluator struct { func NewPolicyEvaluator() *PolicyEvaluator { eval := &PolicyEvaluator{} - // TODO(puerco): Implement functional opts to reuse clients - if eval.reader == nil { - eval.reader = notes.New() - } if eval.authenticator == nil { eval.authenticator = auth.New() @@ -528,7 +553,7 @@ func NewPolicyEvaluator() *PolicyEvaluator { } // EvaluateControl checks the control against the policy and returns the resulting source level and policy path. -func (pe *PolicyEvaluator) EvaluateControl(ctx context.Context, repo *models.Repository, branch *models.Branch, controlStatus *ghcontrol.GhControlStatus) (slsa.SourceVerifiedLevels, string, error) { +func (pe *PolicyEvaluator) EvaluateControl(ctx context.Context, repo *models.Repository, branch *models.Branch, controlStatus *slsa.ControlSet) (slsa.SourceVerifiedLevels, string, error) { // We want to check to ensure the repo hasn't enabled/disabled the rules since // setting the 'since' field in their policy. rp, policyPath, err := pe.GetPolicy(ctx, repo) @@ -542,15 +567,16 @@ func (pe *PolicyEvaluator) EvaluateControl(ctx context.Context, repo *models.Rep policyPath = "DEFAULT" } - if controlStatus.CommitPushTime.Before(branchPolicy.GetSince().AsTime()) { + if controlStatus.Time.Before(branchPolicy.GetSince().AsTime()) { // This commit was pushed before they had an explicit policy. return slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, policyPath, nil } - verifiedLevels, err := evaluateBranchControls(branchPolicy, rp.GetProtectedTag(), controlStatus.Controls) + verifiedLevels, err := evaluateBranchControls(branchPolicy, rp.GetProtectedTag(), controlStatus) if err != nil { return verifiedLevels, policyPath, fmt.Errorf("error evaluating policy %s: %w", policyPath, err) } + return verifiedLevels, policyPath, nil } @@ -572,7 +598,7 @@ func (pe *PolicyEvaluator) EvaluateSourceProv(ctx context.Context, repo *models. policyPath = "DEFAULT" } - verifiedLevels, err := evaluateBranchControls(branchPolicy, rp.GetProtectedTag(), provPred.GetControls()) + verifiedLevels, err := evaluateBranchControls(branchPolicy, rp.GetProtectedTag(), slsa.NewControlSetFromProvanenaceControls(provPred.GetControls())) if err != nil { return slsa.SourceVerifiedLevels{}, policyPath, fmt.Errorf("error evaluating policy %s: %w", policyPath, err) } diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index 8fcef68..cd526d2 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -39,15 +39,19 @@ const ( ) var ( - fixedTime = timestamppb.New(time.Unix(1678886400, 0)) // March 15, 2023 00:00:00 UTC - earlierFixedTime = timestamppb.New(fixedTime.AsTime().Add(-time.Hour)) - laterFixedTime = timestamppb.New(fixedTime.AsTime().Add(time.Hour)) + // fixedTime = timestamppb.New(time.Unix(1678886400, 0)) // March 15, 2023 00:00:00 UTC + // earlierFixedTime = timestamppb.New(fixedTime.AsTime().Add(-time.Hour)) + // laterFixedTime = timestamppb.New(fixedTime.AsTime().Add(time.Hour)) + + fixedTime = time.Unix(1678886400, 0) // March 15, 2023 00:00:00 UTC + earlierFixedTime = fixedTime.Add(-time.Hour) + laterFixedTime = fixedTime.Add(time.Hour) ) func createTestBranchPolicy(branch string) ProtectedBranch { return ProtectedBranch{ Name: branch, - Since: fixedTime, + Since: timestamppb.New(fixedTime), TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: true, } @@ -60,7 +64,7 @@ func createTestPolicy(pb *ProtectedBranch) RepoPolicy { pb, }, ProtectedTag: &ProtectedTag{ - Since: fixedTime, + Since: timestamppb.New(fixedTime), TagHygiene: true, }, } @@ -148,17 +152,35 @@ func validateMockServerRequestPath(t *testing.T, r *http.Request, expectedPolicy } } +// controlsForLevel creates a slsa.ControlSet collection with all required controls +// for the given SLSA level, each with the given Since timestamp. +func controlsForLevel(level slsa.SlsaSourceLevel, since *time.Time) *slsa.ControlSet { + required := slsa.GetRequiredControlsForLevel(level) + controls := &slsa.ControlSet{} + for _, name := range required { + controls.AddControl(&slsa.Control{Name: name, Since: since, State: slsa.StateActive}) + } + return controls +} + +// controlsForLevelWith creates controls for a level plus additional controls. +func controlsForLevelWith(level slsa.SlsaSourceLevel, since *time.Time, extra ...*slsa.Control) *slsa.ControlSet { + controls := controlsForLevel(level, since) + controls.AddControl(extra...) + return controls +} + func TestEvaluateSourceProv_Success(t *testing.T) { - // Controls for mock provenance - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlierFixedTime} - provenanceAvailableEarlier := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: earlierFixedTime} - reviewEnforcedEarlier := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: earlierFixedTime} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlierFixedTime} - orgTestControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_test", Since: earlierFixedTime} + orgTestControl := &slsa.Control{Name: "GH_REQUIRED_CHECK_test", Since: &earlierFixedTime, State: slsa.StateActive} + + // Valid Provenance Predicate with L3 controls + review + tag hygiene + org control + l3Controls := controlsForLevelWith(slsa.SlsaSourceLevel3, &earlierFixedTime, + &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, Since: &earlierFixedTime, State: slsa.StateActive}, + orgTestControl, + ) - // Valid Provenance Predicate (attest.SourceProvenancePred) validProvPredicateL3Controls := provenance.SourceProvenancePred{ - Controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier, orgTestControl}, + Controls: l3Controls.ToProvenanceControls(), } provenanceStatement := createStatementForTest(t, &validProvPredicateL3Controls, provenance.SourceProvPredicateType) @@ -166,17 +188,17 @@ func TestEvaluateSourceProv_Success(t *testing.T) { Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, - Since: fixedTime, + Since: timestamppb.New(fixedTime), OrgStatusCheckControls: []*OrgStatusCheckControl{ { CheckName: "test", PropertyName: "ORG_SOURCE_TESTED", - Since: fixedTime, + Since: timestamppb.New(fixedTime), }, }, } rp := createTestPolicy(&pb) - rp.ProtectedTag.Since = fixedTime + rp.ProtectedTag.Since = timestamppb.New(fixedTime) rp.ProtectedTag.TagHygiene = true expectedPolicyFilePath := createTempPolicyFile(t, &rp) @@ -193,39 +215,33 @@ func TestEvaluateSourceProv_Success(t *testing.T) { if policyPath != expectedPolicyFilePath { t.Errorf("EvaluateSourceProv() policyPath = %q, want %q", policyPath, expectedPolicyFilePath) } - expectedLevels := slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.ReviewEnforced, slsa.TagHygiene, "ORG_SOURCE_TESTED"} + expectedLevels := slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, "ORG_SOURCE_TESTED"} if !slices.Equal(verifiedLevels, expectedLevels) { t.Errorf("EvaluateSourceProv() verifiedLevels = %v, want %v", verifiedLevels, expectedLevels) } } func TestEvaluateSourceProv_Failure(t *testing.T) { - // Controls for mock provenance - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlierFixedTime} - provenanceAvailableEarlier := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: earlierFixedTime} - reviewEnforcedEarlier := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: earlierFixedTime} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlierFixedTime} - // Policies policyL3ReviewTagsNow := RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ - {Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, Since: fixedTime}, + {Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, Since: timestamppb.New(fixedTime)}, }, - ProtectedTag: &ProtectedTag{Since: fixedTime, TagHygiene: true}, + ProtectedTag: &ProtectedTag{Since: timestamppb.New(fixedTime), TagHygiene: true}, } - policyL1NoExtrasNow := RepoPolicy{ // Policy for default/branch not found cases + policyL1NoExtrasNow := RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ - {Name: "otherbranch", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), Since: fixedTime}, + {Name: "otherbranch", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), Since: timestamppb.New(fixedTime)}, }, ProtectedTag: nil, } - // Valid Provenance Predicate (attest.SourceProvenancePred) + // Valid Provenance Predicates validProvPredicateL3Controls := provenance.SourceProvenancePred{ - Controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, + Controls: controlsForLevel(slsa.SlsaSourceLevel3, &earlierFixedTime).ToProvenanceControls(), } validProvPredicateL2Controls := provenance.SourceProvenancePred{ - Controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneEarlier, reviewEnforcedEarlier}, // Missing provenanceAvailable for L3 + Controls: controlsForLevel(slsa.SlsaSourceLevel2, &earlierFixedTime).ToProvenanceControls(), } tests := []struct { @@ -240,7 +256,7 @@ func TestEvaluateSourceProv_Failure(t *testing.T) { policyContent: &policyL3ReviewTagsNow, // Expects L3 provenanceStatement: createStatementForTest(t, &validProvPredicateL2Controls, provenance.SourceProvPredicateType), // Prov only has L2 controls ghConnBranch: "main", - expectedErrorContains: "policy sets target level SLSA_SOURCE_LEVEL_3 which requires [CONTINUITY_ENFORCED TAG_HYGIENE PROVENANCE_AVAILABLE], but branch is only eligible for SLSA_SOURCE_LEVEL_2 because it only has [CONTINUITY_ENFORCED TAG_HYGIENE REVIEW_ENFORCED]", + expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_2", }, { name: "Malformed Policy JSON -> Error", @@ -333,9 +349,9 @@ func createVsaSummary(ref string, verifiedLevels []slsa.ControlName) *provenance func TestEvaluateTagProv_Success(t *testing.T) { // Controls for mock provenance - tagHygieneEarlier := provenance.Control{Name: slsa.TagHygiene.String(), Since: earlierFixedTime} + tagHygieneEarlier := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, Since: &earlierFixedTime, State: slsa.StateActive} origL2ReviewedSummary := createVsaSummary("refs/heads/orig", []slsa.ControlName{ - slsa.ControlName(slsa.SlsaSourceLevel2), slsa.ReviewEnforced, + slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, }) mainL3Summary := createVsaSummary("refs/heads/main", []slsa.ControlName{ slsa.ControlName(slsa.SlsaSourceLevel3), @@ -350,28 +366,28 @@ func TestEvaluateTagProv_Success(t *testing.T) { { name: "Policy has protected_tag setting, and enabled", protectedTagPolicy: &ProtectedTag{ - Since: fixedTime, + Since: timestamppb.New(fixedTime), TagHygiene: true, }, vsaSummaries: []*provenance.VsaSummary{origL2ReviewedSummary}, - expectedLevels: slsa.SourceVerifiedLevels{slsa.ReviewEnforced, slsa.ControlName(slsa.SlsaSourceLevel2)}, + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, }, { name: "Policy has protected_tag setting, and multiple summaries", protectedTagPolicy: &ProtectedTag{ - Since: fixedTime, + Since: timestamppb.New(fixedTime), TagHygiene: true, }, vsaSummaries: []*provenance.VsaSummary{origL2ReviewedSummary, mainL3Summary}, // The spec says we MUST NOT return multiple levels per track in a VSA... expectedLevels: slsa.SourceVerifiedLevels{ - slsa.ReviewEnforced, slsa.ControlName(slsa.SlsaSourceLevel3), + slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, }, }, { name: "Policy has protected_tag setting, and it's not enabled", protectedTagPolicy: &ProtectedTag{ - Since: fixedTime, + Since: timestamppb.New(fixedTime), TagHygiene: false, }, vsaSummaries: []*provenance.VsaSummary{origL2ReviewedSummary}, @@ -380,7 +396,7 @@ func TestEvaluateTagProv_Success(t *testing.T) { { name: "Policy has protected_tag setting, and it's earlier than the control", protectedTagPolicy: &ProtectedTag{ - Since: earlierFixedTime, + Since: timestamppb.New(earlierFixedTime), TagHygiene: false, }, vsaSummaries: []*provenance.VsaSummary{origL2ReviewedSummary}, @@ -396,9 +412,10 @@ func TestEvaluateTagProv_Success(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + s := slsa.ControlSet{Controls: []*slsa.Control{tagHygieneEarlier}} // Valid Provenance Predicate (attest.SourceProvenancePred) tagProvPred := provenance.TagProvenancePred{ - Controls: slsa.Controls{&tagHygieneEarlier}, + Controls: s.ToProvenanceControls(), VsaSummaries: tt.vsaSummaries, } @@ -408,7 +425,7 @@ func TestEvaluateTagProv_Success(t *testing.T) { Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: true, - Since: fixedTime, + Since: timestamppb.New(fixedTime), } rp := createTestPolicy(&pb) rp.ProtectedTag = tt.protectedTagPolicy @@ -436,12 +453,9 @@ func TestEvaluateTagProv_Success(t *testing.T) { } func TestEvaluateControl_Success(t *testing.T) { - // Controls - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlierFixedTime} - provenanceAvailableEarlier := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: earlierFixedTime} - reviewEnforcedEarlier := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: earlierFixedTime} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlierFixedTime} - orgTestControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_test", Since: earlierFixedTime} + orgTestControl := &slsa.Control{Name: "GH_REQUIRED_CHECK_test", Since: &earlierFixedTime, State: slsa.StateActive} + reviewControl := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, Since: &earlierFixedTime, State: slsa.StateActive} + l3ControlsWithExtras := controlsForLevelWith(slsa.SlsaSourceLevel3, &earlierFixedTime, reviewControl, orgTestControl) // Policies fullPolicy := RepoPolicy{ @@ -450,31 +464,31 @@ func TestEvaluateControl_Success(t *testing.T) { Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, - Since: fixedTime, + Since: timestamppb.New(fixedTime), OrgStatusCheckControls: []*OrgStatusCheckControl{ { CheckName: "test", - Since: fixedTime, + Since: timestamppb.New(fixedTime), PropertyName: "ORG_SOURCE_TESTED", }, }, }, }, ProtectedTag: &ProtectedTag{ - Since: fixedTime, + Since: timestamppb.New(fixedTime), TagHygiene: true, }, } basicPolicy := RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ - {Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), Since: fixedTime}, + {Name: "main", TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), Since: timestamppb.New(fixedTime)}, }, } tests := []struct { name string policyContent any // RepoPolicy or string for malformed - controlStatus *ghcontrol.GhControlStatus + controlStatus *slsa.ControlSet ghConnBranch string // Branch for GitHub connection expectedLevels slsa.SourceVerifiedLevels expectedPolicyPath string @@ -482,34 +496,34 @@ func TestEvaluateControl_Success(t *testing.T) { { name: "Commit time before policy Since -> SLSA Level 1", policyContent: &fullPolicy, - controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: earlierFixedTime.AsTime(), // Commit time before policyL3ReviewTagsNow.Since (now) - Controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier, orgTestControl}, + controlStatus: &slsa.ControlSet{ + Time: earlierFixedTime, + Controls: l3ControlsWithExtras.Controls, }, ghConnBranch: "main", - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, // Expect L1 because commit time is before policy enforcement - expectedPolicyPath: "TEMP_POLICY_FILE_PATH", // Placeholder, will be replaced by actual temp file path + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, + expectedPolicyPath: "TEMP_POLICY_FILE_PATH", }, { name: "Commit time after policy Since, controls meet policy -> Expected levels", policyContent: &fullPolicy, - controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: laterFixedTime.AsTime(), - Controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier, orgTestControl}, + controlStatus: &slsa.ControlSet{ + Time: laterFixedTime, + Controls: l3ControlsWithExtras.Controls, }, ghConnBranch: "main", - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.ReviewEnforced, slsa.TagHygiene, "ORG_SOURCE_TESTED"}, + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, "ORG_SOURCE_TESTED"}, expectedPolicyPath: "TEMP_POLICY_FILE_PATH", }, { name: "Branch not in policy, commit after default policy since -> Default policy (SLSA L1)", - policyContent: &basicPolicy, // main is in policy, but we test "develop" - controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: laterFixedTime.AsTime(), - Controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier, orgTestControl}, + policyContent: &basicPolicy, + controlStatus: &slsa.ControlSet{ + Time: laterFixedTime, + Controls: l3ControlsWithExtras.Controls, }, - ghConnBranch: "develop", // Testing "develop" branch - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, // Default is L1 + ghConnBranch: "develop", + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, expectedPolicyPath: "DEFAULT", }, } @@ -529,11 +543,11 @@ func TestEvaluateControl_Success(t *testing.T) { } } - verifiedLevels, policyPath, err := pe.EvaluateControl(ctx, &models.Repository{ - Hostname: "github.com", Path: "local/local", - }, &models.Branch{ - Name: tt.ghConnBranch, - }, tt.controlStatus) + verifiedLevels, policyPath, err := pe.EvaluateControl( + ctx, + &models.Repository{Hostname: "github.com", Path: "local/local"}, + &models.Branch{Name: tt.ghConnBranch}, tt.controlStatus, + ) if err != nil { t.Errorf("EvaluateControl() error = %v, want nil", err) } @@ -554,12 +568,9 @@ func TestEvaluateControl_Success(t *testing.T) { func TestEvaluateControl_Failure(t *testing.T) { now := timestamppb.Now() earlier := timestamppb.New(time.Now().Add(-time.Hour)) + rearlier := earlier.AsTime() later := timestamppb.New(time.Now().Add(time.Hour)) - // Controls - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlier} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlier} - // Policies policyL3Review := RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ @@ -569,17 +580,17 @@ func TestEvaluateControl_Failure(t *testing.T) { tests := []struct { name string - policyContent any // RepoPolicy or string for malformed - controlStatus *ghcontrol.GhControlStatus - ghConnBranch string // Branch for GitHub connection + policyContent any + controlStatus *slsa.ControlSet + ghConnBranch string expectedErrorContains string }{ { name: "Commit time after policy Since, controls DO NOT meet policy -> Error", - policyContent: &policyL3Review, // Requires L3, Review, Tags - controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: later.AsTime(), - Controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneEarlier}, // Only meets L2 + policyContent: &policyL3Review, + controlStatus: &slsa.ControlSet{ + Time: later.AsTime(), + Controls: controlsForLevel(slsa.SlsaSourceLevel2, &rearlier).Controls, }, ghConnBranch: "main", expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_2", @@ -587,9 +598,8 @@ func TestEvaluateControl_Failure(t *testing.T) { { name: "Malformed JSON -> Error", policyContent: "not json", - controlStatus: &ghcontrol.GhControlStatus{ - CommitPushTime: later.AsTime(), - Controls: slsa.Controls{}, + controlStatus: &slsa.ControlSet{ + Time: later.AsTime(), }, ghConnBranch: "main", expectedErrorContains: "syntax error (line 1:1): invalid value", // Error from json.Unmarshal @@ -710,40 +720,34 @@ const ( ) func TestComputeEligibleSlsaLevel(t *testing.T) { - continuityEnforcedControl := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: fixedTime} - provenanceAvailableControl := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: fixedTime} - reviewEnforcedControl := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: fixedTime} - tagHygieneControl := &provenance.Control{Name: slsa.TagHygiene.String(), Since: fixedTime} - tests := []struct { - name string - controls slsa.Controls - expectedLevel slsa.SlsaSourceLevel - expectedReason string + name string + controls *slsa.ControlSet + expectedLevel slsa.SlsaSourceLevel }{ { name: "SLSA Level 4", - controls: slsa.Controls{continuityEnforcedControl, provenanceAvailableControl, reviewEnforcedControl, tagHygieneControl}, + controls: controlsForLevel(slsa.SlsaSourceLevel4, &fixedTime), expectedLevel: slsa.SlsaSourceLevel4, }, { name: "SLSA Level 3", - controls: slsa.Controls{continuityEnforcedControl, provenanceAvailableControl, tagHygieneControl}, + controls: controlsForLevel(slsa.SlsaSourceLevel3, &fixedTime), expectedLevel: slsa.SlsaSourceLevel3, }, { name: "SLSA Level 2", - controls: slsa.Controls{continuityEnforcedControl, tagHygieneControl}, + controls: controlsForLevel(slsa.SlsaSourceLevel2, &fixedTime), expectedLevel: slsa.SlsaSourceLevel2, }, { - name: "SLSA Level 1 - ProvenanceAvailable only", - controls: slsa.Controls{provenanceAvailableControl}, + name: "SLSA Level 1 - partial controls only", + controls: &slsa.ControlSet{Controls: []*slsa.Control{{Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, Since: &fixedTime, State: slsa.StateActive}}}, expectedLevel: slsa.SlsaSourceLevel1, }, { - name: "SLSA Level 1 - ContinuityEnforced control absent", - controls: nil, // Represents absence of ContinuityEnforced; could also use slsa.Controls{} + name: "SLSA Level 1 - no controls", + controls: nil, expectedLevel: slsa.SlsaSourceLevel1, }, } @@ -759,32 +763,28 @@ func TestComputeEligibleSlsaLevel(t *testing.T) { } func TestEvaluateBranchControls(t *testing.T) { - // Controls - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlierFixedTime} - provenanceAvailableEarlier := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: earlierFixedTime} - reviewEnforcedEarlier := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: earlierFixedTime} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlierFixedTime} - tagHygieneNow := &provenance.Control{Name: slsa.TagHygiene.String(), Since: fixedTime} + reviewEarlier := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, Since: &earlierFixedTime, State: slsa.StateActive} + tagHygieneNow := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, Since: &fixedTime, State: slsa.StateActive} // Branch Policies - policyL3Review := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, Since: fixedTime} - policyL4 := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: fixedTime} - policyL1NoExtras := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), RequireReview: false, Since: fixedTime} - policyL2Review := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: true, Since: fixedTime} - policyL2NoReview := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: false, Since: fixedTime} + policyL3Review := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), RequireReview: true, Since: timestamppb.New(fixedTime)} + policyL4 := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: timestamppb.New(fixedTime)} + policyL1NoExtras := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), RequireReview: false, Since: timestamppb.New(fixedTime)} + policyL2Review := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: true, Since: timestamppb.New(fixedTime)} + policyL2NoReview := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), RequireReview: false, Since: timestamppb.New(fixedTime)} // Tag policies - tagHygienePolicy := ProtectedTag{Since: fixedTime, TagHygiene: true} - noTagHygienePolicy := ProtectedTag{Since: fixedTime, TagHygiene: false} + tagHygienePolicy := ProtectedTag{Since: timestamppb.New(fixedTime), TagHygiene: true} + noTagHygienePolicy := ProtectedTag{Since: timestamppb.New(fixedTime), TagHygiene: false} // Policy Since 'earlier' for testing control.Since > policy.Since - policyL1Earlier := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), RequireReview: false, Since: earlierFixedTime} + policyL1Earlier := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel1), RequireReview: false, Since: timestamppb.New(earlierFixedTime)} tests := []struct { name string branchPolicy *ProtectedBranch tagPolicy *ProtectedTag - controls slsa.Controls + controls *slsa.ControlSet expectedLevels slsa.SourceVerifiedLevels expectError bool expectedErrorContains string @@ -793,15 +793,15 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Success - L3, Review, Tags", branchPolicy: &policyL3Review, tagPolicy: &tagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.ReviewEnforced, slsa.TagHygiene}, + controls: controlsForLevelWith(slsa.SlsaSourceLevel3, &earlierFixedTime, reviewEarlier), + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, { name: "Success - L1", branchPolicy: &policyL1NoExtras, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{}, // L1 is met by default if policy targets L1 and other conditions pass + controls: controlsForLevel(slsa.SlsaSourceLevel1, &earlierFixedTime), expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel1)}, expectError: false, }, @@ -809,59 +809,61 @@ func TestEvaluateBranchControls(t *testing.T) { name: "Success - L2 & Review", branchPolicy: &policyL2Review, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, // Provenance not needed for L2 - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.ReviewEnforced}, + controls: controlsForLevelWith(slsa.SlsaSourceLevel2, &earlierFixedTime, reviewEarlier), + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, expectError: false, }, { - name: "Success - L2 & Tags", - branchPolicy: &policyL2NoReview, - tagPolicy: &tagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneEarlier}, // Provenance not needed for L2 - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.TagHygiene}, + name: "Success - L2 & Tags", + branchPolicy: &policyL2NoReview, + tagPolicy: &tagHygienePolicy, + controls: controlsForLevelWith(slsa.SlsaSourceLevel2, &earlierFixedTime, + &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, Since: &earlierFixedTime, State: slsa.StateActive}, + ), + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel2), slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, { name: "Success - L4", branchPolicy: &policyL4, tagPolicy: &tagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel4), slsa.TagHygiene}, + controls: controlsForLevel(slsa.SlsaSourceLevel4, &earlierFixedTime), + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel4), slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, expectError: false, }, { name: "Error - computeSlsaLevel Fails (Policy L3, Controls L1)", - branchPolicy: &policyL3Review, // Wants L3 + branchPolicy: &policyL3Review, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{}, // Only eligible for L1 + controls: &slsa.ControlSet{}, expectedLevels: slsa.SourceVerifiedLevels{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", }, { name: "Error - computeReviewEnforced Fails (Policy L2+Review, Review control missing)", - branchPolicy: &policyL2Review, // Wants L2 & Review + branchPolicy: &policyL2Review, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneEarlier}, // Eligible for L2, but Review control missing + controls: controlsForLevel(slsa.SlsaSourceLevel2, &earlierFixedTime), expectedLevels: slsa.SourceVerifiedLevels{}, expectError: true, expectedErrorContains: "policy requires review, but that control is not enabled", }, { name: "Error - computeTagHygiene Fails (Policy L1+Tags, Tag control Since later than Policy Since)", - branchPolicy: &policyL1Earlier, // Wants L1 & Tags, Policy.Since = earlier - tagPolicy: &ProtectedTag{Since: earlierFixedTime, TagHygiene: true}, - controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneNow}, // Eligible L1, Tag.Since = now + branchPolicy: &policyL1Earlier, + tagPolicy: &ProtectedTag{Since: timestamppb.New(earlierFixedTime), TagHygiene: true}, + controls: controlsForLevelWith(slsa.SlsaSourceLevel1, &earlierFixedTime, tagHygieneNow), expectedLevels: slsa.SourceVerifiedLevels{}, expectError: true, - expectedErrorContains: "policy requires tag hygiene since", // ... but that control has only been enabled since ... + expectedErrorContains: "policy requires tag hygiene since", }, { name: "Success - Mixed Requirements (L3, Review, No Tags)", branchPolicy: &policyL3Review, tagPolicy: &noTagHygienePolicy, - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, - expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.ReviewEnforced}, + controls: controlsForLevelWith(slsa.SlsaSourceLevel3, &earlierFixedTime, reviewEarlier), + expectedLevels: slsa.SourceVerifiedLevels{slsa.ControlName(slsa.SlsaSourceLevel3), slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, expectError: false, }, } @@ -908,158 +910,148 @@ func TestEvaluateBranchControls(t *testing.T) { } } +// assertControlResult is a shared helper for testing computePolicyResult functions. +func assertControlResult(t *testing.T, fnName string, gotControls []slsa.ControlName, err error, expectedControls []slsa.ControlName, expectError bool, expectedErrorContains string) { + t.Helper() + if expectError { + if err == nil { + t.Errorf("%s() error = nil, want non-nil error containing %q", fnName, expectedErrorContains) + } else if !strings.Contains(err.Error(), expectedErrorContains) { + t.Errorf("%s() error = %q, want error containing %q", fnName, err.Error(), expectedErrorContains) + } + } else { + if err != nil { + t.Errorf("%s() error = %v, want nil", fnName, err) + } + } + if !slices.Equal(gotControls, expectedControls) { + t.Errorf("%s() gotControls = %v, want %v", fnName, gotControls, expectedControls) + } +} + func TestComputeTagHygiene(t *testing.T) { now := timestamppb.Now() + tnow := now.AsTime() earlier := timestamppb.New(time.Now().Add(-time.Hour)) - // Branch Policies policyRequiresTagHygieneNow := ProtectedTag{TagHygiene: true, Since: now} policyRequiresTagHygieneEarlier := ProtectedTag{TagHygiene: true, Since: earlier} policyNotRequiresTagHygiene := ProtectedTag{TagHygiene: false, Since: now} - - // Controls - tagHygieneControlEnabledNow := &provenance.Control{Name: slsa.TagHygiene.String(), Since: now} + tagHygieneControlEnabledNow := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, Since: &tnow, State: slsa.StateActive} tests := []struct { name string tagPolicy *ProtectedTag - controls slsa.Controls + computeFn func() ([]slsa.ControlName, error) expectedControls []slsa.ControlName expectError bool expectedErrorContains string }{ { - name: "Policy requires tag hygiene, control compliant (Policy.Since >= Control.Since)", - tagPolicy: &policyRequiresTagHygieneNow, - controls: slsa.Controls{tagHygieneControlEnabledNow}, // Policy.Since == Control.Since - expectedControls: []slsa.ControlName{slsa.TagHygiene}, - expectError: false, + name: "Policy requires tag hygiene, control compliant (Policy.Since >= Control.Since)", + tagPolicy: &policyRequiresTagHygieneNow, + computeFn: func() ([]slsa.ControlName, error) { + return computeTagHygiene(nil, &policyRequiresTagHygieneNow, &slsa.ControlSet{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}) + }, + expectedControls: []slsa.ControlName{slsa.SLSA_SOURCE_SCS_PROTECTED_REFS}, }, { - name: "Policy does not require tag hygiene - control state irrelevant", - tagPolicy: &policyNotRequiresTagHygiene, - controls: slsa.Controls{}, // Control state explicitly shown as irrelevant + name: "Policy does not require tag hygiene - control state irrelevant", + computeFn: func() ([]slsa.ControlName, error) { + return computeTagHygiene(nil, &policyNotRequiresTagHygiene, &slsa.ControlSet{}) + }, expectedControls: []slsa.ControlName{}, - expectError: false, }, { - name: "Policy requires tag hygiene, control not present: fail", - tagPolicy: &policyRequiresTagHygieneNow, - controls: slsa.Controls{}, // Tag Hygiene control missing + name: "Policy requires tag hygiene, control not present: fail", + computeFn: func() ([]slsa.ControlName, error) { + return computeTagHygiene(nil, &policyRequiresTagHygieneNow, &slsa.ControlSet{}) + }, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires tag hygiene, but that control is not enabled", }, { - name: "Policy requires tag hygiene, control enabled, Policy.Since < Control.Since: fail", - tagPolicy: &policyRequiresTagHygieneEarlier, // Policy.Since is 'earlier' - controls: slsa.Controls{tagHygieneControlEnabledNow}, // Control.Since is 'now' + name: "Policy requires tag hygiene, control enabled, Policy.Since < Control.Since: fail", + computeFn: func() ([]slsa.ControlName, error) { + return computeTagHygiene(nil, &policyRequiresTagHygieneEarlier, &slsa.ControlSet{Controls: []*slsa.Control{tagHygieneControlEnabledNow}}) + }, expectedControls: []slsa.ControlName{}, expectError: true, - expectedErrorContains: "policy requires tag hygiene since", // ...but that control has only been enabled since... + expectedErrorContains: "policy requires tag hygiene since", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotControls, err := computeTagHygiene(nil, tt.tagPolicy, tt.controls) - - if tt.expectError { - if err == nil { - t.Errorf("computeTagHygiene() error = nil, want non-nil error containing %q", tt.expectedErrorContains) - } else if !strings.Contains(err.Error(), tt.expectedErrorContains) { - t.Errorf("computeTagHygiene() error = %q, want error containing %q", err.Error(), tt.expectedErrorContains) - } - } else { - if err != nil { - t.Errorf("computeTagHygiene() error = %v, want nil", err) - } - } - - if !slices.Equal(gotControls, tt.expectedControls) { - t.Errorf("computeTagHygiene() gotControls = %v, want %v", gotControls, tt.expectedControls) - } + gotControls, err := tt.computeFn() + assertControlResult(t, "computeTagHygiene", gotControls, err, tt.expectedControls, tt.expectError, tt.expectedErrorContains) }) } } func TestComputeReviewEnforced(t *testing.T) { now := timestamppb.Now() + tnow := now.AsTime() earlier := timestamppb.New(time.Now().Add(-time.Hour)) - // Branch Policies policyRequiresReviewNow := ProtectedBranch{RequireReview: true, Since: now} policyRequiresReviewEarlier := ProtectedBranch{RequireReview: true, Since: earlier} policyNotRequiresReview := ProtectedBranch{RequireReview: false, Since: now} - - // Controls - reviewControlEnabledNow := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: now} + reviewControlEnabledNow := &slsa.Control{Name: slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, Since: &tnow, State: slsa.StateActive} tests := []struct { name string - branchPolicy *ProtectedBranch - controls slsa.Controls + computeFn func() ([]slsa.ControlName, error) expectedControls []slsa.ControlName expectError bool expectedErrorContains string }{ { - name: "Policy requires review, control compliant (Policy.Since >= Control.Since)", - branchPolicy: &policyRequiresReviewNow, - controls: slsa.Controls{reviewControlEnabledNow}, // Policy.Since == Control.Since - expectedControls: []slsa.ControlName{slsa.ReviewEnforced}, - expectError: false, + name: "Policy requires review, control compliant (Policy.Since >= Control.Since)", + computeFn: func() ([]slsa.ControlName, error) { + return computeReviewEnforced(&policyRequiresReviewNow, nil, &slsa.ControlSet{Controls: []*slsa.Control{reviewControlEnabledNow}}) + }, + expectedControls: []slsa.ControlName{slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW}, }, { - name: "Policy does not require review - control state irrelevant", - branchPolicy: &policyNotRequiresReview, - controls: slsa.Controls{}, // Control state explicitly shown as irrelevant + name: "Policy does not require review - control state irrelevant", + computeFn: func() ([]slsa.ControlName, error) { + return computeReviewEnforced(&policyNotRequiresReview, nil, &slsa.ControlSet{}) + }, expectedControls: []slsa.ControlName{}, - expectError: false, }, { - name: "Policy requires review, control not present: fail", - branchPolicy: &policyRequiresReviewNow, - controls: slsa.Controls{}, // Review control missing + name: "Policy requires review, control not present: fail", + computeFn: func() ([]slsa.ControlName, error) { + return computeReviewEnforced(&policyRequiresReviewNow, nil, &slsa.ControlSet{}) + }, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires review, but that control is not enabled", }, { - name: "Policy requires review, control enabled, Policy.Since < Control.Since: fail", - branchPolicy: &policyRequiresReviewEarlier, // Policy.Since is 'earlier' - controls: slsa.Controls{reviewControlEnabledNow}, // Control.Since is 'now' + name: "Policy requires review, control enabled, Policy.Since < Control.Since: fail", + computeFn: func() ([]slsa.ControlName, error) { + return computeReviewEnforced(&policyRequiresReviewEarlier, nil, &slsa.ControlSet{Controls: []*slsa.Control{reviewControlEnabledNow}}) + }, expectedControls: []slsa.ControlName{}, expectError: true, - expectedErrorContains: "policy requires review since", // ...but that control has only been enabled since... + expectedErrorContains: "policy requires review since", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotControls, err := computeReviewEnforced(tt.branchPolicy, nil, tt.controls) - - if tt.expectError { - if err == nil { - t.Errorf("computeReviewEnforced() error = nil, want non-nil error containing %q", tt.expectedErrorContains) - } else if !strings.Contains(err.Error(), tt.expectedErrorContains) { - t.Errorf("computeReviewEnforced() error = %q, want error containing %q", err.Error(), tt.expectedErrorContains) - } - } else { - if err != nil { - t.Errorf("computeReviewEnforced() error = %v, want nil", err) - } - } - - if !slices.Equal(gotControls, tt.expectedControls) { - t.Errorf("computeReviewEnforced() gotEnforced = %v, want %v", gotControls, tt.expectedControls) - } + gotControls, err := tt.computeFn() + assertControlResult(t, "computeReviewEnforced", gotControls, err, tt.expectedControls, tt.expectError, tt.expectedErrorContains) }) } } func TestComputeOrgControls(t *testing.T) { now := timestamppb.Now() + tnow := now.AsTime() earlier := timestamppb.New(time.Now().Add(-time.Hour)) // StatusCheckControls @@ -1069,14 +1061,14 @@ func TestComputeOrgControls(t *testing.T) { invalidPropertyNameControlPolicy := &OrgStatusCheckControl{PropertyName: "SLSA_TESTED", Since: now, CheckName: "test"} // Controls - testedControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_test", Since: now} - lintedControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_run-the-linter", Since: now} - notListedControl := &provenance.Control{Name: "GH_REQUIRED_CHECK_not-configured-in-policy", Since: now} + testedControl := &slsa.Control{Name: "GH_REQUIRED_CHECK_test", Since: &tnow, State: slsa.StateActive} + lintedControl := &slsa.Control{Name: "GH_REQUIRED_CHECK_run-the-linter", Since: &tnow, State: slsa.StateActive} + notListedControl := &slsa.Control{Name: "GH_REQUIRED_CHECK_not-configured-in-policy", Since: &tnow, State: slsa.StateActive} tests := []struct { name string orgCheckPolicies []*OrgStatusCheckControl - controls slsa.Controls + controls *slsa.ControlSet expectedControls []slsa.ControlName expectError bool expectedErrorContains string @@ -1084,28 +1076,28 @@ func TestComputeOrgControls(t *testing.T) { { name: "Single check handled", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy}, - controls: slsa.Controls{testedControl}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{testedControl}}, expectedControls: []slsa.ControlName{"ORG_SOURCE_TESTED"}, expectError: false, }, { name: "Multiple checks handled", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy, lintedControlPolicy}, - controls: slsa.Controls{testedControl, lintedControl}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{testedControl, lintedControl}}, expectedControls: []slsa.ControlName{"ORG_SOURCE_TESTED", "ORG_SOURCE_LINTED"}, expectError: false, }, { name: "Not configured control should not be returned", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy}, - controls: slsa.Controls{testedControl, notListedControl}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{testedControl, notListedControl}}, expectedControls: []slsa.ControlName{"ORG_SOURCE_TESTED"}, expectError: false, }, { name: "Policy requires control but it is not present", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy, lintedControlPolicy}, - controls: slsa.Controls{lintedControl}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{lintedControl}}, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires check 'test', but", @@ -1113,7 +1105,7 @@ func TestComputeOrgControls(t *testing.T) { { name: "Control not enabled long enough fails", orgCheckPolicies: []*OrgStatusCheckControl{earlierTestedControlPolicy}, - controls: slsa.Controls{testedControl}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{testedControl}}, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy requires check 'test' since", @@ -1121,7 +1113,7 @@ func TestComputeOrgControls(t *testing.T) { { name: "Invalid property name fails", orgCheckPolicies: []*OrgStatusCheckControl{testedControlPolicy, invalidPropertyNameControlPolicy}, - controls: slsa.Controls{testedControl}, + controls: &slsa.ControlSet{Controls: []*slsa.Control{testedControl}}, expectedControls: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy specifies an invalid property name", @@ -1153,29 +1145,21 @@ func TestComputeOrgControls(t *testing.T) { } func TestComputeSlsaLevel(t *testing.T) { - now := timestamppb.Now() - earlier := timestamppb.New(time.Now().Add(-time.Hour)) - - // Controls - continuityEnforcedNow := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: now} - provenanceAvailableNow := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: now} - reviewEnforcedNow := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: now} - tagHygieneNow := &provenance.Control{Name: slsa.TagHygiene.String(), Since: now} - continuityEnforcedEarlier := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: earlier} - provenanceAvailableEarlier := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: earlier} - reviewEnforcedEarlier := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: earlier} - tagHygieneEarlier := &provenance.Control{Name: slsa.TagHygiene.String(), Since: earlier} + rnow := time.Now() + now := &rnow + rearlier := time.Now().Add(-time.Hour) + earlier := &rearlier // Branch Policies - policyL4Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: now} - policyL3Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: now} - policyL2Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), Since: now} - policyUnknownLevel := ProtectedBranch{TargetSlsaSourceLevel: "UNKNOWN_LEVEL", Since: now} + policyL4Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: timestamppb.New(rnow)} + policyL3Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: timestamppb.New(rnow)} + policyL2Now := ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), Since: timestamppb.New(rnow)} + policyUnknownLevel := ProtectedBranch{TargetSlsaSourceLevel: "UNKNOWN_LEVEL", Since: timestamppb.New(rnow)} tests := []struct { name string branchPolicy *ProtectedBranch - controls slsa.Controls + controls *slsa.ControlSet expectedLevels []slsa.ControlName expectError bool expectedErrorContains string @@ -1183,72 +1167,68 @@ func TestComputeSlsaLevel(t *testing.T) { { name: "Controls L4-eligible (since 'earlier'), Policy L4 (since 'now'): success", branchPolicy: &policyL4Now, - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, reviewEnforcedEarlier, tagHygieneEarlier}, + controls: controlsForLevel(slsa.SlsaSourceLevel4, earlier), expectedLevels: []slsa.ControlName{slsa.ControlName(slsa.SlsaSourceLevel4)}, expectError: false, }, { name: "Controls L3-eligible (since 'earlier'), Policy L2 (since 'now'): success", - branchPolicy: &policyL2Now, // Policy L2, Since 'now' - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, tagHygieneEarlier}, // Eligible L3 since 'earlier' + branchPolicy: &policyL2Now, + controls: controlsForLevel(slsa.SlsaSourceLevel3, earlier), expectedLevels: []slsa.ControlName{slsa.ControlName(slsa.SlsaSourceLevel2)}, expectError: false, }, { name: "Controls L1-eligible, Policy L2: fail (eligibility)", - branchPolicy: &policyL2Now, // Policy L2 - controls: slsa.Controls{}, // Eligible L1 + branchPolicy: &policyL2Now, + controls: &slsa.ControlSet{}, expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", }, { name: "Eligible L3 (since 'earlier'), Policy L3 (since 'now'): compliant Policy.Since", - branchPolicy: &policyL3Now, // Policy L3, Since 'now' - controls: slsa.Controls{continuityEnforcedEarlier, provenanceAvailableEarlier, tagHygieneEarlier}, // Eligible L3 since 'earlier' - expectedLevels: []slsa.ControlName{slsa.ControlName(slsa.SlsaSourceLevel3)}, // Policy.Since ('now') is not before EligibleSince ('earlier') + branchPolicy: &policyL3Now, + controls: controlsForLevel(slsa.SlsaSourceLevel3, earlier), + expectedLevels: []slsa.ControlName{slsa.ControlName(slsa.SlsaSourceLevel3)}, expectError: false, }, { name: "Controls L4-eligible (since 'now'), Policy L4 (since 'earlier'): fail (Policy.Since too early)", - branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: earlier}, - controls: slsa.Controls{continuityEnforcedNow, provenanceAvailableNow, reviewEnforcedNow, tagHygieneNow}, + branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel4), Since: timestamppb.New(*earlier)}, + controls: controlsForLevel(slsa.SlsaSourceLevel4, now), expectedLevels: []slsa.ControlName{}, expectError: true, - expectedErrorContains: "policy sets target level SLSA_SOURCE_LEVEL_4 since", // ...but it has only been eligible for that level since... + expectedErrorContains: "policy sets target level SLSA_SOURCE_LEVEL_4 since", }, { name: "Controls L3-eligible (since 'now'), Policy L3 (since 'earlier'): fail (Policy.Since too early)", - branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: earlier}, // Policy L3, Since 'earlier' - controls: slsa.Controls{continuityEnforcedNow, provenanceAvailableNow, tagHygieneNow}, // Eligible L3 since 'now' + branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3), Since: timestamppb.New(*earlier)}, + controls: controlsForLevel(slsa.SlsaSourceLevel3, now), expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy sets target level SLSA_SOURCE_LEVEL_3 since", }, { name: "Controls L2-eligible (since 'now'), Policy L2 (since 'earlier'): fail (Policy.Since too early)", - branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), Since: earlier}, - controls: slsa.Controls{continuityEnforcedEarlier, tagHygieneNow}, + branchPolicy: &ProtectedBranch{TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2), Since: timestamppb.New(*earlier)}, + controls: controlsForLevel(slsa.SlsaSourceLevel2, now), expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy sets target level SLSA_SOURCE_LEVEL_2 since", }, { name: "Policy L?'UNKNOWN' (controls L3-eligible): fail (policy target unknown)", - branchPolicy: &policyUnknownLevel, // Policy "UNKNOWN_LEVEL" - controls: slsa.Controls{continuityEnforcedNow, provenanceAvailableNow}, // Eligible L3 + branchPolicy: &policyUnknownLevel, + controls: controlsForLevel(slsa.SlsaSourceLevel3, now), expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "policy sets target level UNKNOWN_LEVEL", }, - // This single case covers eligibility failure where target > eligible. - // It replaces the two previous similar cases: - // "computeEligibleSince returns nil (controls insufficient for target level)" which was L2 controls for L3 policy - // "Controls for L1, Policy L3, computeEligibleSince for L3 returns nil" which was L1 controls for L3 policy { name: "Controls L1-eligible, Policy L3: fail (eligibility)", - branchPolicy: &policyL3Now, // Policy L3 - controls: slsa.Controls{}, // Eligible L1 + branchPolicy: &policyL3Now, + controls: &slsa.ControlSet{}, expectedLevels: []slsa.ControlName{}, expectError: true, expectedErrorContains: "but branch is only eligible for SLSA_SOURCE_LEVEL_1", @@ -1278,112 +1258,126 @@ func TestComputeSlsaLevel(t *testing.T) { } } +// controlsForLevelWithOverride creates controls for a level, then overrides the +// Since timestamp for a specific control name. +func controlsForLevelWithOverride(level slsa.SlsaSourceLevel, baseSince *time.Time, overrides map[slsa.ControlName]time.Time) *slsa.ControlSet { + controls := controlsForLevel(level, baseSince) + for i, c := range controls.Controls { + if since, ok := overrides[c.GetName()]; ok { + controls.Controls[i] = &slsa.Control{Name: c.GetName(), Since: &since, State: slsa.StateActive} + } + } + return controls +} + func TestComputeEligibleSince(t *testing.T) { - time1 := timestamppb.Now() - time2 := timestamppb.New(time.Now().Add(time.Hour)) - zeroTime := timestamppb.New(time.Time{}) - - continuityEnforcedT1 := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: time1} - provenanceAvailableT1 := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: time1} - reviewEnforcedT1 := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: time1} - tagHygieneT1 := &provenance.Control{Name: slsa.TagHygiene.String(), Since: time1} - continuityEnforcedT2 := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: time2} - provenanceAvailableT2 := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: time2} - reviewEnforcedT2 := &provenance.Control{Name: slsa.ReviewEnforced.String(), Since: time2} - tagHygieneZero := &provenance.Control{Name: slsa.TagHygiene.String(), Since: zeroTime} - continuityEnforcedZero := &provenance.Control{Name: slsa.ContinuityEnforced.String(), Since: zeroTime} - provenanceAvailableZero := &provenance.Control{Name: slsa.ProvenanceAvailable.String(), Since: zeroTime} + time1 := time.Now() + time2 := time.Now().Add(time.Hour) + zeroTime := time.Time{} tests := []struct { name string - controls slsa.Controls + controls *slsa.ControlSet level slsa.SlsaSourceLevel - expectedTime *timestamppb.Timestamp + expectedTime *time.Time expectError bool expectedError string }{ { - name: "L4 eligible (prov, review later)", - controls: slsa.Controls{continuityEnforcedT1, provenanceAvailableT2, reviewEnforcedT2, tagHygieneT1}, + name: "L4 eligible (two controls later)", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel4, &time1, map[slsa.ControlName]time.Time{ + slsa.SLSA_SOURCE_SCS_PROVENANCE: time2, + slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW: time2, + }), level: slsa.SlsaSourceLevel4, - expectedTime: time2, + expectedTime: &time2, expectError: false, }, { - name: "L4 eligible (continuity later)", - controls: slsa.Controls{continuityEnforcedT2, provenanceAvailableT1, reviewEnforcedT1, tagHygieneT1}, + name: "L4 eligible (one control later)", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel4, &time1, map[slsa.ControlName]time.Time{ + slsa.SLSA_SOURCE_SCS_CONTINUITY: time2, + }), level: slsa.SlsaSourceLevel4, - expectedTime: time2, + expectedTime: &time2, expectError: false, }, { - name: "L3 eligible (ProvLater), L3 requested: expect Prov.Since", - controls: slsa.Controls{continuityEnforcedT1, provenanceAvailableT2, tagHygieneT1}, + name: "L3 eligible (ProvLater), L3 requested: expect Prov.Since", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, &time1, map[slsa.ControlName]time.Time{ + slsa.SLSA_SOURCE_SCS_PROVENANCE: time2, + }), level: slsa.SlsaSourceLevel3, - expectedTime: time2, + expectedTime: &time2, expectError: false, }, { - name: "L3 eligible (ContLater), L3 requested: expect Cont.Since", - controls: slsa.Controls{continuityEnforcedT2, provenanceAvailableT1, tagHygieneT1}, + name: "L3 eligible (ContLater), L3 requested: expect Cont.Since", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, &time1, map[slsa.ControlName]time.Time{ + slsa.SLSA_SOURCE_SCS_CONTINUITY: time2, + }), level: slsa.SlsaSourceLevel3, - expectedTime: time2, + expectedTime: &time2, expectError: false, }, { - name: "L2 eligible (Cont&HygieneOnly), L2 requested: expect Cont.Since", // Was: "Eligible for SLSA Level 2" - controls: slsa.Controls{continuityEnforcedT1, tagHygieneT1}, + name: "L2 eligible, L2 requested: expect base time", + controls: controlsForLevel(slsa.SlsaSourceLevel2, &time1), level: slsa.SlsaSourceLevel2, - expectedTime: time1, + expectedTime: &time1, expectError: false, }, { - name: "L1 eligible (NoControls), L1 requested: expect ZeroTime", // Was: "Eligible for SLSA Level 1" - controls: slsa.Controls{}, + name: "L1 eligible, L1 requested: expect ZeroTime", + controls: controlsForLevel(slsa.SlsaSourceLevel1, &zeroTime), level: slsa.SlsaSourceLevel1, - expectedTime: zeroTime, + expectedTime: &zeroTime, expectError: false, }, { - name: "L3 eligible, L2 requested: expect Cont.Since", - controls: slsa.Controls{continuityEnforcedT1, provenanceAvailableT2, tagHygieneT1}, + name: "L3 eligible, L2 requested: expect base time", + controls: controlsForLevel(slsa.SlsaSourceLevel3, &time1), level: slsa.SlsaSourceLevel2, - expectedTime: time1, + expectedTime: &time1, expectError: false, }, { name: "L2 eligible, L3 requested: expect nil, no error", - controls: slsa.Controls{continuityEnforcedT1, tagHygieneT1}, + controls: controlsForLevel(slsa.SlsaSourceLevel2, &time1), level: slsa.SlsaSourceLevel3, expectedTime: nil, expectError: false, }, { name: "Unknown level requested: expect nil, error", - controls: slsa.Controls{}, + controls: &slsa.ControlSet{}, level: slsa.SlsaSourceLevel("UNKNOWN_LEVEL"), - expectedTime: zeroTime, + expectedTime: &zeroTime, expectError: false, }, { - name: "L3 eligible (ContZero, ProvNonZero, TagNoZero), L3 requested: expect Prov.Since", - controls: slsa.Controls{continuityEnforcedZero, provenanceAvailableT2, tagHygieneT1}, + name: "L3 eligible (ContZero, ProvNonZero), L3 requested: expect Prov.Since", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, &zeroTime, map[slsa.ControlName]time.Time{ + slsa.SLSA_SOURCE_SCS_PROVENANCE: time2, + }), level: slsa.SlsaSourceLevel3, - expectedTime: time2, + expectedTime: &time2, expectError: false, }, { - name: "L3 eligible (ContNonZero, ProvZero, TagNoZero), L3 requested: expect Cont.Since", - controls: slsa.Controls{continuityEnforcedT1, provenanceAvailableZero, tagHygieneT1}, + name: "L3 eligible (ContNonZero, ProvZero), L3 requested: expect Cont.Since", + controls: controlsForLevelWithOverride(slsa.SlsaSourceLevel3, &zeroTime, map[slsa.ControlName]time.Time{ + slsa.SLSA_SOURCE_SCS_CONTINUITY: time1, + }), level: slsa.SlsaSourceLevel3, - expectedTime: time1, + expectedTime: &time1, expectError: false, }, { name: "L3 eligible (AllZero), L3 requested: expect ZeroTime", - controls: slsa.Controls{continuityEnforcedZero, provenanceAvailableZero, tagHygieneZero}, + controls: controlsForLevel(slsa.SlsaSourceLevel3, &zeroTime), level: slsa.SlsaSourceLevel3, - expectedTime: zeroTime, + expectedTime: &zeroTime, expectError: false, }, } @@ -1411,7 +1405,7 @@ func TestComputeEligibleSince(t *testing.T) { } else { if gotTime == nil { t.Errorf("computeEligibleSince() gotTime = nil, want %v", tt.expectedTime) - } else if !gotTime.Equal(tt.expectedTime.AsTime()) { + } else if !gotTime.Equal(*tt.expectedTime) { t.Errorf("computeEligibleSince() gotTime = %v, want %v", *gotTime, tt.expectedTime) } } @@ -1479,7 +1473,7 @@ func TestGetPolicy_Local_NotFoundCases(t *testing.T) { branchName: "develop", policyToCreate: RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ - {Name: "feature", Since: fixedTime, TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2)}, + {Name: "feature", Since: timestamppb.New(fixedTime), TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel2)}, }, }, }, @@ -1623,7 +1617,7 @@ func TestGetPolicy_Remote_NotFoundCases(t *testing.T) { mockHTTPStatus: http.StatusOK, mockPolicyContent: &RepoPolicy{ ProtectedBranches: []*ProtectedBranch{ - {Name: "main", Since: fixedTime, TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3)}, + {Name: "main", Since: timestamppb.New(fixedTime), TargetSlsaSourceLevel: string(slsa.SlsaSourceLevel3)}, }, }, expectedPolicyPath: mockPolicyPath, diff --git a/pkg/slsa/controls.go b/pkg/slsa/controls.go new file mode 100644 index 0000000..f8b36ad --- /dev/null +++ b/pkg/slsa/controls.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors +// SPDX-License-Identifier: Apache-2.0 + +package slsa + +import "slices" + +type ( + ControlName string + ControlState string + + // ControlNameSet is a list of control names + ControlNameSet []ControlName +) + +func (c ControlName) String() string { + return string(c) +} + +const ( + // Control constants + DEPRECATED_ContinuityEnforced ControlName = "CONTINUITY_ENFORCED" + DEPRECATED_ProvenanceAvailable ControlName = "PROVENANCE_AVAILABLE" + DEPRECATED_ReviewEnforced ControlName = "REVIEW_ENFORCED" + DEPRECATED_TagHygiene ControlName = "TAG_HYGIENE" + + // Virtual control to manage policy lifcycle + PolicyAvailable ControlName = "POLICY_AVAILABLE" + + SLSA_SOURCE_ORG_SCS ControlName = "SLSA_SOURCE_ORG_SCS" + SLSA_SOURCE_ORG_ACCESS_CONTROL ControlName = "SLSA_SOURCE_ORG_ACCESS_CONTROL" + SLSA_SOURCE_ORG_SAFE_EXPUNGE ControlName = "SLSA_SOURCE_ORG_SAFE_EXPUNGE" + SLSA_SOURCE_ORG_CONTINUITY ControlName = "SLSA_SOURCE_ORG_CONTINUITY" + SLSA_SOURCE_SCS_REPO_ID ControlName = "SLSA_SOURCE_SCS_REPO_ID" + SLSA_SOURCE_SCS_REVISION_ID ControlName = "SLSA_SOURCE_SCS_REVISION_ID" + SLSA_SOURCE_SCS_DIFF_DISPLAY ControlName = "SLSA_SOURCE_SCS_DIFF_DISPLAY" + SLSA_SOURCE_SCS_VSA ControlName = "SLSA_SOURCE_SCS_VSA" + SLSA_SOURCE_SCS_HISTORY ControlName = "SLSA_SOURCE_SCS_HISTORY" + SLSA_SOURCE_SCS_CONTINUITY ControlName = "SLSA_SOURCE_SCS_CONTINUITY" + SLSA_SOURCE_SCS_IDENTITY ControlName = "SLSA_SOURCE_SCS_IDENTITY" + SLSA_SOURCE_SCS_PROVENANCE ControlName = "SLSA_SOURCE_SCS_PROVENANCE" + SLSA_SOURCE_SCS_PROTECTED_REFS ControlName = "SLSA_SOURCE_SCS_PROTECTED_REFS" + SLSA_SOURCE_SCS_TWO_PARTY_REVIEW ControlName = "SLSA_SOURCE_SCS_TWO_PARTY_REVIEW" + + // Control lifecycle states + StateNotEnabled ControlState = "not_enabled" + StateInProgress ControlState = "in_progress" + StateActive ControlState = "active" +) + +// AllLevelControls is a set holding all controls of the SLSA Source spec +var AllLevelControls = ControlNameSet{ + SLSA_SOURCE_ORG_SCS, + SLSA_SOURCE_ORG_ACCESS_CONTROL, + SLSA_SOURCE_ORG_SAFE_EXPUNGE, + SLSA_SOURCE_ORG_CONTINUITY, + SLSA_SOURCE_SCS_REPO_ID, + SLSA_SOURCE_SCS_REVISION_ID, + SLSA_SOURCE_SCS_DIFF_DISPLAY, + SLSA_SOURCE_SCS_VSA, + SLSA_SOURCE_SCS_HISTORY, + SLSA_SOURCE_SCS_CONTINUITY, + SLSA_SOURCE_SCS_IDENTITY, + SLSA_SOURCE_SCS_PROVENANCE, + SLSA_SOURCE_SCS_PROTECTED_REFS, + SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, +} + +func (cs ControlNameSet) GetControl(ctrl ControlName) ControlName { + if slices.Contains(cs, ctrl) { + return ctrl + } + return "" +} diff --git a/pkg/slsa/levels.go b/pkg/slsa/levels.go new file mode 100644 index 0000000..b7d6c86 --- /dev/null +++ b/pkg/slsa/levels.go @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: Copyright 2026 The SLSA Authors +// SPDX-License-Identifier: Apache-2.0 + +package slsa + +type ( + SlsaSourceLevel ControlName +) + +const ( + SlsaSourceLevel0 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_0" + SlsaSourceLevel1 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_1" + SlsaSourceLevel2 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_2" + SlsaSourceLevel3 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_3" + SlsaSourceLevel4 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_4" +) + +type Level struct { + Level uint8 + Controls ControlNameSet +} + +var Level0 = ControlNameSet{} + +var Level1 = ControlNameSet{ + SLSA_SOURCE_ORG_SCS, + SLSA_SOURCE_SCS_REPO_ID, + SLSA_SOURCE_SCS_REVISION_ID, + SLSA_SOURCE_SCS_DIFF_DISPLAY, + SLSA_SOURCE_SCS_VSA, +} + +var Level2 = ControlNameSet{ + SLSA_SOURCE_ORG_SCS, + SLSA_SOURCE_ORG_ACCESS_CONTROL, + SLSA_SOURCE_ORG_SAFE_EXPUNGE, + SLSA_SOURCE_SCS_REPO_ID, + SLSA_SOURCE_SCS_REVISION_ID, + SLSA_SOURCE_SCS_DIFF_DISPLAY, + SLSA_SOURCE_SCS_VSA, + SLSA_SOURCE_SCS_HISTORY, + SLSA_SOURCE_SCS_CONTINUITY, + SLSA_SOURCE_SCS_IDENTITY, + SLSA_SOURCE_SCS_PROVENANCE, +} + +var Level3 = ControlNameSet{ + SLSA_SOURCE_ORG_SCS, + SLSA_SOURCE_ORG_ACCESS_CONTROL, + SLSA_SOURCE_ORG_SAFE_EXPUNGE, + SLSA_SOURCE_ORG_CONTINUITY, + SLSA_SOURCE_SCS_REPO_ID, + SLSA_SOURCE_SCS_REVISION_ID, + SLSA_SOURCE_SCS_DIFF_DISPLAY, + SLSA_SOURCE_SCS_VSA, + SLSA_SOURCE_SCS_HISTORY, + SLSA_SOURCE_SCS_CONTINUITY, + SLSA_SOURCE_SCS_IDENTITY, + SLSA_SOURCE_SCS_PROVENANCE, + SLSA_SOURCE_SCS_PROTECTED_REFS, +} + +var Level4 = ControlNameSet{ + SLSA_SOURCE_ORG_SCS, + SLSA_SOURCE_ORG_ACCESS_CONTROL, + SLSA_SOURCE_ORG_SAFE_EXPUNGE, + SLSA_SOURCE_ORG_CONTINUITY, + SLSA_SOURCE_SCS_REPO_ID, + SLSA_SOURCE_SCS_REVISION_ID, + SLSA_SOURCE_SCS_DIFF_DISPLAY, + SLSA_SOURCE_SCS_VSA, + SLSA_SOURCE_SCS_HISTORY, + SLSA_SOURCE_SCS_CONTINUITY, + SLSA_SOURCE_SCS_IDENTITY, + SLSA_SOURCE_SCS_PROVENANCE, + SLSA_SOURCE_SCS_PROTECTED_REFS, + SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, +} diff --git a/pkg/slsa/slsa_types.go b/pkg/slsa/slsa_types.go index c705585..f785e84 100644 --- a/pkg/slsa/slsa_types.go +++ b/pkg/slsa/slsa_types.go @@ -12,41 +12,12 @@ import ( "github.com/slsa-framework/source-tool/pkg/provenance" ) -type ( - ControlName string - ControlState string - SlsaSourceLevel ControlName -) - -func (c ControlName) String() string { - return string(c) -} - const ( - SlsaSourceLevel1 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_1" - SlsaSourceLevel2 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_2" - SlsaSourceLevel3 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_3" - SlsaSourceLevel4 SlsaSourceLevel = "SLSA_SOURCE_LEVEL_4" - ContinuityEnforced ControlName = "CONTINUITY_ENFORCED" - ProvenanceAvailable ControlName = "PROVENANCE_AVAILABLE" - ReviewEnforced ControlName = "REVIEW_ENFORCED" - TagHygiene ControlName = "TAG_HYGIENE" - PolicyAvailable ControlName = "POLICY_AVAILABLE" - SourceBranchesAnnotation = "source_branches" - SourceRefsAnnotation = "source_refs" - AllowedOrgPropPrefix = "ORG_SOURCE_" - - // Control lifecycle states - StateNotEnabled ControlState = "not_enabled" - StateInProgress ControlState = "in_progress" - StateActive ControlState = "active" + SourceBranchesAnnotation = "source_branches" + SourceRefsAnnotation = "source_refs" + AllowedOrgPropPrefix = "ORG_SOURCE_" ) -// AllLevelControls lists all the SLSA controls managed by sourcetool -var AllLevelControls = []ControlName{ - ContinuityEnforced, ProvenanceAvailable, ReviewEnforced, TagHygiene, -} - func IsSlsaSourceLevel(control ControlName) bool { return slices.Contains( []ControlName{ @@ -64,64 +35,37 @@ func IsLevelHigherOrEqualTo(level1, level2 SlsaSourceLevel) bool { return level1 >= level2 } -type Controls []*provenance.Control - -// Adds the control to the list. Ignores nil controls. -// Does not check for duplicate controls. -func (controls *Controls) AddControl(newControls ...*provenance.Control) { - if controls == nil { - controls = &Controls{} - } - for _, c := range newControls { - if c == nil { - continue - } - *controls = append(*controls, c) - } -} - -// Gets the control with the corresponding name, returns nil if not found. -func (controls *Controls) GetControl(name ControlName) *provenance.Control { - for _, control := range *controls { - if control.GetName() == name.String() { - return control - } - } - return nil -} +// These can be any string, not just SlsaLevels +type SourceVerifiedLevels []ControlName -func (controls *Controls) AreControlsAvailable(names []ControlName) bool { - for _, name := range names { - if controls.GetControl(name) == nil { - return false +// Levels returns the contrlols that are only SLSA levels (ignoring others) +func (svl SourceVerifiedLevels) Levels() SourceVerifiedLevels { + ret := SourceVerifiedLevels{} + for _, c := range svl { + if c == ControlName(SlsaSourceLevel0) || + c == ControlName(SlsaSourceLevel1) || + c == ControlName(SlsaSourceLevel2) || + c == ControlName(SlsaSourceLevel3) || + c == ControlName(SlsaSourceLevel4) { + ret = append(ret, c) } } - return true -} - -// Returns the names of the controls. -func (controls *Controls) Names() []ControlName { - names := make([]ControlName, len(*controls)) - for i := range *controls { - names[i] = ControlName((*controls)[i].GetName()) - } - return names + return ret } -// These can be any string, not just SlsaLevels -type SourceVerifiedLevels []ControlName - // Returns the list of control names that must be set for the given slsa level. -func GetRequiredControlsForLevel(level SlsaSourceLevel) []ControlName { +func GetRequiredControlsForLevel(level SlsaSourceLevel) ControlNameSet { switch level { + case SlsaSourceLevel0: + return Level0 case SlsaSourceLevel1: - return []ControlName{} + return Level1 case SlsaSourceLevel2: - return []ControlName{ContinuityEnforced, TagHygiene} + return Level2 case SlsaSourceLevel3: - return []ControlName{ContinuityEnforced, TagHygiene, ProvenanceAvailable} + return Level3 case SlsaSourceLevel4: - return []ControlName{ContinuityEnforced, TagHygiene, ProvenanceAvailable, ReviewEnforced} + return Level4 default: return []ControlName{} } @@ -142,16 +86,32 @@ func ControlNamesToStrings(controlNames []ControlName) []string { return strs } +func NewControlSetFromProvanenaceControls(provControls []*provenance.Control) *ControlSet { + set := &ControlSet{ + Controls: []*Control{}, + } + + for _, ctl := range provControls { + t := ctl.GetSince().AsTime() + set.Controls = append(set.Controls, &Control{ + Name: ControlName(ctl.GetName()), + State: StateActive, + Since: &t, + }) + } + return set +} + // NewControlStatus returns a new control status object initialized with // all existing controls in not_enabled state. -func NewControlSetStatus() *ControlSetStatus { - status := &ControlSetStatus{ +func NewControlSet() *ControlSet { + status := &ControlSet{ Time: time.Now(), - Controls: []ControlStatus{}, + Controls: []*Control{}, } for _, c := range AllLevelControls { - status.Controls = append(status.Controls, ControlStatus{ + status.Controls = append(status.Controls, &Control{ Name: c, State: StateNotEnabled, }) @@ -160,17 +120,25 @@ func NewControlSetStatus() *ControlSetStatus { return status } -// ControlSetStatus is a snapshot of the status of SLSA controls in a branch at +// ControlSet is a snapshot of the status of SLSA controls in a branch at // a point in time. -type ControlSetStatus struct { - RepoUri string - Branch string - Time time.Time - Controls []ControlStatus -} - -// ControlStatus captures the status of a control as seen from a VCS system -type ControlStatus struct { +type ControlSet struct { + RepoUri string + Branch string + // The time we are observing the controls + Time time.Time + // The time the commit we're evaluating was pushed. + CommitPushTime time.Time + // The actor that pushed the commit. + ActorLogin string + // The type of activity that created the commit. + ActivityType string + // List of controls + Controls []*Control +} + +// Control captures the status of a control as seen from a VCS system +type Control struct { Name ControlName State ControlState `json:"control_state"` Since *time.Time `json:"since,omitempty"` @@ -178,6 +146,14 @@ type ControlStatus struct { RecommendedAction *ControlRecommendedAction } +func (cs *Control) GetName() ControlName { + return cs.Name +} + +func (cs *Control) GetSince() *time.Time { + return cs.Since +} + // ControlRecommendedAction captures the recommended action to complete // a control's implementation. type ControlRecommendedAction struct { @@ -187,23 +163,21 @@ type ControlRecommendedAction struct { // GetActiveControls returns a Controls collection with all the controls // which are active in the set. -func (cs *ControlSetStatus) GetActiveControls() *Controls { - ret := Controls{} +func (cs *ControlSet) GetActiveControls() *ControlSet { + ret := ControlSet{} if cs == nil { return &ret } for _, c := range cs.Controls { if c.State == StateActive { - ret.AddControl(&provenance.Control{ - Name: c.Name.String(), Since: timestamppb.New(*c.Since), - }) + ret.AddControl(c) } } return &ret } // SetControlState sets the state of a control in the set by name. -func (cs *ControlSetStatus) SetControlState(ctrlName ControlName, state ControlState) { +func (cs *ControlSet) SetControlState(ctrlName ControlName, state ControlState) { for i := range cs.Controls { if cs.Controls[i].Name == ctrlName { cs.Controls[i].State = state @@ -211,3 +185,65 @@ func (cs *ControlSetStatus) SetControlState(ctrlName ControlName, state ControlS } } } + +// Adds the control to the list. Ignores nil controls. +// Does not check for duplicate controls. +func (cs *ControlSet) AddControl(newControls ...*Control) { + if cs == nil { + cs = &ControlSet{} + } + for _, c := range newControls { + if c == nil { + continue + } + cs.Controls = append(cs.Controls, c) + } +} + +// Gets the control with the corresponding name, returns nil if not found. +func (cs *ControlSet) GetControl(name ControlName) *Control { + for _, control := range cs.Controls { + if control.GetName() == name { + return control + } + } + return nil +} + +// This checks if the controls are present in the array. But As we merged the +// controls array with the struct we also check if they are all active +func (cs *ControlSet) AreControlsAvailable(names []ControlName) bool { + for _, name := range names { + ctl := cs.GetControl(name) + if ctl == nil || ctl.State != StateActive { + return false + } + } + return true +} + +// Returns the names of the controls. +func (cs *ControlSet) Names() []ControlName { + names := make([]ControlName, len(cs.Controls)) + for i := range cs.Controls { + names[i] = cs.Controls[i].GetName() + } + return names +} + +func (cs *ControlSet) ToProvenanceControls() []*provenance.Control { + ret := []*provenance.Control{} + for _, ctl := range cs.Controls { + if ctl.State != StateActive { + continue + } + c := &provenance.Control{ + Name: ctl.Name.String(), + } + if ctl.Since != nil { + c.Since = timestamppb.New(*ctl.Since) + } + ret = append(ret, c) + } + return ret +} diff --git a/pkg/sourcetool/backends/attestation/notes/backend.go b/pkg/sourcetool/backends/attestation/notes/backend.go deleted file mode 100644 index 5caf0ec..0000000 --- a/pkg/sourcetool/backends/attestation/notes/backend.go +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 The SLSA Authors -// SPDX-License-Identifier: Apache-2.0 - -// Package notes implements an attestation storage backend that reads from -// git commit notes -package notes - -import ( - "context" - "fmt" - - vpb "github.com/in-toto/attestation/go/predicates/vsa/v1" - attestation "github.com/in-toto/attestation/go/v1" - - "github.com/slsa-framework/source-tool/pkg/attest" - "github.com/slsa-framework/source-tool/pkg/auth" - "github.com/slsa-framework/source-tool/pkg/ghcontrol" - "github.com/slsa-framework/source-tool/pkg/provenance" - "github.com/slsa-framework/source-tool/pkg/sourcetool/models" -) - -type Backend struct { - authenticator *auth.Authenticator -} - -func New() *Backend { - return &Backend{ - authenticator: auth.New(), - } -} - -// getGitHubConnection gets a github connector to the specified branch -func (b *Backend) getGitHubConnection(branch *models.Branch) (*ghcontrol.GitHubConnection, error) { - if branch.Repository == nil { - return nil, fmt.Errorf("branch does not have its repository set") - } - - client, err := b.authenticator.GetGitHubClient() - if err != nil { - return nil, fmt.Errorf("creating GitHub client: %w", err) - } - - repoOwner, repoName, err := branch.Repository.PathAsGitHubOwnerName() - if err != nil { - return nil, err - } - - return ghcontrol.NewGhConnectionWithClient(repoOwner, repoName, branch.FullRef(), client), nil -} - -// GetCommitVsa retrieves a VSA by looking into the specified commit notes -func (b *Backend) GetCommitVsa(ctx context.Context, branch *models.Branch, commit *models.Commit) (*attestation.Statement, *vpb.VerificationSummary, error) { - gcx, err := b.getGitHubConnection(branch) - if err != nil { - return nil, nil, err - } - statement, predicate, err := attest.GetVsa(ctx, gcx, attest.GetDefaultVerifier(), commit.SHA, branch.FullRef()) - if err != nil { - return nil, nil, fmt.Errorf("reading VSA: %w", err) - } - - return statement, predicate, nil -} - -// GetCommitProvenance gets the provenance attestation of a commit in a branch -func (b *Backend) GetCommitProvenance(ctx context.Context, branch *models.Branch, commit *models.Commit) (*attestation.Statement, *provenance.SourceProvenancePred, error) { - gcx, err := b.getGitHubConnection(branch) - if err != nil { - return nil, nil, err - } - - pa := attest.NewProvenanceAttestor(gcx, attest.GetDefaultVerifier()) - statement, predicate, err := pa.GetProvenance(ctx, commit.SHA, branch.FullRef()) - if err != nil { - return nil, nil, fmt.Errorf("reading provenance attestation: %w", err) - } - - return statement, predicate, nil -} diff --git a/pkg/sourcetool/backends/vcs/github/github.go b/pkg/sourcetool/backends/vcs/github/github.go index 02ddf3c..215cc8c 100644 --- a/pkg/sourcetool/backends/vcs/github/github.go +++ b/pkg/sourcetool/backends/vcs/github/github.go @@ -8,19 +8,68 @@ import ( "errors" "fmt" "log" + "time" "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/auth" "github.com/slsa-framework/source-tool/pkg/ghcontrol" - "github.com/slsa-framework/source-tool/pkg/provenance" "github.com/slsa-framework/source-tool/pkg/slsa" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) -func New() *Backend { +// InherentControls are the controls that are always true because we are +// in git and/org GitHub. +var InherentControls = slsa.ControlNameSet{ + // GitHub uses git + slsa.SLSA_SOURCE_ORG_SCS, + + // GitHub enforces access control + slsa.SLSA_SOURCE_ORG_ACCESS_CONTROL, + + // There is no safe expunge in Git or GitHub but as long as users + // cannot force push, then things cannot be expunged. So I think + // it passes. + // slsa.SLSA_SOURCE_ORG_SAFE_EXPUNGE, + + // All the org controls are default expect for expunge, so we tie + // org continuity to the branch continuity + // slsa.SLSA_SOURCE_ORG_CONTINUITY, + + // GitHub gives you a repo id/uri + slsa.SLSA_SOURCE_SCS_REPO_ID, + + // Git commit + slsa.SLSA_SOURCE_SCS_REVISION_ID, + + // git diff + slsa.SLSA_SOURCE_SCS_DIFF_DISPLAY, + + // We disable VSA until checking we have one + // slsa.SLSA_SOURCE_SCS_VSA, + + // Git is change history + slsa.SLSA_SOURCE_SCS_HISTORY, + + // We will compute continuity from the GH api + // slsa.SLSA_SOURCE_SCS_CONTINUITY, + + // Both git and GitHub have user identities + slsa.SLSA_SOURCE_SCS_IDENTITY, + + // We disable provenance until we check we have an attestation + // slsa.SLSA_SOURCE_SCS_PROVENANCE, + + // This depends on branch protection + // slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, + + // We will check for two party review in the API + // slsa.SLSA_SOURCE_SCS_TWO_PARTY_REVIEW, +} + +func New(options *models.BackendOptions) *Backend { return &Backend{ authenticator: auth.New(), - Options: Options{UseFork: true}, + Options: options, } } @@ -31,7 +80,7 @@ type Options struct { // Backend implemets the GitHub sourcetool backend type Backend struct { authenticator *auth.Authenticator - Options Options + Options *models.BackendOptions } // getGitHubConnection builds a github connector to a repository @@ -54,10 +103,15 @@ func (b *Backend) getGitHubConnection(repository *models.Repository, ref string) return nil, err } - return ghcontrol.NewGhConnectionWithClient(owner, name, ref, client), nil + ghc := ghcontrol.NewGhConnectionWithClient(owner, name, ref, client) + ghc.Options.AllowMergeCommits = b.Options.AllowMergeCommits + return ghc, nil } -func (b *Backend) GetBranchControls(ctx context.Context, r *models.Repository, branch *models.Branch) (*slsa.ControlSetStatus, error) { +func (b *Backend) GetBranchControls(ctx context.Context, branch *models.Branch) (*slsa.ControlSet, error) { + if branch.Repository == nil { + return nil, fmt.Errorf("branch has no repository") + } // get latest commit ghc, err := b.getGitHubConnection(branch.Repository, branch.FullRef()) if err != nil { @@ -70,20 +124,26 @@ func (b *Backend) GetBranchControls(ctx context.Context, r *models.Repository, b return nil, fmt.Errorf("fetching latest commit from %q: %w", branch.FullRef(), err) } - return b.GetBranchControlsAtCommit(ctx, r, branch, &models.Commit{SHA: commit}) + return b.GetBranchControlsAtCommit(ctx, branch, &models.Commit{SHA: commit}) } // GetBranchControlsAtCommit -func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repository, branch *models.Branch, commit *models.Commit) (*slsa.ControlSetStatus, error) { +func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*slsa.ControlSet, error) { + if branch.Repository == nil { + return nil, fmt.Errorf("branch has no repository") + } + if commit == nil { return nil, errors.New("commit is not set") } + ghc, err := b.getGitHubConnection(branch.Repository, branch.FullRef()) if err != nil { return nil, fmt.Errorf("getting github connection: %w", err) } - // Get the active controls + // The branch controls returned from ghcontrol only include the 4 + // legacy checks sourcetool did (continuity, review, RequiredChecks, tag hygiene) activeControls, err := ghc.GetBranchControls(ctx, branch.FullRef()) if err != nil { return nil, fmt.Errorf("checking status: %w", err) @@ -91,30 +151,101 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos // We need to manually check for PROVENANCE_AVAILABLE which is not // handled by ghcontrol - attestor := attest.NewProvenanceAttestor( - ghc, attest.GetDefaultVerifier(), + attester, err := attest.NewAttester( + attest.WithBackend(b), attest.WithVerifier(attest.GetDefaultVerifier()), + attest.WithAuthenticator(b.authenticator), ) + if err != nil { + return nil, err + } // Fetch the attestation. If found, then add the control: - attestation, _, err := attestor.GetProvenance(ctx, commit.SHA, branch.FullRef()) + attestation, err := attester.GetRevisionProvenance(ctx, branch, commit) if err != nil { return nil, fmt.Errorf("attempting to read provenance from commit %q: %w", commit.SHA, err) } if attestation != nil { - activeControls.AddControl(&provenance.Control{ - Name: slsa.ProvenanceAvailable.String(), - }) + // We carry over the since date from the previous attestation, only if + // it > 0 (unix origin) + var t *time.Time + if ctrl := attestation.GetControl(slsa.SLSA_SOURCE_SCS_PROVENANCE.String()); ctrl != nil { + if ctrl.GetSince() != nil && ctrl.GetSince().AsTime().Unix() != 0 { + rt := ctrl.GetSince().AsTime() + t = &rt + } + } else if ctrl := attestation.GetControl(slsa.DEPRECATED_ProvenanceAvailable.String()); ctrl != nil { + if ctrl.GetSince() != nil && ctrl.GetSince().AsTime().Unix() != 0 { + rt := ctrl.GetSince().AsTime() + t = &rt + } + } + + if tc := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_PROVENANCE); tc != nil { + if t != nil { + tc.Since = t + } + } else { + activeControls.AddControl(&slsa.Control{ + Name: slsa.SLSA_SOURCE_SCS_PROVENANCE, + Since: t, + }) + } } else { log.Printf("No provenance attestation found on %s", commit.SHA) } - status := slsa.NewControlSetStatus() + _, vsaPred, err := attester.GetRevisionVSA(ctx, branch, commit) + if err != nil { + return nil, fmt.Errorf("reading VSA: %w", err) + } + + if vsaPred != nil { + if tc := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_VSA); tc == nil { + activeControls.AddControl(&slsa.Control{ + Name: slsa.SLSA_SOURCE_SCS_VSA, + }) + } + } else { + log.Printf("No VSA found for commit") + } + + // NewControlSet returns all the controls for the framework in + // StateNotEnabled. + status := slsa.NewControlSet() + sinceForever := time.Unix(1207836000, 0) // April 10, 2008 (when github came online) for i, ctrl := range status.Controls { + // Check if it's an inherent control, turn it on and don't look back + if c := InherentControls.GetControl(slsa.ControlName(ctrl.Name.String())); c != "" { + status.Controls[i].Since = &sinceForever + status.Controls[i].State = slsa.StateActive + status.Controls[i].Message = "Inherent" + continue + } + + // Check if it's one of the active controls and enable it and copy the since date if c := activeControls.GetControl(ctrl.Name); c != nil { - t := c.GetSince().AsTime() - status.Controls[i].Since = &t + status.Controls[i].Since = c.Since status.Controls[i].State = slsa.StateActive - status.Controls[i].Message = b.controlImplementationMessage(slsa.ControlName(c.GetName())) + status.Controls[i].Message = b.controlImplementationMessage(c.GetName()) + } + + // Enable ORG_SAFE_EXPUNGE when branch protection (protected refs) is active. + // Without force push, content cannot be expunged. + if ctrl.Name == slsa.SLSA_SOURCE_ORG_SAFE_EXPUNGE { + if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS); c != nil { + status.Controls[i].Since = c.Since + status.Controls[i].State = slsa.StateActive + status.Controls[i].Message = b.controlImplementationMessage(ctrl.Name) + } + } + + // Enable ORG_CONTINUITY when SCS branch continuity is active. + if ctrl.Name == slsa.SLSA_SOURCE_ORG_CONTINUITY { + if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_CONTINUITY); c != nil { + status.Controls[i].Since = c.Since + status.Controls[i].State = slsa.StateActive + status.Controls[i].Message = b.controlImplementationMessage(ctrl.Name) + } } } @@ -123,8 +254,8 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos // here and report back the status. switchProvCtlToInProgress := false var provenanceMessage string - if c := activeControls.GetControl(slsa.ProvenanceAvailable); c == nil { - pr, err := b.FindWorkflowPR(ctx, r) + if c := activeControls.GetControl(slsa.SLSA_SOURCE_SCS_PROVENANCE); c == nil { + pr, err := b.FindWorkflowPR(ctx, branch.Repository) if err != nil { return nil, fmt.Errorf("looking for provenance workflow pull request: %w", err) } @@ -139,11 +270,11 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos // Populate the recommended actions for i := range status.Controls { // Piggyback on this loop to switch the provenance status for efficiency - if switchProvCtlToInProgress && status.Controls[i].Name == slsa.ProvenanceAvailable { + if switchProvCtlToInProgress && status.Controls[i].Name == slsa.SLSA_SOURCE_SCS_PROVENANCE { status.Controls[i].State = slsa.StateInProgress status.Controls[i].Message = provenanceMessage } - action := b.getRecommendedAction(r, branch, status.Controls[i].Name, status.Controls[i].State) + action := b.getRecommendedAction(branch.Repository, branch, status.Controls[i].Name, status.Controls[i].State) status.Controls[i].RecommendedAction = action } @@ -153,14 +284,15 @@ func (b *Backend) GetBranchControlsAtCommit(ctx context.Context, r *models.Repos // controlImplementationMessage returns an implementation message to populate the // status message when controls are active. func (b *Backend) controlImplementationMessage(ctrlName slsa.ControlName) string { + //nolint:exhaustive switch ctrlName { - case slsa.ProvenanceAvailable: + case slsa.SLSA_SOURCE_SCS_PROVENANCE, slsa.DEPRECATED_ProvenanceAvailable: return "Signed provenance metadata is being published on every commit" - case slsa.TagHygiene: + case slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, slsa.DEPRECATED_TagHygiene: return "Tag protections are configured in the repository" - case slsa.ReviewEnforced: + case slsa.DEPRECATED_ReviewEnforced: return "Code review is enforced in the repository" - case slsa.ContinuityEnforced: + case slsa.SLSA_SOURCE_ORG_CONTINUITY, slsa.DEPRECATED_ContinuityEnforced: return "Push and delete protection is enabled on the branch" case slsa.PolicyAvailable: return "The repository has published a policy" @@ -169,8 +301,11 @@ func (b *Backend) controlImplementationMessage(ctrlName slsa.ControlName) string } } -func (b *Backend) GetTagControls(context.Context, *models.Tag) (*slsa.Controls, error) { - return nil, fmt.Errorf("not yet implemented") +func (b *Backend) GetTagControls(ctx context.Context, branch *models.Branch, tag *models.Tag) (*slsa.ControlSet, error) { + if tag.Commit == nil || tag.Commit.SHA == "" { + return nil, errors.New("tag commit is empty") + } + return b.GetBranchControlsAtCommit(ctx, branch, tag.Commit) } func (b *Backend) ControlConfigurationDescr(branch *models.Branch, config models.ControlConfiguration) string { @@ -229,7 +364,7 @@ func (b *Backend) GetLatestCommit(ctx context.Context, r *models.Repository, bra func (b *Backend) getRecommendedAction(r *models.Repository, _ *models.Branch, control slsa.ControlName, state slsa.ControlState) *slsa.ControlRecommendedAction { //nolint:exhaustive // Not all drivers handle all controls switch control { - case slsa.ProvenanceAvailable: + case slsa.DEPRECATED_ProvenanceAvailable: switch state { case slsa.StateInProgress: return &slsa.ControlRecommendedAction{ @@ -243,7 +378,7 @@ func (b *Backend) getRecommendedAction(r *models.Repository, _ *models.Branch, c default: return nil } - case slsa.ContinuityEnforced: + case slsa.SLSA_SOURCE_ORG_CONTINUITY, slsa.DEPRECATED_ContinuityEnforced: if state == slsa.StateNotEnabled { return &slsa.ControlRecommendedAction{ Message: "Enable branch push/delete protection", @@ -251,7 +386,7 @@ func (b *Backend) getRecommendedAction(r *models.Repository, _ *models.Branch, c } } return nil - case slsa.TagHygiene: + case slsa.SLSA_SOURCE_SCS_PROTECTED_REFS, slsa.DEPRECATED_TagHygiene: if state == slsa.StateNotEnabled { return &slsa.ControlRecommendedAction{ Message: "Enable tag push/update/delete protection", @@ -263,3 +398,60 @@ func (b *Backend) getRecommendedAction(r *models.Repository, _ *models.Branch, c return nil } } + +// GetPreviousCommit takes a commit in +func (b *Backend) GetPreviousCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*models.Commit, error) { + ghx, err := b.getGitHubConnection(branch.Repository, branch.FullRef()) + if err != nil { + return nil, err + } + // TODO:IMPLEMENT + rawCommit, err := ghx.GetPriorCommit(ctx, commit.SHA) + if err != nil { + return nil, fmt.Errorf("fetching previous commit: %w", err) + } + return &models.Commit{ + SHA: rawCommit, + }, nil +} + +// GetDefaultBranch returns the default branch +func (b *Backend) GetDefaultBranch(ctx context.Context, repo *models.Repository) (*models.Branch, error) { + ghx, err := b.getGitHubConnection(repo, "") + if err != nil { + return nil, err + } + branchName, err := ghx.GetDefaultBranch(ctx) + if err != nil { + return nil, fmt.Errorf("fetching default branch: %w", err) + } + + return &models.Branch{ + Name: branchName, + Repository: repo, + }, nil +} + +// GetRevisionCommit returns the commit of a revision (or error) +func (b *Backend) GetRevisionCommit(ctx context.Context, repo *models.Repository, rev models.Revision) (*models.Commit, error) { + switch inst := rev.(type) { + case *models.Commit: + return inst, nil + case *models.Tag: + if inst.Commit == nil { + ghx, err := b.getGitHubConnection(repo, "") + if err != nil { + return nil, err + } + tagCommit, err := ghx.GetTagCommit(ctx, inst.GetName()) + if err != nil { + return nil, err + } + + inst.Commit = tagCommit + } + return inst.Commit, nil + default: + return nil, errors.New("not implemented yet or invalid revision") + } +} diff --git a/pkg/sourcetool/backends/vcs/github/manage.go b/pkg/sourcetool/backends/vcs/github/manage.go index 8f9e408..362e7a9 100644 --- a/pkg/sourcetool/backends/vcs/github/manage.go +++ b/pkg/sourcetool/backends/vcs/github/manage.go @@ -18,10 +18,10 @@ import ( ) const ( - ActionsOrg = "slsa-framework" - ActionsRepo = "source-actions" - workflowPath = ".github/workflows/compute_slsa_source.yaml" - workflowSource = "git+https://github.com/slsa-" + ActionsOrg = "slsa-framework" + ActionsRepo = "source-actions" + workflowPath = ".github/workflows/compute_slsa_source.yaml" + // workflowSource = "git+https://github.com/slsa-" // workflowCommitMessage will be used as the commit message and the PR title workflowCommitMessage = "Add SLSA Source Provenance Workflow" diff --git a/pkg/sourcetool/implementation.go b/pkg/sourcetool/implementation.go index ecc519c..9c7f763 100644 --- a/pkg/sourcetool/implementation.go +++ b/pkg/sourcetool/implementation.go @@ -20,8 +20,6 @@ import ( "github.com/slsa-framework/source-tool/pkg/repo" roptions "github.com/slsa-framework/source-tool/pkg/repo/options" "github.com/slsa-framework/source-tool/pkg/slsa" - "github.com/slsa-framework/source-tool/pkg/sourcetool/backends/attestation/notes" - ghbackend "github.com/slsa-framework/source-tool/pkg/sourcetool/backends/vcs/github" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" "github.com/slsa-framework/source-tool/pkg/sourcetool/options" ) @@ -35,11 +33,10 @@ type toolImplementation interface { CreatePolicyPR(*auth.Authenticator, *options.Options, *models.Repository, *policy.RepoPolicy) (*models.PullRequest, error) CheckForks(*options.Options) error SearchPullRequest(context.Context, *auth.Authenticator, *models.Repository, string) (*models.PullRequest, error) - GetVcsBackend(*models.Repository) (models.VcsBackend, error) - GetAttestationReader(*models.Repository) (models.AttestationStorageReader, error) - GetBranchControls(context.Context, models.VcsBackend, *models.Repository, *models.Branch) (*slsa.ControlSetStatus, error) + GetBranchControls(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSet, error) + GetBranchControlsAtCommit(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSet, error) ConfigureControls(models.VcsBackend, *models.Repository, []*models.Branch, []models.ControlConfiguration) error - GetPolicyStatus(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.ControlStatus, error) + GetPolicyStatus(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.Control, error) CreateRepositoryFork(context.Context, *auth.Authenticator, *models.Repository, string) error } @@ -53,21 +50,15 @@ func (impl *defaultToolImplementation) ConfigureControls( } func (impl *defaultToolImplementation) GetBranchControls( - ctx context.Context, backend models.VcsBackend, r *models.Repository, branch *models.Branch, -) (*slsa.ControlSetStatus, error) { - return backend.GetBranchControls(ctx, r, branch) + ctx context.Context, backend models.VcsBackend, branch *models.Branch, +) (*slsa.ControlSet, error) { + return backend.GetBranchControls(ctx, branch) } -// GetAttestationReader returns the att reader object -func (impl *defaultToolImplementation) GetAttestationReader(_ *models.Repository) (models.AttestationStorageReader, error) { - // We only have the notes backend for now - return notes.New(), nil -} - -// GetVcsBackend returns the VCS backend to handle the repository defined in the options -func (impl *defaultToolImplementation) GetVcsBackend(*models.Repository) (models.VcsBackend, error) { - // for now we only support github, so there - return ghbackend.New(), nil +func (impl *defaultToolImplementation) GetBranchControlsAtCommit( + ctx context.Context, backend models.VcsBackend, branch *models.Branch, commit *models.Commit, +) (*slsa.ControlSet, error) { + return backend.GetBranchControlsAtCommit(ctx, branch, commit) } // VerifyOptions checks options are in good shape to run @@ -225,7 +216,7 @@ func (impl *defaultToolImplementation) SearchPullRequest(ctx context.Context, a // GetPolicyStatus returns the status of the policy as a slsa ControlStatus func (impl *defaultToolImplementation) GetPolicyStatus( ctx context.Context, a *auth.Authenticator, opts *options.Options, r *models.Repository, -) (*slsa.ControlStatus, error) { +) (*slsa.Control, error) { // First: Look for the policy. If found then we are done pcy, _, err := policy.NewPolicyEvaluator().GetPolicy(ctx, r) if err != nil { @@ -237,7 +228,7 @@ func (impl *defaultToolImplementation) GetPolicyStatus( if len(pcy.GetProtectedBranches()) > 0 { t = pcy.GetProtectedBranches()[0].GetSince().AsTime() } - return &slsa.ControlStatus{ + return &slsa.Control{ Name: slsa.PolicyAvailable, State: slsa.StateActive, Since: &t, @@ -269,7 +260,7 @@ func (impl *defaultToolImplementation) GetPolicyStatus( // No pull request found. Not implemented if prNr == nil { - return &slsa.ControlStatus{ + return &slsa.Control{ Name: slsa.PolicyAvailable, State: slsa.StateNotEnabled, Since: nil, @@ -281,7 +272,7 @@ func (impl *defaultToolImplementation) GetPolicyStatus( }, nil } - return &slsa.ControlStatus{ + return &slsa.Control{ Name: slsa.PolicyAvailable, State: slsa.StateInProgress, Since: prNr.Time, diff --git a/pkg/sourcetool/models/models.go b/pkg/sourcetool/models/models.go index bc11404..06b9246 100644 --- a/pkg/sourcetool/models/models.go +++ b/pkg/sourcetool/models/models.go @@ -39,13 +39,21 @@ type AttestationStorageReader interface { // //counterfeiter:generate . VcsBackend type VcsBackend interface { - GetBranchControls(context.Context, *Repository, *Branch) (*slsa.ControlSetStatus, error) - GetBranchControlsAtCommit(context.Context, *Repository, *Branch, *Commit) (*slsa.ControlSetStatus, error) - GetTagControls(context.Context, *Tag) (*slsa.Controls, error) + GetBranchControls(context.Context, *Branch) (*slsa.ControlSet, error) + GetBranchControlsAtCommit(context.Context, *Branch, *Commit) (*slsa.ControlSet, error) + GetTagControls(context.Context, *Branch, *Tag) (*slsa.ControlSet, error) ControlConfigurationDescr(*Branch, ControlConfiguration) string ConfigureControls(*Repository, []*Branch, []ControlConfiguration) error GetLatestCommit(context.Context, *Repository, *Branch) (*Commit, error) ControlPrecheck(*Repository, []*Branch, ControlConfiguration) (bool, string, ControlPreRemediationFn, error) + GetPreviousCommit(context.Context, *Branch, *Commit) (*Commit, error) + GetDefaultBranch(context.Context, *Repository) (*Branch, error) + GetRevisionCommit(context.Context, *Repository, Revision) (*Commit, error) +} + +type BackendOptions struct { + AllowMergeCommits bool + DriverOptions any } // ControlPreRemediation is a function returned by the VCS backends @@ -68,11 +76,49 @@ type Commit struct { Message string } +// Both tags and branches must implement the reference interface +var ( + _ Reference = (*Branch)(nil) + _ Reference = (*Tag)(nil) + _ Revision = (*Tag)(nil) + _ Revision = (*Commit)(nil) +) + +type Revision interface { + GetCommit() *Commit +} + +type Reference interface { + FullRef() string + GetRepository() *Repository + GetName() string +} + +func (c *Commit) GetCommit() *Commit { + return c +} + +func (c *Commit) ToResourceDescriptor() *attestation.ResourceDescriptor { + return &attestation.ResourceDescriptor{ + Digest: map[string]string{ + "sha1": c.SHA, "gitCommit": c.SHA, + }, + } +} + type Branch struct { Name string Repository *Repository } +func (b *Branch) GetRepository() *Repository { + return b.Repository +} + +func (b *Branch) GetName() string { + return b.Name +} + func (b *Branch) FullRef() string { return fmt.Sprintf("refs/heads/%s", b.Name) } @@ -88,9 +134,6 @@ func (r *Repository) GetHttpURL() string { return "" } u := fmt.Sprintf("https://%s/%s", r.Hostname, r.Path) - if r.Hostname == "github.com" { - u += ".git" - } return u } @@ -114,8 +157,28 @@ func (r *Repository) PathAsGitHubOwnerName() (owner, name string, err error) { } type Tag struct { - Name string - Commit *Commit + Name string + Commit *Commit + Repository *Repository +} + +func (t *Tag) GetName() string { + return t.Name +} + +func (t *Tag) GetRepository() *Repository { + return t.Repository +} + +func (t *Tag) GetCommit() *Commit { + return t.Commit +} + +func (t *Tag) FullRef() string { + if t.Name == "" { + return "" + } + return "refs/tags/" + t.Name } // PullRequest models a GitHub pull request. diff --git a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go index 8528ebd..3740caf 100644 --- a/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go +++ b/pkg/sourcetool/models/modelsfakes/fake_vcs_backend.go @@ -54,35 +54,47 @@ type FakeVcsBackend struct { result3 models.ControlPreRemediationFn result4 error } - GetBranchControlsStub func(context.Context, *models.Repository, *models.Branch) (*slsa.ControlSetStatus, error) + GetBranchControlsStub func(context.Context, *models.Branch) (*slsa.ControlSet, error) getBranchControlsMutex sync.RWMutex getBranchControlsArgsForCall []struct { arg1 context.Context - arg2 *models.Repository - arg3 *models.Branch + arg2 *models.Branch } getBranchControlsReturns struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } getBranchControlsReturnsOnCall map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } - GetBranchControlsAtCommitStub func(context.Context, *models.Repository, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error) + GetBranchControlsAtCommitStub func(context.Context, *models.Branch, *models.Commit) (*slsa.ControlSet, error) getBranchControlsAtCommitMutex sync.RWMutex getBranchControlsAtCommitArgsForCall []struct { arg1 context.Context - arg2 *models.Repository - arg3 *models.Branch - arg4 *models.Commit + arg2 *models.Branch + arg3 *models.Commit } getBranchControlsAtCommitReturns struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error } getBranchControlsAtCommitReturnsOnCall map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet + result2 error + } + GetDefaultBranchStub func(context.Context, *models.Repository) (*models.Branch, error) + getDefaultBranchMutex sync.RWMutex + getDefaultBranchArgsForCall []struct { + arg1 context.Context + arg2 *models.Repository + } + getDefaultBranchReturns struct { + result1 *models.Branch + result2 error + } + getDefaultBranchReturnsOnCall map[int]struct { + result1 *models.Branch result2 error } GetLatestCommitStub func(context.Context, *models.Repository, *models.Branch) (*models.Commit, error) @@ -100,18 +112,49 @@ type FakeVcsBackend struct { result1 *models.Commit result2 error } - GetTagControlsStub func(context.Context, *models.Tag) (*slsa.Controls, error) + GetPreviousCommitStub func(context.Context, *models.Branch, *models.Commit) (*models.Commit, error) + getPreviousCommitMutex sync.RWMutex + getPreviousCommitArgsForCall []struct { + arg1 context.Context + arg2 *models.Branch + arg3 *models.Commit + } + getPreviousCommitReturns struct { + result1 *models.Commit + result2 error + } + getPreviousCommitReturnsOnCall map[int]struct { + result1 *models.Commit + result2 error + } + GetRevisionCommitStub func(context.Context, *models.Repository, models.Revision) (*models.Commit, error) + getRevisionCommitMutex sync.RWMutex + getRevisionCommitArgsForCall []struct { + arg1 context.Context + arg2 *models.Repository + arg3 models.Revision + } + getRevisionCommitReturns struct { + result1 *models.Commit + result2 error + } + getRevisionCommitReturnsOnCall map[int]struct { + result1 *models.Commit + result2 error + } + GetTagControlsStub func(context.Context, *models.Branch, *models.Tag) (*slsa.ControlSet, error) getTagControlsMutex sync.RWMutex getTagControlsArgsForCall []struct { arg1 context.Context - arg2 *models.Tag + arg2 *models.Branch + arg3 *models.Tag } getTagControlsReturns struct { - result1 *slsa.Controls + result1 *slsa.ControlSet result2 error } getTagControlsReturnsOnCall map[int]struct { - result1 *slsa.Controls + result1 *slsa.ControlSet result2 error } invocations map[string][][]interface{} @@ -330,20 +373,19 @@ func (fake *FakeVcsBackend) ControlPrecheckReturnsOnCall(i int, result1 bool, re }{result1, result2, result3, result4} } -func (fake *FakeVcsBackend) GetBranchControls(arg1 context.Context, arg2 *models.Repository, arg3 *models.Branch) (*slsa.ControlSetStatus, error) { +func (fake *FakeVcsBackend) GetBranchControls(arg1 context.Context, arg2 *models.Branch) (*slsa.ControlSet, error) { fake.getBranchControlsMutex.Lock() ret, specificReturn := fake.getBranchControlsReturnsOnCall[len(fake.getBranchControlsArgsForCall)] fake.getBranchControlsArgsForCall = append(fake.getBranchControlsArgsForCall, struct { arg1 context.Context - arg2 *models.Repository - arg3 *models.Branch - }{arg1, arg2, arg3}) + arg2 *models.Branch + }{arg1, arg2}) stub := fake.GetBranchControlsStub fakeReturns := fake.getBranchControlsReturns - fake.recordInvocation("GetBranchControls", []interface{}{arg1, arg2, arg3}) + fake.recordInvocation("GetBranchControls", []interface{}{arg1, arg2}) fake.getBranchControlsMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3) + return stub(arg1, arg2) } if specificReturn { return ret.result1, ret.result2 @@ -357,60 +399,59 @@ func (fake *FakeVcsBackend) GetBranchControlsCallCount() int { return len(fake.getBranchControlsArgsForCall) } -func (fake *FakeVcsBackend) GetBranchControlsCalls(stub func(context.Context, *models.Repository, *models.Branch) (*slsa.ControlSetStatus, error)) { +func (fake *FakeVcsBackend) GetBranchControlsCalls(stub func(context.Context, *models.Branch) (*slsa.ControlSet, error)) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = stub } -func (fake *FakeVcsBackend) GetBranchControlsArgsForCall(i int) (context.Context, *models.Repository, *models.Branch) { +func (fake *FakeVcsBackend) GetBranchControlsArgsForCall(i int) (context.Context, *models.Branch) { fake.getBranchControlsMutex.RLock() defer fake.getBranchControlsMutex.RUnlock() argsForCall := fake.getBranchControlsArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 + return argsForCall.arg1, argsForCall.arg2 } -func (fake *FakeVcsBackend) GetBranchControlsReturns(result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeVcsBackend) GetBranchControlsReturns(result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = nil fake.getBranchControlsReturns = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeVcsBackend) GetBranchControlsReturnsOnCall(i int, result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeVcsBackend) GetBranchControlsReturnsOnCall(i int, result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsMutex.Lock() defer fake.getBranchControlsMutex.Unlock() fake.GetBranchControlsStub = nil if fake.getBranchControlsReturnsOnCall == nil { fake.getBranchControlsReturnsOnCall = make(map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }) } fake.getBranchControlsReturnsOnCall[i] = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeVcsBackend) GetBranchControlsAtCommit(arg1 context.Context, arg2 *models.Repository, arg3 *models.Branch, arg4 *models.Commit) (*slsa.ControlSetStatus, error) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommit(arg1 context.Context, arg2 *models.Branch, arg3 *models.Commit) (*slsa.ControlSet, error) { fake.getBranchControlsAtCommitMutex.Lock() ret, specificReturn := fake.getBranchControlsAtCommitReturnsOnCall[len(fake.getBranchControlsAtCommitArgsForCall)] fake.getBranchControlsAtCommitArgsForCall = append(fake.getBranchControlsAtCommitArgsForCall, struct { arg1 context.Context - arg2 *models.Repository - arg3 *models.Branch - arg4 *models.Commit - }{arg1, arg2, arg3, arg4}) + arg2 *models.Branch + arg3 *models.Commit + }{arg1, arg2, arg3}) stub := fake.GetBranchControlsAtCommitStub fakeReturns := fake.getBranchControlsAtCommitReturns - fake.recordInvocation("GetBranchControlsAtCommit", []interface{}{arg1, arg2, arg3, arg4}) + fake.recordInvocation("GetBranchControlsAtCommit", []interface{}{arg1, arg2, arg3}) fake.getBranchControlsAtCommitMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1, ret.result2 @@ -424,41 +465,106 @@ func (fake *FakeVcsBackend) GetBranchControlsAtCommitCallCount() int { return len(fake.getBranchControlsAtCommitArgsForCall) } -func (fake *FakeVcsBackend) GetBranchControlsAtCommitCalls(stub func(context.Context, *models.Repository, *models.Branch, *models.Commit) (*slsa.ControlSetStatus, error)) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommitCalls(stub func(context.Context, *models.Branch, *models.Commit) (*slsa.ControlSet, error)) { fake.getBranchControlsAtCommitMutex.Lock() defer fake.getBranchControlsAtCommitMutex.Unlock() fake.GetBranchControlsAtCommitStub = stub } -func (fake *FakeVcsBackend) GetBranchControlsAtCommitArgsForCall(i int) (context.Context, *models.Repository, *models.Branch, *models.Commit) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommitArgsForCall(i int) (context.Context, *models.Branch, *models.Commit) { fake.getBranchControlsAtCommitMutex.RLock() defer fake.getBranchControlsAtCommitMutex.RUnlock() argsForCall := fake.getBranchControlsAtCommitArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } -func (fake *FakeVcsBackend) GetBranchControlsAtCommitReturns(result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommitReturns(result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsAtCommitMutex.Lock() defer fake.getBranchControlsAtCommitMutex.Unlock() fake.GetBranchControlsAtCommitStub = nil fake.getBranchControlsAtCommitReturns = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeVcsBackend) GetBranchControlsAtCommitReturnsOnCall(i int, result1 *slsa.ControlSetStatus, result2 error) { +func (fake *FakeVcsBackend) GetBranchControlsAtCommitReturnsOnCall(i int, result1 *slsa.ControlSet, result2 error) { fake.getBranchControlsAtCommitMutex.Lock() defer fake.getBranchControlsAtCommitMutex.Unlock() fake.GetBranchControlsAtCommitStub = nil if fake.getBranchControlsAtCommitReturnsOnCall == nil { fake.getBranchControlsAtCommitReturnsOnCall = make(map[int]struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet result2 error }) } fake.getBranchControlsAtCommitReturnsOnCall[i] = struct { - result1 *slsa.ControlSetStatus + result1 *slsa.ControlSet + result2 error + }{result1, result2} +} + +func (fake *FakeVcsBackend) GetDefaultBranch(arg1 context.Context, arg2 *models.Repository) (*models.Branch, error) { + fake.getDefaultBranchMutex.Lock() + ret, specificReturn := fake.getDefaultBranchReturnsOnCall[len(fake.getDefaultBranchArgsForCall)] + fake.getDefaultBranchArgsForCall = append(fake.getDefaultBranchArgsForCall, struct { + arg1 context.Context + arg2 *models.Repository + }{arg1, arg2}) + stub := fake.GetDefaultBranchStub + fakeReturns := fake.getDefaultBranchReturns + fake.recordInvocation("GetDefaultBranch", []interface{}{arg1, arg2}) + fake.getDefaultBranchMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVcsBackend) GetDefaultBranchCallCount() int { + fake.getDefaultBranchMutex.RLock() + defer fake.getDefaultBranchMutex.RUnlock() + return len(fake.getDefaultBranchArgsForCall) +} + +func (fake *FakeVcsBackend) GetDefaultBranchCalls(stub func(context.Context, *models.Repository) (*models.Branch, error)) { + fake.getDefaultBranchMutex.Lock() + defer fake.getDefaultBranchMutex.Unlock() + fake.GetDefaultBranchStub = stub +} + +func (fake *FakeVcsBackend) GetDefaultBranchArgsForCall(i int) (context.Context, *models.Repository) { + fake.getDefaultBranchMutex.RLock() + defer fake.getDefaultBranchMutex.RUnlock() + argsForCall := fake.getDefaultBranchArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeVcsBackend) GetDefaultBranchReturns(result1 *models.Branch, result2 error) { + fake.getDefaultBranchMutex.Lock() + defer fake.getDefaultBranchMutex.Unlock() + fake.GetDefaultBranchStub = nil + fake.getDefaultBranchReturns = struct { + result1 *models.Branch + result2 error + }{result1, result2} +} + +func (fake *FakeVcsBackend) GetDefaultBranchReturnsOnCall(i int, result1 *models.Branch, result2 error) { + fake.getDefaultBranchMutex.Lock() + defer fake.getDefaultBranchMutex.Unlock() + fake.GetDefaultBranchStub = nil + if fake.getDefaultBranchReturnsOnCall == nil { + fake.getDefaultBranchReturnsOnCall = make(map[int]struct { + result1 *models.Branch + result2 error + }) + } + fake.getDefaultBranchReturnsOnCall[i] = struct { + result1 *models.Branch result2 error }{result1, result2} } @@ -529,19 +635,152 @@ func (fake *FakeVcsBackend) GetLatestCommitReturnsOnCall(i int, result1 *models. }{result1, result2} } -func (fake *FakeVcsBackend) GetTagControls(arg1 context.Context, arg2 *models.Tag) (*slsa.Controls, error) { +func (fake *FakeVcsBackend) GetPreviousCommit(arg1 context.Context, arg2 *models.Branch, arg3 *models.Commit) (*models.Commit, error) { + fake.getPreviousCommitMutex.Lock() + ret, specificReturn := fake.getPreviousCommitReturnsOnCall[len(fake.getPreviousCommitArgsForCall)] + fake.getPreviousCommitArgsForCall = append(fake.getPreviousCommitArgsForCall, struct { + arg1 context.Context + arg2 *models.Branch + arg3 *models.Commit + }{arg1, arg2, arg3}) + stub := fake.GetPreviousCommitStub + fakeReturns := fake.getPreviousCommitReturns + fake.recordInvocation("GetPreviousCommit", []interface{}{arg1, arg2, arg3}) + fake.getPreviousCommitMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVcsBackend) GetPreviousCommitCallCount() int { + fake.getPreviousCommitMutex.RLock() + defer fake.getPreviousCommitMutex.RUnlock() + return len(fake.getPreviousCommitArgsForCall) +} + +func (fake *FakeVcsBackend) GetPreviousCommitCalls(stub func(context.Context, *models.Branch, *models.Commit) (*models.Commit, error)) { + fake.getPreviousCommitMutex.Lock() + defer fake.getPreviousCommitMutex.Unlock() + fake.GetPreviousCommitStub = stub +} + +func (fake *FakeVcsBackend) GetPreviousCommitArgsForCall(i int) (context.Context, *models.Branch, *models.Commit) { + fake.getPreviousCommitMutex.RLock() + defer fake.getPreviousCommitMutex.RUnlock() + argsForCall := fake.getPreviousCommitArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVcsBackend) GetPreviousCommitReturns(result1 *models.Commit, result2 error) { + fake.getPreviousCommitMutex.Lock() + defer fake.getPreviousCommitMutex.Unlock() + fake.GetPreviousCommitStub = nil + fake.getPreviousCommitReturns = struct { + result1 *models.Commit + result2 error + }{result1, result2} +} + +func (fake *FakeVcsBackend) GetPreviousCommitReturnsOnCall(i int, result1 *models.Commit, result2 error) { + fake.getPreviousCommitMutex.Lock() + defer fake.getPreviousCommitMutex.Unlock() + fake.GetPreviousCommitStub = nil + if fake.getPreviousCommitReturnsOnCall == nil { + fake.getPreviousCommitReturnsOnCall = make(map[int]struct { + result1 *models.Commit + result2 error + }) + } + fake.getPreviousCommitReturnsOnCall[i] = struct { + result1 *models.Commit + result2 error + }{result1, result2} +} + +func (fake *FakeVcsBackend) GetRevisionCommit(arg1 context.Context, arg2 *models.Repository, arg3 models.Revision) (*models.Commit, error) { + fake.getRevisionCommitMutex.Lock() + ret, specificReturn := fake.getRevisionCommitReturnsOnCall[len(fake.getRevisionCommitArgsForCall)] + fake.getRevisionCommitArgsForCall = append(fake.getRevisionCommitArgsForCall, struct { + arg1 context.Context + arg2 *models.Repository + arg3 models.Revision + }{arg1, arg2, arg3}) + stub := fake.GetRevisionCommitStub + fakeReturns := fake.getRevisionCommitReturns + fake.recordInvocation("GetRevisionCommit", []interface{}{arg1, arg2, arg3}) + fake.getRevisionCommitMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeVcsBackend) GetRevisionCommitCallCount() int { + fake.getRevisionCommitMutex.RLock() + defer fake.getRevisionCommitMutex.RUnlock() + return len(fake.getRevisionCommitArgsForCall) +} + +func (fake *FakeVcsBackend) GetRevisionCommitCalls(stub func(context.Context, *models.Repository, models.Revision) (*models.Commit, error)) { + fake.getRevisionCommitMutex.Lock() + defer fake.getRevisionCommitMutex.Unlock() + fake.GetRevisionCommitStub = stub +} + +func (fake *FakeVcsBackend) GetRevisionCommitArgsForCall(i int) (context.Context, *models.Repository, models.Revision) { + fake.getRevisionCommitMutex.RLock() + defer fake.getRevisionCommitMutex.RUnlock() + argsForCall := fake.getRevisionCommitArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeVcsBackend) GetRevisionCommitReturns(result1 *models.Commit, result2 error) { + fake.getRevisionCommitMutex.Lock() + defer fake.getRevisionCommitMutex.Unlock() + fake.GetRevisionCommitStub = nil + fake.getRevisionCommitReturns = struct { + result1 *models.Commit + result2 error + }{result1, result2} +} + +func (fake *FakeVcsBackend) GetRevisionCommitReturnsOnCall(i int, result1 *models.Commit, result2 error) { + fake.getRevisionCommitMutex.Lock() + defer fake.getRevisionCommitMutex.Unlock() + fake.GetRevisionCommitStub = nil + if fake.getRevisionCommitReturnsOnCall == nil { + fake.getRevisionCommitReturnsOnCall = make(map[int]struct { + result1 *models.Commit + result2 error + }) + } + fake.getRevisionCommitReturnsOnCall[i] = struct { + result1 *models.Commit + result2 error + }{result1, result2} +} + +func (fake *FakeVcsBackend) GetTagControls(arg1 context.Context, arg2 *models.Branch, arg3 *models.Tag) (*slsa.ControlSet, error) { fake.getTagControlsMutex.Lock() ret, specificReturn := fake.getTagControlsReturnsOnCall[len(fake.getTagControlsArgsForCall)] fake.getTagControlsArgsForCall = append(fake.getTagControlsArgsForCall, struct { arg1 context.Context - arg2 *models.Tag - }{arg1, arg2}) + arg2 *models.Branch + arg3 *models.Tag + }{arg1, arg2, arg3}) stub := fake.GetTagControlsStub fakeReturns := fake.getTagControlsReturns - fake.recordInvocation("GetTagControls", []interface{}{arg1, arg2}) + fake.recordInvocation("GetTagControls", []interface{}{arg1, arg2, arg3}) fake.getTagControlsMutex.Unlock() if stub != nil { - return stub(arg1, arg2) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1, ret.result2 @@ -555,41 +794,41 @@ func (fake *FakeVcsBackend) GetTagControlsCallCount() int { return len(fake.getTagControlsArgsForCall) } -func (fake *FakeVcsBackend) GetTagControlsCalls(stub func(context.Context, *models.Tag) (*slsa.Controls, error)) { +func (fake *FakeVcsBackend) GetTagControlsCalls(stub func(context.Context, *models.Branch, *models.Tag) (*slsa.ControlSet, error)) { fake.getTagControlsMutex.Lock() defer fake.getTagControlsMutex.Unlock() fake.GetTagControlsStub = stub } -func (fake *FakeVcsBackend) GetTagControlsArgsForCall(i int) (context.Context, *models.Tag) { +func (fake *FakeVcsBackend) GetTagControlsArgsForCall(i int) (context.Context, *models.Branch, *models.Tag) { fake.getTagControlsMutex.RLock() defer fake.getTagControlsMutex.RUnlock() argsForCall := fake.getTagControlsArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } -func (fake *FakeVcsBackend) GetTagControlsReturns(result1 *slsa.Controls, result2 error) { +func (fake *FakeVcsBackend) GetTagControlsReturns(result1 *slsa.ControlSet, result2 error) { fake.getTagControlsMutex.Lock() defer fake.getTagControlsMutex.Unlock() fake.GetTagControlsStub = nil fake.getTagControlsReturns = struct { - result1 *slsa.Controls + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeVcsBackend) GetTagControlsReturnsOnCall(i int, result1 *slsa.Controls, result2 error) { +func (fake *FakeVcsBackend) GetTagControlsReturnsOnCall(i int, result1 *slsa.ControlSet, result2 error) { fake.getTagControlsMutex.Lock() defer fake.getTagControlsMutex.Unlock() fake.GetTagControlsStub = nil if fake.getTagControlsReturnsOnCall == nil { fake.getTagControlsReturnsOnCall = make(map[int]struct { - result1 *slsa.Controls + result1 *slsa.ControlSet result2 error }) } fake.getTagControlsReturnsOnCall[i] = struct { - result1 *slsa.Controls + result1 *slsa.ControlSet result2 error }{result1, result2} } diff --git a/pkg/sourcetool/options.go b/pkg/sourcetool/options.go index ff5a09a..94c7d62 100644 --- a/pkg/sourcetool/options.go +++ b/pkg/sourcetool/options.go @@ -11,6 +11,41 @@ import ( type ConfigFn func(*Tool) error +func WithGithubCollector(yesno bool) ConfigFn { + return func(t *Tool) error { + t.Options.InitGHCollector = yesno + return nil + } +} + +func WithGithubStorer(yesno bool) ConfigFn { + return func(t *Tool) error { + t.Options.InitGHStorer = yesno + return nil + } +} + +func WithNotesCollector(yesno bool) ConfigFn { + return func(t *Tool) error { + t.Options.InitNotesCollector = yesno + return nil + } +} + +func WithNotesStorer(yesno bool) ConfigFn { + return func(t *Tool) error { + t.Options.InitNotesStorer = yesno + return nil + } +} + +func WithStorageLocation(l ...string) ConfigFn { + return func(t *Tool) error { + t.Options.StorageLocations = l + return nil + } +} + func WithAuthenticator(a *auth.Authenticator) ConfigFn { return func(t *Tool) error { if a == nil { @@ -48,3 +83,10 @@ func WithPolicyRepo(slug string) ConfigFn { return nil } } + +func WithAllowMergeCommits(allow bool) ConfigFn { + return func(t *Tool) error { + t.Options.AllowMergeCommits = allow + return nil + } +} diff --git a/pkg/sourcetool/options/options.go b/pkg/sourcetool/options/options.go index 26e1617..d506c42 100644 --- a/pkg/sourcetool/options/options.go +++ b/pkg/sourcetool/options/options.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/slsa-framework/source-tool/pkg/policy" + "github.com/slsa-framework/source-tool/pkg/sourcetool/models" ) type Options struct { @@ -20,11 +21,25 @@ type Options struct { // PolicyRepo is the repository where the policies are stored PolicyRepo string + + // Initialize GitHub attestations storer and fetcher + InitGHCollector bool + InitGHStorer bool + + // Initialize Dynamic notes storer and fetcher + InitNotesCollector bool + InitNotesStorer bool + + StorageLocations []string + + models.BackendOptions } // DefaultOptions holds the default options the tool initializes with var Default = Options{ - PolicyRepo: fmt.Sprintf("%s/%s", policy.SourcePolicyRepoOwner, policy.SourcePolicyRepo), - UseSSH: true, - CreatePolicyPR: true, + PolicyRepo: fmt.Sprintf("%s/%s", policy.SourcePolicyRepoOwner, policy.SourcePolicyRepo), + UseSSH: true, + CreatePolicyPR: true, + InitNotesCollector: true, + BackendOptions: models.BackendOptions{}, } diff --git a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go index d6e6457..0d38f3d 100644 --- a/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go +++ b/pkg/sourcetool/sourcetoolfakes/fake_tool_implementation.go @@ -79,36 +79,38 @@ type FakeToolImplementation struct { createRepositoryForkReturnsOnCall map[int]struct { result1 error } - GetAttestationReaderStub func(*models.Repository) (models.AttestationStorageReader, error) - getAttestationReaderMutex sync.RWMutex - getAttestationReaderArgsForCall []struct { - arg1 *models.Repository + GetBranchControlsStub func(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSet, error) + getBranchControlsMutex sync.RWMutex + getBranchControlsArgsForCall []struct { + arg1 context.Context + arg2 models.VcsBackend + arg3 *models.Branch } - getAttestationReaderReturns struct { - result1 models.AttestationStorageReader + getBranchControlsReturns struct { + result1 *slsa.ControlSet result2 error } - getAttestationReaderReturnsOnCall map[int]struct { - result1 models.AttestationStorageReader + getBranchControlsReturnsOnCall map[int]struct { + result1 *slsa.ControlSet result2 error } - GetBranchControlsStub func(context.Context, models.VcsBackend, *models.Repository, *models.Branch) (*slsa.ControlSetStatus, error) - getBranchControlsMutex sync.RWMutex - getBranchControlsArgsForCall []struct { + GetBranchControlsAtCommitStub func(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSet, error) + getBranchControlsAtCommitMutex sync.RWMutex + getBranchControlsAtCommitArgsForCall []struct { arg1 context.Context arg2 models.VcsBackend - arg3 *models.Repository - arg4 *models.Branch + arg3 *models.Branch + arg4 *models.Commit } - getBranchControlsReturns struct { - result1 *slsa.ControlSetStatus + getBranchControlsAtCommitReturns struct { + result1 *slsa.ControlSet result2 error } - getBranchControlsReturnsOnCall map[int]struct { - result1 *slsa.ControlSetStatus + getBranchControlsAtCommitReturnsOnCall map[int]struct { + result1 *slsa.ControlSet result2 error } - GetPolicyStatusStub func(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.ControlStatus, error) + GetPolicyStatusStub func(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.Control, error) getPolicyStatusMutex sync.RWMutex getPolicyStatusArgsForCall []struct { arg1 context.Context @@ -117,24 +119,11 @@ type FakeToolImplementation struct { arg4 *models.Repository } getPolicyStatusReturns struct { - result1 *slsa.ControlStatus + result1 *slsa.Control result2 error } getPolicyStatusReturnsOnCall map[int]struct { - result1 *slsa.ControlStatus - result2 error - } - GetVcsBackendStub func(*models.Repository) (models.VcsBackend, error) - getVcsBackendMutex sync.RWMutex - getVcsBackendArgsForCall []struct { - arg1 *models.Repository - } - getVcsBackendReturns struct { - result1 models.VcsBackend - result2 error - } - getVcsBackendReturnsOnCall map[int]struct { - result1 models.VcsBackend + result1 *slsa.Control result2 error } SearchPullRequestStub func(context.Context, *auth.Authenticator, *models.Repository, string) (*models.PullRequest, error) @@ -496,18 +485,20 @@ func (fake *FakeToolImplementation) CreateRepositoryForkReturnsOnCall(i int, res }{result1} } -func (fake *FakeToolImplementation) GetAttestationReader(arg1 *models.Repository) (models.AttestationStorageReader, error) { - fake.getAttestationReaderMutex.Lock() - ret, specificReturn := fake.getAttestationReaderReturnsOnCall[len(fake.getAttestationReaderArgsForCall)] - fake.getAttestationReaderArgsForCall = append(fake.getAttestationReaderArgsForCall, struct { - arg1 *models.Repository - }{arg1}) - stub := fake.GetAttestationReaderStub - fakeReturns := fake.getAttestationReaderReturns - fake.recordInvocation("GetAttestationReader", []interface{}{arg1}) - fake.getAttestationReaderMutex.Unlock() +func (fake *FakeToolImplementation) GetBranchControls(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Branch) (*slsa.ControlSet, error) { + fake.getBranchControlsMutex.Lock() + ret, specificReturn := fake.getBranchControlsReturnsOnCall[len(fake.getBranchControlsArgsForCall)] + fake.getBranchControlsArgsForCall = append(fake.getBranchControlsArgsForCall, struct { + arg1 context.Context + arg2 models.VcsBackend + arg3 *models.Branch + }{arg1, arg2, arg3}) + stub := fake.GetBranchControlsStub + fakeReturns := fake.getBranchControlsReturns + fake.recordInvocation("GetBranchControls", []interface{}{arg1, arg2, arg3}) + fake.getBranchControlsMutex.Unlock() if stub != nil { - return stub(arg1) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1, ret.result2 @@ -515,64 +506,64 @@ func (fake *FakeToolImplementation) GetAttestationReader(arg1 *models.Repository return fakeReturns.result1, fakeReturns.result2 } -func (fake *FakeToolImplementation) GetAttestationReaderCallCount() int { - fake.getAttestationReaderMutex.RLock() - defer fake.getAttestationReaderMutex.RUnlock() - return len(fake.getAttestationReaderArgsForCall) +func (fake *FakeToolImplementation) GetBranchControlsCallCount() int { + fake.getBranchControlsMutex.RLock() + defer fake.getBranchControlsMutex.RUnlock() + return len(fake.getBranchControlsArgsForCall) } -func (fake *FakeToolImplementation) GetAttestationReaderCalls(stub func(*models.Repository) (models.AttestationStorageReader, error)) { - fake.getAttestationReaderMutex.Lock() - defer fake.getAttestationReaderMutex.Unlock() - fake.GetAttestationReaderStub = stub +func (fake *FakeToolImplementation) GetBranchControlsCalls(stub func(context.Context, models.VcsBackend, *models.Branch) (*slsa.ControlSet, error)) { + fake.getBranchControlsMutex.Lock() + defer fake.getBranchControlsMutex.Unlock() + fake.GetBranchControlsStub = stub } -func (fake *FakeToolImplementation) GetAttestationReaderArgsForCall(i int) *models.Repository { - fake.getAttestationReaderMutex.RLock() - defer fake.getAttestationReaderMutex.RUnlock() - argsForCall := fake.getAttestationReaderArgsForCall[i] - return argsForCall.arg1 +func (fake *FakeToolImplementation) GetBranchControlsArgsForCall(i int) (context.Context, models.VcsBackend, *models.Branch) { + fake.getBranchControlsMutex.RLock() + defer fake.getBranchControlsMutex.RUnlock() + argsForCall := fake.getBranchControlsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } -func (fake *FakeToolImplementation) GetAttestationReaderReturns(result1 models.AttestationStorageReader, result2 error) { - fake.getAttestationReaderMutex.Lock() - defer fake.getAttestationReaderMutex.Unlock() - fake.GetAttestationReaderStub = nil - fake.getAttestationReaderReturns = struct { - result1 models.AttestationStorageReader +func (fake *FakeToolImplementation) GetBranchControlsReturns(result1 *slsa.ControlSet, result2 error) { + fake.getBranchControlsMutex.Lock() + defer fake.getBranchControlsMutex.Unlock() + fake.GetBranchControlsStub = nil + fake.getBranchControlsReturns = struct { + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeToolImplementation) GetAttestationReaderReturnsOnCall(i int, result1 models.AttestationStorageReader, result2 error) { - fake.getAttestationReaderMutex.Lock() - defer fake.getAttestationReaderMutex.Unlock() - fake.GetAttestationReaderStub = nil - if fake.getAttestationReaderReturnsOnCall == nil { - fake.getAttestationReaderReturnsOnCall = make(map[int]struct { - result1 models.AttestationStorageReader +func (fake *FakeToolImplementation) GetBranchControlsReturnsOnCall(i int, result1 *slsa.ControlSet, result2 error) { + fake.getBranchControlsMutex.Lock() + defer fake.getBranchControlsMutex.Unlock() + fake.GetBranchControlsStub = nil + if fake.getBranchControlsReturnsOnCall == nil { + fake.getBranchControlsReturnsOnCall = make(map[int]struct { + result1 *slsa.ControlSet result2 error }) } - fake.getAttestationReaderReturnsOnCall[i] = struct { - result1 models.AttestationStorageReader + fake.getBranchControlsReturnsOnCall[i] = struct { + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeToolImplementation) GetBranchControls(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Repository, arg4 *models.Branch) (*slsa.ControlSetStatus, error) { - fake.getBranchControlsMutex.Lock() - ret, specificReturn := fake.getBranchControlsReturnsOnCall[len(fake.getBranchControlsArgsForCall)] - fake.getBranchControlsArgsForCall = append(fake.getBranchControlsArgsForCall, struct { +func (fake *FakeToolImplementation) GetBranchControlsAtCommit(arg1 context.Context, arg2 models.VcsBackend, arg3 *models.Branch, arg4 *models.Commit) (*slsa.ControlSet, error) { + fake.getBranchControlsAtCommitMutex.Lock() + ret, specificReturn := fake.getBranchControlsAtCommitReturnsOnCall[len(fake.getBranchControlsAtCommitArgsForCall)] + fake.getBranchControlsAtCommitArgsForCall = append(fake.getBranchControlsAtCommitArgsForCall, struct { arg1 context.Context arg2 models.VcsBackend - arg3 *models.Repository - arg4 *models.Branch + arg3 *models.Branch + arg4 *models.Commit }{arg1, arg2, arg3, arg4}) - stub := fake.GetBranchControlsStub - fakeReturns := fake.getBranchControlsReturns - fake.recordInvocation("GetBranchControls", []interface{}{arg1, arg2, arg3, arg4}) - fake.getBranchControlsMutex.Unlock() + stub := fake.GetBranchControlsAtCommitStub + fakeReturns := fake.getBranchControlsAtCommitReturns + fake.recordInvocation("GetBranchControlsAtCommit", []interface{}{arg1, arg2, arg3, arg4}) + fake.getBranchControlsAtCommitMutex.Unlock() if stub != nil { return stub(arg1, arg2, arg3, arg4) } @@ -582,52 +573,52 @@ func (fake *FakeToolImplementation) GetBranchControls(arg1 context.Context, arg2 return fakeReturns.result1, fakeReturns.result2 } -func (fake *FakeToolImplementation) GetBranchControlsCallCount() int { - fake.getBranchControlsMutex.RLock() - defer fake.getBranchControlsMutex.RUnlock() - return len(fake.getBranchControlsArgsForCall) +func (fake *FakeToolImplementation) GetBranchControlsAtCommitCallCount() int { + fake.getBranchControlsAtCommitMutex.RLock() + defer fake.getBranchControlsAtCommitMutex.RUnlock() + return len(fake.getBranchControlsAtCommitArgsForCall) } -func (fake *FakeToolImplementation) GetBranchControlsCalls(stub func(context.Context, models.VcsBackend, *models.Repository, *models.Branch) (*slsa.ControlSetStatus, error)) { - fake.getBranchControlsMutex.Lock() - defer fake.getBranchControlsMutex.Unlock() - fake.GetBranchControlsStub = stub +func (fake *FakeToolImplementation) GetBranchControlsAtCommitCalls(stub func(context.Context, models.VcsBackend, *models.Branch, *models.Commit) (*slsa.ControlSet, error)) { + fake.getBranchControlsAtCommitMutex.Lock() + defer fake.getBranchControlsAtCommitMutex.Unlock() + fake.GetBranchControlsAtCommitStub = stub } -func (fake *FakeToolImplementation) GetBranchControlsArgsForCall(i int) (context.Context, models.VcsBackend, *models.Repository, *models.Branch) { - fake.getBranchControlsMutex.RLock() - defer fake.getBranchControlsMutex.RUnlock() - argsForCall := fake.getBranchControlsArgsForCall[i] +func (fake *FakeToolImplementation) GetBranchControlsAtCommitArgsForCall(i int) (context.Context, models.VcsBackend, *models.Branch, *models.Commit) { + fake.getBranchControlsAtCommitMutex.RLock() + defer fake.getBranchControlsAtCommitMutex.RUnlock() + argsForCall := fake.getBranchControlsAtCommitArgsForCall[i] return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 } -func (fake *FakeToolImplementation) GetBranchControlsReturns(result1 *slsa.ControlSetStatus, result2 error) { - fake.getBranchControlsMutex.Lock() - defer fake.getBranchControlsMutex.Unlock() - fake.GetBranchControlsStub = nil - fake.getBranchControlsReturns = struct { - result1 *slsa.ControlSetStatus +func (fake *FakeToolImplementation) GetBranchControlsAtCommitReturns(result1 *slsa.ControlSet, result2 error) { + fake.getBranchControlsAtCommitMutex.Lock() + defer fake.getBranchControlsAtCommitMutex.Unlock() + fake.GetBranchControlsAtCommitStub = nil + fake.getBranchControlsAtCommitReturns = struct { + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeToolImplementation) GetBranchControlsReturnsOnCall(i int, result1 *slsa.ControlSetStatus, result2 error) { - fake.getBranchControlsMutex.Lock() - defer fake.getBranchControlsMutex.Unlock() - fake.GetBranchControlsStub = nil - if fake.getBranchControlsReturnsOnCall == nil { - fake.getBranchControlsReturnsOnCall = make(map[int]struct { - result1 *slsa.ControlSetStatus +func (fake *FakeToolImplementation) GetBranchControlsAtCommitReturnsOnCall(i int, result1 *slsa.ControlSet, result2 error) { + fake.getBranchControlsAtCommitMutex.Lock() + defer fake.getBranchControlsAtCommitMutex.Unlock() + fake.GetBranchControlsAtCommitStub = nil + if fake.getBranchControlsAtCommitReturnsOnCall == nil { + fake.getBranchControlsAtCommitReturnsOnCall = make(map[int]struct { + result1 *slsa.ControlSet result2 error }) } - fake.getBranchControlsReturnsOnCall[i] = struct { - result1 *slsa.ControlSetStatus + fake.getBranchControlsAtCommitReturnsOnCall[i] = struct { + result1 *slsa.ControlSet result2 error }{result1, result2} } -func (fake *FakeToolImplementation) GetPolicyStatus(arg1 context.Context, arg2 *auth.Authenticator, arg3 *options.Options, arg4 *models.Repository) (*slsa.ControlStatus, error) { +func (fake *FakeToolImplementation) GetPolicyStatus(arg1 context.Context, arg2 *auth.Authenticator, arg3 *options.Options, arg4 *models.Repository) (*slsa.Control, error) { fake.getPolicyStatusMutex.Lock() ret, specificReturn := fake.getPolicyStatusReturnsOnCall[len(fake.getPolicyStatusArgsForCall)] fake.getPolicyStatusArgsForCall = append(fake.getPolicyStatusArgsForCall, struct { @@ -655,7 +646,7 @@ func (fake *FakeToolImplementation) GetPolicyStatusCallCount() int { return len(fake.getPolicyStatusArgsForCall) } -func (fake *FakeToolImplementation) GetPolicyStatusCalls(stub func(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.ControlStatus, error)) { +func (fake *FakeToolImplementation) GetPolicyStatusCalls(stub func(context.Context, *auth.Authenticator, *options.Options, *models.Repository) (*slsa.Control, error)) { fake.getPolicyStatusMutex.Lock() defer fake.getPolicyStatusMutex.Unlock() fake.GetPolicyStatusStub = stub @@ -668,92 +659,28 @@ func (fake *FakeToolImplementation) GetPolicyStatusArgsForCall(i int) (context.C return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 } -func (fake *FakeToolImplementation) GetPolicyStatusReturns(result1 *slsa.ControlStatus, result2 error) { +func (fake *FakeToolImplementation) GetPolicyStatusReturns(result1 *slsa.Control, result2 error) { fake.getPolicyStatusMutex.Lock() defer fake.getPolicyStatusMutex.Unlock() fake.GetPolicyStatusStub = nil fake.getPolicyStatusReturns = struct { - result1 *slsa.ControlStatus + result1 *slsa.Control result2 error }{result1, result2} } -func (fake *FakeToolImplementation) GetPolicyStatusReturnsOnCall(i int, result1 *slsa.ControlStatus, result2 error) { +func (fake *FakeToolImplementation) GetPolicyStatusReturnsOnCall(i int, result1 *slsa.Control, result2 error) { fake.getPolicyStatusMutex.Lock() defer fake.getPolicyStatusMutex.Unlock() fake.GetPolicyStatusStub = nil if fake.getPolicyStatusReturnsOnCall == nil { fake.getPolicyStatusReturnsOnCall = make(map[int]struct { - result1 *slsa.ControlStatus + result1 *slsa.Control result2 error }) } fake.getPolicyStatusReturnsOnCall[i] = struct { - result1 *slsa.ControlStatus - result2 error - }{result1, result2} -} - -func (fake *FakeToolImplementation) GetVcsBackend(arg1 *models.Repository) (models.VcsBackend, error) { - fake.getVcsBackendMutex.Lock() - ret, specificReturn := fake.getVcsBackendReturnsOnCall[len(fake.getVcsBackendArgsForCall)] - fake.getVcsBackendArgsForCall = append(fake.getVcsBackendArgsForCall, struct { - arg1 *models.Repository - }{arg1}) - stub := fake.GetVcsBackendStub - fakeReturns := fake.getVcsBackendReturns - fake.recordInvocation("GetVcsBackend", []interface{}{arg1}) - fake.getVcsBackendMutex.Unlock() - if stub != nil { - return stub(arg1) - } - if specificReturn { - return ret.result1, ret.result2 - } - return fakeReturns.result1, fakeReturns.result2 -} - -func (fake *FakeToolImplementation) GetVcsBackendCallCount() int { - fake.getVcsBackendMutex.RLock() - defer fake.getVcsBackendMutex.RUnlock() - return len(fake.getVcsBackendArgsForCall) -} - -func (fake *FakeToolImplementation) GetVcsBackendCalls(stub func(*models.Repository) (models.VcsBackend, error)) { - fake.getVcsBackendMutex.Lock() - defer fake.getVcsBackendMutex.Unlock() - fake.GetVcsBackendStub = stub -} - -func (fake *FakeToolImplementation) GetVcsBackendArgsForCall(i int) *models.Repository { - fake.getVcsBackendMutex.RLock() - defer fake.getVcsBackendMutex.RUnlock() - argsForCall := fake.getVcsBackendArgsForCall[i] - return argsForCall.arg1 -} - -func (fake *FakeToolImplementation) GetVcsBackendReturns(result1 models.VcsBackend, result2 error) { - fake.getVcsBackendMutex.Lock() - defer fake.getVcsBackendMutex.Unlock() - fake.GetVcsBackendStub = nil - fake.getVcsBackendReturns = struct { - result1 models.VcsBackend - result2 error - }{result1, result2} -} - -func (fake *FakeToolImplementation) GetVcsBackendReturnsOnCall(i int, result1 models.VcsBackend, result2 error) { - fake.getVcsBackendMutex.Lock() - defer fake.getVcsBackendMutex.Unlock() - fake.GetVcsBackendStub = nil - if fake.getVcsBackendReturnsOnCall == nil { - fake.getVcsBackendReturnsOnCall = make(map[int]struct { - result1 models.VcsBackend - result2 error - }) - } - fake.getVcsBackendReturnsOnCall[i] = struct { - result1 models.VcsBackend + result1 *slsa.Control result2 error }{result1, result2} } diff --git a/pkg/sourcetool/tool.go b/pkg/sourcetool/tool.go index f7970ae..039b12c 100644 --- a/pkg/sourcetool/tool.go +++ b/pkg/sourcetool/tool.go @@ -9,15 +9,22 @@ import ( "context" "errors" "fmt" + "os" "slices" "strings" "time" + "github.com/carabiner-dev/collector" + cgithub "github.com/carabiner-dev/collector/repository/github" + "github.com/carabiner-dev/collector/repository/note" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/slsa-framework/source-tool/pkg/attest" "github.com/slsa-framework/source-tool/pkg/auth" "github.com/slsa-framework/source-tool/pkg/policy" "github.com/slsa-framework/source-tool/pkg/slsa" + "github.com/slsa-framework/source-tool/pkg/sourcetool/backends/vcs/github" "github.com/slsa-framework/source-tool/pkg/sourcetool/models" "github.com/slsa-framework/source-tool/pkg/sourcetool/options" ) @@ -39,6 +46,22 @@ func New(funcs ...ConfigFn) (*Tool, error) { } } + t.backend = github.New(&t.Options.BackendOptions) + + // Create the tool's attester + attester, err := attest.NewAttester( + attest.WithVerifier(attest.GetDefaultVerifier()), + attest.WithBackend(t.backend), + attest.WithGithubCollector(t.Options.InitGHCollector), + attest.WithNotesCollector(t.Options.InitNotesCollector), + attest.WithAuthenticator(t.Authenticator), + ) + if err != nil { + return nil, fmt.Errorf("creating attester: %w", err) + } + + t.attester = attester + return t, nil } @@ -47,31 +70,56 @@ func New(funcs ...ConfigFn) (*Tool, error) { // we want to slowly move it to public function under this struct. type Tool struct { Authenticator *auth.Authenticator + attester *attest.Attester + backend models.VcsBackend Options options.Options impl toolImplementation } // GetRepoControls returns the controls that are enabled in a repository branch. -func (t *Tool) GetBranchControls(ctx context.Context, r *models.Repository, branch *models.Branch) (*slsa.ControlSetStatus, error) { - backend, err := t.impl.GetVcsBackend(r) +func (t *Tool) GetBranchControls(ctx context.Context, branch *models.Branch) (*slsa.ControlSet, error) { + if branch.Repository == nil { + return nil, fmt.Errorf("repositoryu not specified in branch") + } + + // Get the control status in the branch. Backends are expected to + // return the full SLSA Source control catalog + controls, err := t.impl.GetBranchControls(ctx, t.backend, branch) if err != nil { - return nil, fmt.Errorf("getting VCS backend: %w", err) + return nil, fmt.Errorf("getting branch controls: %w", err) + } + + // We also abstract the repository policy as a control to report its status + status, err := t.impl.GetPolicyStatus(ctx, t.Authenticator, &t.Options, branch.Repository) + if err != nil { + return nil, fmt.Errorf("reading policy status: %w", err) + } + + controls.Controls = append(controls.Controls, status) + + return controls, err +} + +// GetRepoControls returns the controls that are enabled in a repository branch. +func (t *Tool) GetBranchControlsAtCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*slsa.ControlSet, error) { + if branch.Repository == nil { + return nil, fmt.Errorf("repositoryu not specified in branch") } // Get the control status in the branch. Backends are expected to // return the full SLSA Source control catalog - controls, err := t.impl.GetBranchControls(ctx, backend, r, branch) + controls, err := t.impl.GetBranchControlsAtCommit(ctx, t.backend, branch, commit) if err != nil { return nil, fmt.Errorf("getting branch controls: %w", err) } // We also abstract the repository policy as a control to report its status - status, err := t.impl.GetPolicyStatus(ctx, t.Authenticator, &t.Options, r) + status, err := t.impl.GetPolicyStatus(ctx, t.Authenticator, &t.Options, branch.Repository) if err != nil { return nil, fmt.Errorf("reading policy status: %w", err) } - controls.Controls = append(controls.Controls, *status) + controls.Controls = append(controls.Controls, status) return controls, err } @@ -79,16 +127,11 @@ func (t *Tool) GetBranchControls(ctx context.Context, r *models.Repository, bran // OnboardRepository configures a repository to set up the required controls // to meet SLSA Source L3. func (t *Tool) OnboardRepository(ctx context.Context, repo *models.Repository, branches []*models.Branch) error { - backend, err := t.impl.GetVcsBackend(repo) - if err != nil { - return fmt.Errorf("getting VCS backend: %w", err) - } - if err := t.impl.VerifyOptionsForFullOnboard(t.Authenticator, &t.Options); err != nil { return fmt.Errorf("verifying options: %w", err) } - if err := backend.ConfigureControls( + if err := t.backend.ConfigureControls( repo, branches, []models.ControlConfiguration{ models.CONFIG_BRANCH_RULES, models.CONFIG_GEN_PROVENANCE, models.CONFIG_TAG_RULES, }, @@ -101,11 +144,6 @@ func (t *Tool) OnboardRepository(ctx context.Context, repo *models.Repository, b // ConfigureControls sets up a control in the repo func (t *Tool) ConfigureControls(ctx context.Context, repo *models.Repository, branches []*models.Branch, configs []models.ControlConfiguration) error { - backend, err := t.impl.GetVcsBackend(repo) - if err != nil { - return fmt.Errorf("getting VCS backend: %w", err) - } - // The policy configuration is not handled by the backend if slices.Contains(configs, models.CONFIG_POLICY) { if err := t.impl.CheckPolicyFork(&t.Options); err != nil { @@ -123,17 +161,12 @@ func (t *Tool) ConfigureControls(ctx context.Context, repo *models.Repository, b } } - return t.impl.ConfigureControls(backend, repo, branches, configs) + return t.impl.ConfigureControls(t.backend, repo, branches, configs) } // ControlConfigurationDescr returns a description of the controls func (t *Tool) ControlConfigurationDescr(branch *models.Branch, config models.ControlConfiguration) string { - backend, err := t.impl.GetVcsBackend(branch.Repository) - if err != nil { - return "" - } - - return backend.ControlConfigurationDescr(branch, config) + return t.backend.ControlConfigurationDescr(branch, config) } func (t *Tool) FindPolicyPR(ctx context.Context, repo *models.Repository) (*models.PullRequest, error) { @@ -180,12 +213,8 @@ func (t *Tool) CreateBranchPolicy(ctx context.Context, r *models.Repository, bra if branches == nil { return nil, errors.New("no branches defined") } - backend, err := t.impl.GetVcsBackend(r) - if err != nil { - return nil, fmt.Errorf("getting backend: %w", err) - } - controls, err := t.impl.GetBranchControls(ctx, backend, r, branches[0]) + controls, err := t.impl.GetBranchControls(ctx, t.backend, branches[0]) if err != nil { return nil, fmt.Errorf("getting branch controls: %w", err) } @@ -195,7 +224,7 @@ func (t *Tool) CreateBranchPolicy(ctx context.Context, r *models.Repository, bra // This function will be moved to the policy package once we start integrating // it with the global data models (if we do). -func (t *Tool) createPolicy(r *models.Repository, branch *models.Branch, controls *slsa.ControlSetStatus) (*policy.RepoPolicy, error) { +func (t *Tool) createPolicy(r *models.Repository, branch *models.Branch, controls *slsa.ControlSet) (*policy.RepoPolicy, error) { // Default to SLSA1 since unset date eligibleSince := &time.Time{} eligibleLevel := slsa.SlsaSourceLevel1 @@ -204,8 +233,8 @@ func (t *Tool) createPolicy(r *models.Repository, branch *models.Branch, control // Unless there is previous provenance metadata, then we can compute // a higher level if controls != nil { - eligibleLevel = policy.ComputeEligibleSlsaLevel(*controls.GetActiveControls()) - eligibleSince, err = policy.ComputeEligibleSince(*controls.GetActiveControls(), eligibleLevel) + eligibleLevel = policy.ComputeEligibleSlsaLevel(controls.GetActiveControls()) + eligibleSince, err = policy.ComputeEligibleSince(controls.GetActiveControls(), eligibleLevel) if err != nil { return nil, fmt.Errorf("could not compute eligible_since: %w", err) } @@ -223,10 +252,10 @@ func (t *Tool) createPolicy(r *models.Repository, branch *models.Branch, control } // If the controls returned - tagHygiene := controls.GetActiveControls().GetControl(slsa.TagHygiene) + tagHygiene := controls.GetActiveControls().GetControl(slsa.SLSA_SOURCE_SCS_PROTECTED_REFS) if tagHygiene != nil { p.ProtectedTag = &policy.ProtectedTag{ - Since: tagHygiene.GetSince(), + Since: timestamppb.New(*tagHygiene.GetSince()), TagHygiene: true, } } @@ -280,10 +309,258 @@ func (t *Tool) CreatePolicyRepoFork(ctx context.Context) error { func (t *Tool) ControlPrecheck( _ context.Context, r *models.Repository, branches []*models.Branch, config models.ControlConfiguration, ) (ok bool, remediationMessage string, remediateFn models.ControlPreRemediationFn, err error) { - backend, err := t.impl.GetVcsBackend(r) + return t.backend.ControlPrecheck(r, branches, config) +} + +// Attester returns an attester object with the tool configuration +func (t *Tool) Attester() *attest.Attester { + return t.attester +} + +// Backend returns the VCS backend +func (t *Tool) Backend() models.VcsBackend { + return t.backend +} + +// GetPreviousCommit returns the previous commit of the passed commit +func (t *Tool) GetPreviousCommit(ctx context.Context, branch *models.Branch, commit *models.Commit) (*models.Commit, error) { + return t.backend.GetPreviousCommit(ctx, branch, commit) +} + +type AttestOptions struct { + LocalPolicy string + Sign bool + OutputPath string + UseStdOut bool + Push bool +} + +type AttOpFn func(*AttestOptions) error + +func WithLocalPolicy(p string) AttOpFn { + return func(ao *AttestOptions) error { + ao.LocalPolicy = p + return nil + } +} + +func WithSign(s bool) AttOpFn { + return func(ao *AttestOptions) error { + ao.Sign = s + return nil + } +} + +func WithOutputPath(p string) AttOpFn { + return func(ao *AttestOptions) error { + ao.OutputPath = p + return nil + } +} + +func WithUseStdout(s bool) AttOpFn { + return func(ao *AttestOptions) error { + ao.UseStdOut = s + return nil + } +} + +func WithPush(s bool) AttOpFn { + return func(ao *AttestOptions) error { + ao.Push = s + return nil + } +} + +var defaultAttestOptions = AttestOptions{ + Sign: true, + UseStdOut: true, +} + +// AttestRevision checks the source control system status, the repository policy +// and generates the repository attestations (provenance & VSA) for a revision +func (t *Tool) AttestRevision( + ctx context.Context, branch *models.Branch, rev models.Revision, funcs ...AttOpFn, +) (slsa.SourceVerifiedLevels, error) { + var agent *collector.Agent + var err error + + // Initialize the attest options + opts := defaultAttestOptions + for _, f := range funcs { + if err := f(&opts); err != nil { + return nil, err + } + } + + // Create the agent if we are pushing + if opts.Push { + agent, err = t.getAttestationStore(branch) + if err != nil { + return nil, fmt.Errorf("unable to intitializer storate agent: %w", err) + } + } + + var vsaData string + var provenanceData []byte + var verifiedLevels slsa.SourceVerifiedLevels + var policyPath string + + _, isCommit := rev.(*models.Commit) + tag, isTag := rev.(*models.Tag) + + if isCommit { + // 1. Create the provenance attestation + prov, err := t.Attester().CreateSourceProvenance(ctx, branch, rev.GetCommit()) + if err != nil { + return nil, err + } + + // 2. Run the provenance against the policy to determine if the controls + // are still in the context and timelines of the policy. + pe := policy.NewPolicyEvaluator() + pe.UseLocalPolicy = opts.LocalPolicy + verifiedLevels, policyPath, err = pe.EvaluateSourceProv(ctx, branch.Repository, branch, prov) + if err != nil { + return nil, fmt.Errorf("evaluating provenance with policy: %w", err) + } + + provenanceData, err = protojson.Marshal(prov) + if err != nil { + return nil, fmt.Errorf("generating provenance attestation: %w", err) + } + } + + if isTag { + // 1. Create the provenance attestation + prov, err := t.Attester().CreateTagProvenance(ctx, branch, tag, "") + if err != nil { + return nil, err + } + + // 2. Run the provenance against the policy to determine if the controls + // are still in the context and timelines of the policy. + pe := policy.NewPolicyEvaluator() + pe.UseLocalPolicy = opts.LocalPolicy + verifiedLevels, policyPath, err = pe.EvaluateTagProv(ctx, branch.Repository, prov) + if err != nil { + return nil, fmt.Errorf("evaluating provenance with policy: %w", err) + } + + provenanceData, err = protojson.Marshal(prov) + if err != nil { + return nil, fmt.Errorf("generating tag provenance attestation: %w", err) + } + } + + // create vsa + vsaData, err = attest.CreateUnsignedSourceVsa( + branch, rev.GetCommit(), verifiedLevels, policyPath, + ) + if err != nil { + return nil, fmt.Errorf("creating VSA: %w", err) + } + + if opts.Sign { + provenanceDataString, err := attest.Sign(string(provenanceData)) + if err != nil { + return nil, err + } + provenanceData = []byte(provenanceDataString) + + vsaData, err = attest.Sign(vsaData) + if err != nil { + return nil, err + } + } + + if opts.UseStdOut { + fmt.Printf("%s\n%s\n", string(provenanceData), vsaData) + } + + fpath := opts.OutputPath + if fpath == "" { + f, err := os.CreateTemp("", "attestations-") + if err != nil { + return nil, fmt.Errorf("opening tmp file: %w", err) + } + f.Close() //nolint:errcheck,gosec + fpath = f.Name() + } + + defer func() { + if opts.OutputPath == "" { + os.Remove(fpath) //nolint:errcheck,gosec + } + }() + + if err := os.WriteFile( + fpath, fmt.Appendf(nil, "%s\n%s\n", string(provenanceData), vsaData), os.FileMode(0o600), + ); err != nil { + return nil, fmt.Errorf("writing attestations: %w", err) + } + + if opts.Push { + if err := agent.StoreFromFiles(ctx, []string{fpath}); err != nil { + return nil, fmt.Errorf("pushing attestations: %w", err) + } + } + + return verifiedLevels, nil +} + +// getAttestationStore returns a collector with storer reposistories to push +// the generated attestations. +func (t *Tool) getAttestationStore(branch *models.Branch) (*collector.Agent, error) { + if len(t.Options.StorageLocations) == 0 && !t.Options.InitGHStorer && !t.Options.InitNotesStorer { + return nil, errors.New("no storage locations defined") + } + + if t.Authenticator == nil { + return nil, errors.New("tool has no authenticator configured") + } + + token, err := t.Authenticator.ReadToken() if err != nil { - return false, "", nil, fmt.Errorf("getting VCS backend: %w", err) + return nil, fmt.Errorf("unable to read auth token: %w", err) + } + copts := []collector.InitFunction{} + // Init GitHub storer + if t.Options.InitGHStorer { + ghrepo, err := cgithub.New( + cgithub.WithRepo(branch.Repository.GetHttpURL()), + cgithub.WithToken(token), + ) + if err != nil { + return nil, fmt.Errorf("initializing gitrhub repo: %w", err) + } + copts = append(copts, collector.WithRepository(ghrepo)) } - return backend.ControlPrecheck(r, branches, config) + // Init commit notes storer + if t.Options.InitNotesStorer { + notesrepo, err := note.NewDynamic( + note.WithLocator(branch.Repository.GetHttpURL()), + note.WithHttpAuth("github", token), + note.WithPush(true), + ) + if err != nil { + return nil, fmt.Errorf("initializing notes collector: %w", err) + } + copts = append(copts, collector.WithRepository(notesrepo)) + } + + // Retrurn the agent with the configured storers + c, err := collector.New(copts...) + if err != nil { + return nil, fmt.Errorf("initializing storage agent: %w", err) + } + + // Add any additional storage locations + for _, str := range t.Options.StorageLocations { + if err := c.AddRepositoryFromString(str); err != nil { + return nil, fmt.Errorf("initializing repo %q: %w", str, err) + } + } + return c, nil } diff --git a/pkg/sourcetool/tool_test.go b/pkg/sourcetool/tool_test.go index 008bbac..ee73c22 100644 --- a/pkg/sourcetool/tool_test.go +++ b/pkg/sourcetool/tool_test.go @@ -20,13 +20,13 @@ func TestGetBranchControls(t *testing.T) { t.Run("GetActiveControls-success", func(t *testing.T) { t.Parallel() i := &sourcetoolfakes.FakeToolImplementation{} - i.GetPolicyStatusReturns(&slsa.ControlStatus{}, nil) - i.GetBranchControlsReturns(&slsa.ControlSetStatus{ + i.GetPolicyStatusReturns(&slsa.Control{}, nil) + i.GetBranchControlsReturns(&slsa.ControlSet{ RepoUri: "github.com/ok/repo", Branch: "main", - Controls: []slsa.ControlStatus{ + Controls: []*slsa.Control{ { - Name: slsa.ContinuityEnforced, + Name: slsa.SLSA_SOURCE_SCS_CONTINUITY, State: slsa.StateNotEnabled, Message: "Continuity enforced", }, @@ -35,11 +35,11 @@ func TestGetBranchControls(t *testing.T) { tool := &Tool{ impl: i, } - res, err := tool.GetBranchControls(t.Context(), &models.Repository{}, &models.Branch{}) + res, err := tool.GetBranchControls(t.Context(), &models.Branch{Repository: &models.Repository{}}) + require.NoError(t, err) require.NotNil(t, res) // This always has one more as we add the synyhetic policy check require.Len(t, res.Controls, 2) - require.NoError(t, err) }) t.Run("GetActiveControls-fails", func(t *testing.T) { t.Parallel() @@ -48,7 +48,7 @@ func TestGetBranchControls(t *testing.T) { tool := &Tool{ impl: i, } - _, err := tool.GetBranchControls(t.Context(), &models.Repository{}, &models.Branch{}) + _, err := tool.GetBranchControls(t.Context(), &models.Branch{}) require.Error(t, err) }) } @@ -71,16 +71,6 @@ func TestConfigureControls(t *testing.T) { return i }, }, - { - name: "GetVcsBackend-fails", mustErr: true, - controls: []models.ControlConfiguration{models.CONFIG_BRANCH_RULES}, - prepare: func(t *testing.T) toolImplementation { - t.Helper() - i := &sourcetoolfakes.FakeToolImplementation{} - i.GetVcsBackendReturns(nil, syntErr) - return i - }, - }, { name: "CheckPolicyFork-fails", mustErr: true, controls: []models.ControlConfiguration{models.CONFIG_POLICY}, @@ -97,8 +87,6 @@ func TestConfigureControls(t *testing.T) { prepare: func(t *testing.T) toolImplementation { t.Helper() i := &sourcetoolfakes.FakeToolImplementation{} - i.GetVcsBackendReturns(&modelsfakes.FakeVcsBackend{}, nil) - i.GetAttestationReaderReturns(&modelsfakes.FakeAttestationStorageReader{}, nil) i.CreatePolicyPRReturns(nil, syntErr) return i }, @@ -292,50 +280,62 @@ func TestOnboardRepository(t *testing.T) { t.Parallel() for _, tt := range []struct { name string - getSut func(t *testing.T) toolImplementation + impl func(t *testing.T) toolImplementation + backend func(t *testing.T) models.VcsBackend mustErr bool }{ - {"normal", func(t *testing.T) toolImplementation { - t.Helper() - timp := &sourcetoolfakes.FakeToolImplementation{} - timp.GetVcsBackendReturns(&modelsfakes.FakeVcsBackend{}, nil) - timp.VerifyOptionsForFullOnboardReturns(nil) - timp.ConfigureControlsReturns(nil) - return timp - }, false}, - {"get-vcs-fails", func(t *testing.T) toolImplementation { - t.Helper() - timp := &sourcetoolfakes.FakeToolImplementation{} - timp.GetVcsBackendReturns(nil, errors.New("vcsborked")) - timp.VerifyOptionsForFullOnboardReturns(nil) - timp.ConfigureControlsReturns(nil) - return timp - }, true}, - {"get-verifyoptions-fails", func(t *testing.T) toolImplementation { - t.Helper() - timp := &sourcetoolfakes.FakeToolImplementation{} - timp.GetVcsBackendReturns(&modelsfakes.FakeVcsBackend{}, nil) - timp.VerifyOptionsForFullOnboardReturns(errors.New("onboarderr")) - return timp - }, true}, - {"backend-configure-controls-fails", func(t *testing.T) toolImplementation { - t.Helper() - bend := modelsfakes.FakeVcsBackend{} - bend.ConfigureControlsReturns(errors.New("configure-error")) - - timp := &sourcetoolfakes.FakeToolImplementation{} - timp.GetVcsBackendReturns(&bend, nil) - timp.VerifyOptionsForFullOnboardReturns(nil) - return timp - }, true}, + { + name: "normal", + impl: func(t *testing.T) toolImplementation { + t.Helper() + timp := &sourcetoolfakes.FakeToolImplementation{} + timp.VerifyOptionsForFullOnboardReturns(nil) + return timp + }, + backend: func(t *testing.T) models.VcsBackend { + t.Helper() + return &modelsfakes.FakeVcsBackend{} + }, + mustErr: false, + }, + { + name: "get-verifyoptions-fails", + impl: func(t *testing.T) toolImplementation { + t.Helper() + timp := &sourcetoolfakes.FakeToolImplementation{} + timp.VerifyOptionsForFullOnboardReturns(errors.New("onboarderr")) + return timp + }, + backend: func(t *testing.T) models.VcsBackend { + t.Helper() + return &modelsfakes.FakeVcsBackend{} + }, + mustErr: true, + }, + { + name: "backend-configure-controls-fails", + impl: func(t *testing.T) toolImplementation { + t.Helper() + timp := &sourcetoolfakes.FakeToolImplementation{} + timp.VerifyOptionsForFullOnboardReturns(nil) + return timp + }, + backend: func(t *testing.T) models.VcsBackend { + t.Helper() + bend := &modelsfakes.FakeVcsBackend{} + bend.ConfigureControlsReturns(errors.New("configure-error")) + return bend + }, + mustErr: true, + }, } { t.Run(tt.name, func(t *testing.T) { t.Parallel() - impl := tt.getSut(t) - tool, err := New() - require.NoError(t, err) - tool.impl = impl - err = tool.OnboardRepository(t.Context(), &models.Repository{Path: "example/repo"}, []*models.Branch{{Name: "main"}}) + tool := &Tool{ + impl: tt.impl(t), + backend: tt.backend(t), + } + err := tool.OnboardRepository(t.Context(), &models.Repository{Path: "example/repo"}, []*models.Branch{{Name: "main"}}) if tt.mustErr { require.Error(t, err) return