├── .bom-config.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── release.yml │ └── snapshot.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .ko.yaml ├── CONTRIBUTING.md ├── LICENSE ├── OWNERS ├── OWNERS_ALIASES ├── README.md ├── SECURITY_CONTACTS ├── cloudbuild.yaml ├── cmd └── tejolote │ └── main.go ├── code-of-conduct.md ├── docs ├── molcajete.jpg ├── pubsub.md └── slsa-logo.png ├── go.mod ├── go.sum ├── internal └── cmd │ ├── attest.go │ ├── flags.go │ ├── main.go │ ├── run.go │ └── start.go ├── mage.go ├── magefile.go ├── pkg ├── attestation │ ├── attestation.go │ └── sign.go ├── builder │ ├── builder.go │ └── driver │ │ ├── driver.go │ │ ├── gcb.go │ │ ├── gcb_test.go │ │ └── github.go ├── config │ └── build.go ├── exec │ ├── artifact.go │ ├── run.go │ ├── runner.go │ ├── runner_implementation.go │ └── step.go ├── git │ ├── repository.go │ └── repository_unit_test.go ├── github │ ├── github.go │ └── types.go ├── run │ └── run.go ├── sbom │ └── sbom.go ├── store │ ├── driver │ │ ├── actions.go │ │ ├── actions_test.go │ │ ├── attestation.go │ │ ├── directory.go │ │ ├── directory_test.go │ │ ├── gcb.go │ │ ├── gcb_test.go │ │ ├── gcs.go │ │ ├── gcs_test.go │ │ ├── github.go │ │ ├── github_test.go │ │ ├── oci.go │ │ ├── oci_test.go │ │ └── spdx.go │ ├── snapshot │ │ ├── snapshot.go │ │ └── snapshot_test.go │ └── store.go └── watcher │ ├── watcher.go │ └── watcher_test.go └── scripts ├── boilerplate ├── boilerplate.Dockerfile.txt ├── boilerplate.Makefile.txt ├── boilerplate.generatego.txt ├── boilerplate.go.txt ├── boilerplate.py.txt └── boilerplate.sh.txt └── verify-build.sh /.bom-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | namespace: https://sigs.k8s.io/tejolote 3 | license: Apache-2.0 4 | name: tejolote 5 | creator: 6 | person: The Kubernetes Authors 7 | tool: tejolote 8 | 9 | artifacts: 10 | - type: file 11 | source: tejolote-amd64-windows.exe 12 | license: Apache-2.0 13 | gomodules: true 14 | 15 | - type: file 16 | source: tejolote-amd64-darwin 17 | license: Apache-2.0 18 | gomodules: true 19 | 20 | - type: file 21 | source: tejolote-amd64-linux 22 | license: Apache-2.0 23 | gomodules: true 24 | 25 | - type: file 26 | source: tejolote-arm-linux 27 | license: Apache-2.0 28 | gomodules: true 29 | 30 | - type: file 31 | source: tejolote-arm64-darwin 32 | license: Apache-2.0 33 | gomodules: true 34 | 35 | - type: file 36 | source: tejolote-arm64-linux 37 | license: Apache-2.0 38 | gomodules: true 39 | 40 | - type: file 41 | source: tejolote-ppc64le-linux 42 | license: Apache-2.0 43 | gomodules: true 44 | 45 | - type: file 46 | source: tejolote-s390x-linux 47 | license: Apache-2.0 48 | gomodules: true 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug encountered while using Kubernetes release engineering tooling 4 | labels: kind/bug, sig/release, area/release-eng 5 | 6 | --- 7 | 8 | 12 | 13 | #### What happened: 14 | 15 | #### What you expected to happen: 16 | 17 | #### How to reproduce it (as minimally and precisely as possible): 18 | 19 | #### Anything else we need to know?: 20 | 21 | #### Environment: 22 | 23 | - Cloud provider or hardware configuration: 24 | - OS (e.g: `cat /etc/os-release`): 25 | - Kernel (e.g. `uname -a`): 26 | - Others: 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a feature for the Kubernetes release engineering tooling 4 | labels: kind/feature, sig/release, area/release-eng 5 | 6 | --- 7 | 8 | 9 | #### What would you like to be added: 10 | 11 | #### Why is this needed: 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | #### What type of PR is this? 11 | 12 | 27 | 28 | #### What this PR does / why we need it: 29 | 30 | #### Which issue(s) this PR fixes: 31 | 32 | 42 | 43 | #### Special notes for your reviewer: 44 | 45 | #### Does this PR introduce a user-facing change? 46 | 47 | 54 | 55 | ```release-note 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security Announcements 4 | 5 | Join the [kubernetes-security-announce] group for security and vulnerability announcements. 6 | 7 | You can also subscribe to an RSS feed of the above using [this link][kubernetes-security-announce-rss]. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Instructions for reporting a vulnerability can be found on the 12 | [Kubernetes Security and Disclosure Information] page. 13 | 14 | ## Supported Versions 15 | 16 | Information about supported Kubernetes versions can be found on the 17 | [Kubernetes version and version skew support policy] page on the Kubernetes website. 18 | 19 | [kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce 20 | [kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50 21 | [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions 22 | [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | 5 | - package-ecosystem: gomod 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | labels: 10 | - "area/dependency" 11 | - "release-note-none" 12 | - "ok-to-test" 13 | open-pull-requests-limit: 10 14 | groups: 15 | all: 16 | update-types: 17 | - "minor" 18 | - "patch" 19 | 20 | - package-ecosystem: "github-actions" 21 | directory: "/" 22 | schedule: 23 | interval: "daily" 24 | labels: 25 | - "area/dependency" 26 | - "release-note-none" 27 | - "ok-to-test" 28 | open-pull-requests-limit: 10 29 | groups: 30 | all: 31 | update-types: 32 | - "minor" 33 | - "patch" 34 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright 2022 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | name: lint 5 | 6 | on: 7 | push: 8 | branches: 9 | - 'main' 10 | pull_request: 11 | 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 18 | with: 19 | persist-credentials: false 20 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 21 | with: 22 | go-version: '1.24' 23 | check-latest: true 24 | 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 27 | with: 28 | version: v2.1 29 | args: --verbose 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | id-token: write 14 | contents: write 15 | 16 | env: 17 | COSIGN_YES: "true" 18 | 19 | steps: 20 | - name: Check out code onto GOPATH 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | fetch-depth: 1 24 | 25 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 26 | with: 27 | go-version: '1.23' 28 | check-latest: true 29 | 30 | - name: Install cosign 31 | uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 32 | 33 | - name: Install bom 34 | uses: kubernetes-sigs/release-actions/setup-bom@a30d93cf2aa029e1e4c8a6c79f766aebf429fddb # v0.3.1 35 | 36 | - name: Install GoReleaser 37 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 38 | with: 39 | install-only: true 40 | 41 | - name: Get TAG 42 | id: get_tag 43 | run: echo "TAG=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" 44 | 45 | - name: Run Mage 46 | uses: magefile/mage-action@6f50bbb8ea47d56e62dee92392788acbc8192d0b # v3.1.0 47 | with: 48 | version: latest 49 | args: buildBinaries 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | attestation: 54 | runs-on: ubuntu-latest 55 | 56 | permissions: 57 | id-token: write 58 | contents: write 59 | 60 | needs: 61 | - release 62 | 63 | steps: 64 | - name: Check out code onto GOPATH 65 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 66 | with: 67 | fetch-depth: 1 68 | 69 | - name: Set tag output 70 | id: tag 71 | run: echo "tag_name=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" 72 | 73 | - name: Install tejolote 74 | uses: kubernetes-sigs/release-actions/setup-tejolote@a30d93cf2aa029e1e4c8a6c79f766aebf429fddb # v0.3.1 75 | 76 | - run: | 77 | tejolote attest --artifacts github://kubernetes-sigs/tejolote/${{ steps.tag.outputs.tag_name }} github://kubernetes-sigs/tejolote/"${GITHUB_RUN_ID}" --output tejolote.intoto.json --sign 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | 81 | - name: Release 82 | uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 83 | with: 84 | files: tejolote.intoto.json 85 | tag_name: "${{ steps.tag.outputs.tag_name }}" 86 | token: ${{ secrets.GITHUB_TOKEN }} 87 | env: 88 | GITHUB_REPOSITORY: kubernetes-sigs/tejolote 89 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | 9 | jobs: 10 | snapshot: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code onto GOPATH 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 18 | with: 19 | go-version: '1.23' 20 | check-latest: true 21 | 22 | - name: Install GoReleaser 23 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 24 | with: 25 | install-only: true 26 | 27 | - name: Install bom 28 | uses: kubernetes-sigs/release-actions/setup-bom@a30d93cf2aa029e1e4c8a6c79f766aebf429fddb # v0.3.1 29 | 30 | - name: Run Mage 31 | uses: magefile/mage-action@6f50bbb8ea47d56e62dee92392788acbc8192d0b # v3.1.0 32 | with: 33 | version: latest 34 | args: buildBinariesSnapshot 35 | 36 | - name: check binary 37 | run: ./dist/tejolote-amd64-linux version 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | # This is where the result of the go build goes 14 | /output*/ 15 | /_output*/ 16 | /_output 17 | 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | # vendor/ 21 | .vscode 22 | 23 | 24 | # downloaded and built binaries 25 | bin 26 | qemu-*-static 27 | rootfs.tar 28 | dist/ 29 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright 2022 The Kubernetes Authors. 2 | # SPDX-License-Identifier: Apache-2.0 3 | --- 4 | version: "2" 5 | run: 6 | concurrency: 6 7 | timeout: 5m 8 | issues: 9 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 10 | max-issues-per-linter: 0 11 | 12 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 13 | max-same-issues: 0 14 | formatters: 15 | # Enable specific formatter. 16 | # Default: [] (uses standard Go formatting) 17 | enable: 18 | - gci 19 | - gofmt 20 | - gofumpt 21 | - goimports 22 | # - golines 23 | settings: 24 | gci: 25 | no-inline-comments: false 26 | no-prefix-comments: true 27 | sections: 28 | - standard 29 | - default 30 | - prefix(github.com/kubernetes-sigs/tejolote) 31 | linters: 32 | enable: 33 | - asasalint 34 | - asciicheck 35 | - bidichk 36 | - bodyclose 37 | - canonicalheader 38 | - containedctx 39 | # - contextcheck 40 | - copyloopvar 41 | - decorder 42 | - dogsled 43 | - dupl 44 | - durationcheck 45 | # - errcheck 46 | - errchkjson 47 | - errname 48 | - errorlint 49 | - exhaustive 50 | - exptostd 51 | - fatcontext 52 | - forcetypeassert 53 | - ginkgolinter 54 | - gocheckcompilerdirectives 55 | - gochecksumtype 56 | - goconst 57 | - gocritic 58 | - gocyclo 59 | # - godot 60 | - godox 61 | - goheader 62 | - gomoddirectives 63 | - gomodguard 64 | - goprintffuncname 65 | - gosec 66 | - gosmopolitan 67 | - govet 68 | - grouper 69 | - iface 70 | - importas 71 | - ineffassign 72 | - intrange 73 | - loggercheck 74 | - makezero 75 | - mirror 76 | - misspell 77 | - musttag 78 | - nakedret 79 | - nilerr 80 | - nilnesserr 81 | # - nlreturn 82 | - noctx 83 | - nolintlint 84 | - nosprintfhostport 85 | # - perfsprint 86 | - prealloc 87 | - predeclared 88 | - promlinter 89 | - protogetter 90 | - reassign 91 | - recvcheck 92 | # - revive 93 | - rowserrcheck 94 | - sloglint 95 | - spancheck 96 | - sqlclosecheck 97 | - staticcheck 98 | - tagalign 99 | - testableexamples 100 | - testifylint 101 | - tparallel 102 | - unconvert 103 | - unparam 104 | - unused 105 | - usestdlibvars 106 | - usetesting 107 | - wastedassign 108 | - whitespace 109 | # - wsl 110 | - zerologlint 111 | disable: 112 | - errcheck 113 | settings: 114 | gocyclo: 115 | min-complexity: 35 116 | godox: 117 | keywords: 118 | - BUG 119 | - FIXME 120 | - HACK 121 | gosec: 122 | excludes: 123 | - G304 124 | gosmopolitan: 125 | # Allow and ignore `time.Local` usages. 126 | # 127 | # Default: false 128 | allow-time-local: true 129 | perfsprint: 130 | integer-format: false 131 | gocritic: 132 | enabled-checks: 133 | # Diagnostic 134 | - commentedOutCode 135 | - nilValReturn 136 | - sloppyReassign 137 | - weakCond 138 | - octalLiteral 139 | 140 | # Performance 141 | - appendCombine 142 | - equalFold 143 | - hugeParam 144 | - indexAlloc 145 | - rangeExprCopy 146 | - rangeValCopy 147 | 148 | # Style 149 | - boolExprSimplify 150 | - commentedOutImport 151 | - docStub 152 | - emptyFallthrough 153 | - emptyStringTest 154 | - hexLiteral 155 | - methodExprCall 156 | - stringXbytes 157 | - typeAssertChain 158 | - unlabelStmt 159 | - yodaStyleExpr 160 | # - ifElseChain 161 | 162 | # Opinionated 163 | - builtinShadow 164 | - importShadow 165 | - initClause 166 | - nestingReduce 167 | - paramTypeCombine 168 | - ptrToRefParam 169 | - typeUnparen 170 | - unnamedResult 171 | - unnecessaryBlock 172 | nolintlint: 173 | # Enable to ensure that nolint directives are all used. Default is true. 174 | allow-unused: false 175 | # Exclude following linters from requiring an explanation. Default is []. 176 | allow-no-explanation: [] 177 | # Enable to require an explanation of nonzero length after each nolint directive. Default is false. 178 | # TODO(lint): Enforce explanations for `nolint` directives 179 | require-explanation: false 180 | # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. 181 | require-specific: true 182 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: tejolote 2 | version: 2 3 | 4 | env: 5 | - GO111MODULE=on 6 | - CGO_ENABLED=0 7 | - COSIGN_YES=true 8 | 9 | before: 10 | hooks: 11 | - go mod tidy 12 | - /bin/bash -c 'if [ -n "$(git --no-pager diff --exit-code go.mod go.sum)" ]; then exit 1; fi' 13 | 14 | gomod: 15 | proxy: true 16 | 17 | builds: 18 | - id: tejolote 19 | main: ./cmd/tejolote 20 | no_unique_dist_dir: true 21 | binary: tejolote-{{ .Arch }}-{{ .Os }} 22 | goos: 23 | - darwin 24 | - linux 25 | - windows 26 | goarch: 27 | - amd64 28 | - arm64 29 | - arm 30 | - s390x 31 | - ppc64le 32 | goarm: 33 | - '7' 34 | ignore: 35 | - goos: windows 36 | goarch: arm64 37 | - goos: windows 38 | goarch: arm 39 | - goos: windows 40 | goarch: s390x 41 | - goos: windows 42 | goarch: ppc64le 43 | flags: 44 | - -trimpath 45 | ldflags: 46 | - "{{ .Env.TEJOLOTE_LDFLAGS }}" 47 | 48 | archives: 49 | - format: binary 50 | name_template: tejolote-{{ .Arch }}-{{ .Os }} 51 | allow_different_binary_count: true 52 | 53 | signs: 54 | # Keyless 55 | - id: tejolote-keyless 56 | signature: "${artifact}.sig" 57 | certificate: "${artifact}.pem" 58 | cmd: cosign 59 | args: 60 | - "sign-blob" 61 | - "--output-signature" 62 | - "${artifact}.sig" 63 | - "--output-certificate" 64 | - "${artifact}.pem" 65 | - "${artifact}" 66 | artifacts: all 67 | 68 | sboms: 69 | - id: tejolote 70 | cmd: bom 71 | args: 72 | - generate 73 | - "--output" 74 | - "tejolote.json.spdx" 75 | - "-d" 76 | - "../" 77 | - "-c" 78 | - "../.bom-config.yaml" 79 | - "--format" 80 | - "json" 81 | artifacts: any 82 | documents: 83 | - "tejolote.json.spdx" 84 | 85 | checksum: 86 | name_template: 'checksums.txt' 87 | 88 | snapshot: 89 | version_template: "{{ .Tag }}-next" 90 | 91 | release: 92 | github: 93 | owner: kubernetes-sigs 94 | name: tejolote 95 | prerelease: auto 96 | 97 | changelog: 98 | disable: true 99 | -------------------------------------------------------------------------------- /.ko.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | defaultBaseImage: cgr.dev/chainguard/static:latest 3 | 4 | builds: 5 | - id: tejolote 6 | dir: . 7 | main: ./cmd/tejolote 8 | env: 9 | - CGO_ENABLED=0 10 | flags: 11 | - -trimpath 12 | ldflags: 13 | - -extldflags "-static" 14 | - "{{ .Env.TEJOLOTE_LDFLAGS }}" 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Welcome to Kubernetes. We are excited about the prospect of you joining our [community](https://git.k8s.io/community)! The Kubernetes community abides by the CNCF [code of conduct](code-of-conduct.md). Here is an excerpt: 4 | 5 | _As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities._ 6 | 7 | ## Getting Started 8 | 9 | We have full documentation on how to get started contributing here: 10 | 11 | 14 | 15 | - [Contributor License Agreement](https://git.k8s.io/community/CLA.md) Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests 16 | - [Kubernetes Contributor Guide](https://git.k8s.io/community/contributors/guide) - Main contributor documentation, or you can just jump directly to the [contributing section](https://git.k8s.io/community/contributors/guide#contributing) 17 | - [Contributor Cheat Sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet) - Common resources for existing developers 18 | 19 | ## Mentorship 20 | 21 | - [Mentoring Initiatives](https://git.k8s.io/community/mentoring) - We have a diverse set of mentorship programs available that are always looking for volunteers! 22 | 23 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # See the OWNERS docs at https://go.k8s.io/owners 2 | 3 | approvers: 4 | - sig-release-leads 5 | - release-engineering-approvers 6 | reviewers: 7 | - release-engineering-reviewers 8 | -------------------------------------------------------------------------------- /OWNERS_ALIASES: -------------------------------------------------------------------------------- 1 | aliases: 2 | sig-release-leads: 3 | - cpanato # SIG Technical Lead 4 | - jeremyrickard # SIG Chair 5 | - justaugustus # SIG Chair 6 | - puerco # SIG Technical Lead 7 | - saschagrunert # SIG Chair 8 | - Verolop # SIG Technical Lead 9 | release-engineering-approvers: 10 | - cpanato # subproject owner / Release Manager 11 | - jeremyrickard # subproject owner / Release Manager 12 | - justaugustus # subproject owner / Release Manager 13 | - palnabarun # Release Manager 14 | - puerco # subproject owner / Release Manager 15 | - saschagrunert # subproject owner / Release Manager 16 | - xmudrii # Release Manager 17 | - Verolop # subproject owner / Release Manager 18 | release-engineering-reviewers: 19 | - ameukam # Release Manager Associate 20 | - cici37 # Release Manager Associate 21 | - jimangel # Release Manager Associate 22 | - jrsapi # Release Manager Associate 23 | - salaxander # Release Manager Associate 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tejolote 2 | 3 | A highly configurable build executor and observer designed to generate 4 | signed [SLSA](https://slsa.dev/) provenance attestations about build runs. 5 | 6 | ## What Does That Mean!? 7 | 8 | ![SLSA Logo](docs/slsa-logo.png) 9 | 10 | If you are not familiar with 11 | [provenance](https://www.tiktok.com/@chainguard_dev/video/7133203786927050027) attestations, think of them as non-falsifiable documents that inform you 12 | users how software was built, mainly: 13 | 14 | What went in → What was done to the source code (and by who) → what came out. 15 | 16 | A provenance attestation provides users with full transparency to the 17 | build process of the software they consume, allowing them to know where 18 | it came from, how it was built and by who. 19 | 20 | ## Key Features 21 | 22 | Tejolote is designed to observe build systems as they run to gather data 23 | about transformations done to software as it goes through the build process. 24 | It features a pluggable model to add more build systems and artifact 25 | storage as the need arises. 26 | 27 | * Support for multiple build systems (currently 28 | [Google Cloud Build](https://cloud.google.com/build), 29 | [Github Actions](https://github.com/features/actions), 30 | [Prow](https://github.com/kubernetes/test-infra/tree/master/prow) 31 | coming soon). 32 | * Support for gathering attestation data in multiple stages or observing a build 33 | while it runs. 34 | * Collection of artifacts from different sources (build system native, 35 | directories, OCI registries, Google Cloud Storage buckets). 36 | * Attestation signing using [sigstore](https://sigstore.dev) 37 | * Attaching attestations to container images as cosign 38 | 39 | ## Operational Model 40 | 41 | Tejolote watches your build system build (or transform) your software 42 | project. It treats your build as a black box and makes no assumptions as 43 | to the security of the build itself. 44 | 45 | It will trust the inputs you tell it to consider and the artifacts your 46 | build produces by looking a the location you instruct it too look for them. 47 | 48 | ```mermaid 49 | flowchart LR 50 | 51 | subgraph Build System 52 | direction LR 53 | clone("Clone Repository") --> build(Run Build) --> publish(Publish Artifacts) 54 | fetch("Fetch Materials") --> build 55 | publish --> oci(Container Registry) 56 | publish --> gcs(GCS Bucket) 57 | publish --> file(Filesystem) 58 | end 59 | subgraph Tejolote 60 | direction LR 61 | watch(Watch Build System) --> attest(Attest) --> sign(Sign) 62 | watch-. RECORD .-o clone 63 | watch-. RECORD .-o fetch 64 | watch-. CONTINOUSLY OBSERVE .-o build 65 | watch-. COLLECT .-o publish 66 | end 67 | 68 | ``` 69 | 70 | While build systems can themselves provide information about the 71 | artifacts produced after a run, Tejolote sits one level above and 72 | will expect artifacts to appear in the storage location(s) you 73 | tell it to monitor. 74 | 75 | ## Example 76 | 77 | Let's say for example you want to attest a Cloud Build job that produces 78 | a bunch of binaries in a GCS bucket. In this case, the gcb project is 79 | `example-project` and artifacts are uploaded to the bucket `test-bucket` 80 | in the directory `/test`: 81 | 82 | ```bash 83 | tejolote attest \ 84 | gcb://kubernetes-release-test/3190d867-f2e5-4969-aafd-0117b6c8ed12 \ 85 | --artifacts=gs://ulabs-cloud-tests/test/ 86 | ``` 87 | 88 | These are made up examples, but Tejolote would produce an attestation 89 | similar to this: 90 | 91 | ```json 92 | { 93 | "_type": "https://in-toto.io/Statement/v0.1", 94 | "predicateType": "https://slsa.dev/provenance/v0.2", 95 | "subject": [ 96 | { 97 | "name": "gs://ulabs-cloud-tests/test/bom-windows-amd64.exe", 98 | "digest": { 99 | "sha256": "c03c50f220b095bf52a0ca496989a6c07f198d03cb8aad19834df143625ee821" 100 | } 101 | } 102 | ], 103 | "predicate": { 104 | "builder": { 105 | "id": "" 106 | }, 107 | "buildType": "https://cloudbuild.googleapis.com/CloudBuildYaml@v1", 108 | "invocation": { 109 | "configSource": {} 110 | }, 111 | "buildConfig": { 112 | "steps": [ 113 | { 114 | "image": "gcr.io/cloud-builders/git", 115 | "arguments": [ 116 | "clone", 117 | "https://github.com/kubernetes/release" 118 | ] 119 | }, 120 | ... 121 | ``` 122 | 123 | Both build system runs and artifact repositories are specified by using 124 | [spec urls](docs/spec-urls.md) that point to the specific runs and storage 125 | location. Check out the 126 | 127 | ## What's with the name? 128 | 129 | Tejolote /ˌteɪhəˈloʊteɪ/ : From the nahua word _texolotl_. 130 | 131 | ![molcajete and tejolote](docs/molcajete.jpg) 132 | 133 | A tejolote is the handle of the [_molcajete_](https://en.wikipedia.org/wiki/Molcajete), the prehispanic mortar used to make 134 | salsa. 135 | 136 | So, the idea is to use tejolote to get some salsa out of your project :) 137 | -------------------------------------------------------------------------------- /SECURITY_CONTACTS: -------------------------------------------------------------------------------- 1 | # Defined below are the security contacts for this repo. 2 | # 3 | # They are the contact point for the Product Security Committee to reach out 4 | # to for triaging and handling of incoming issues. 5 | # 6 | # The below names agree to abide by the 7 | # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) 8 | # and will be removed and replaced if they violate that agreement. 9 | # 10 | # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE 11 | # INSTRUCTIONS AT https://kubernetes.io/security/ 12 | 13 | cpanato 14 | jeremyrickard 15 | justaugustus 16 | puerco 17 | saschagrunert 18 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | # See https://cloud.google.com/cloud-build/docs/build-config 2 | --- 3 | timeout: 7200s 4 | options: 5 | substitution_option: ALLOW_LOOSE 6 | machineType: 'N1_HIGHCPU_8' 7 | steps: 8 | - name: gcr.io/cloud-builders/git 9 | dir: "go/src/sigs.k8s.io" 10 | args: 11 | - "clone" 12 | - "https://github.com/kubernetes-sigs/tejolote" 13 | 14 | - name: gcr.io/cloud-builders/git 15 | entrypoint: "bash" 16 | dir: "go/src/sigs.k8s.io/tejolote" 17 | args: 18 | - '-c' 19 | - | 20 | git fetch 21 | echo "Checking out ${_PULL_BASE_REF}" 22 | git checkout ${_PULL_BASE_REF} 23 | 24 | - name: 'gcr.io/k8s-staging-releng/releng-ci:latest-go1.23-bookworm' 25 | dir: "go/src/sigs.k8s.io/tejolote" 26 | entrypoint: go 27 | env: 28 | - DOCKER_CLI_EXPERIMENTAL=enabled 29 | - TAG=$_GIT_TAG 30 | - PULL_BASE_REF=$_PULL_BASE_REF 31 | - KO_DOCKER_REPO=gcr.io/k8s-staging-tejolote/tejolote 32 | args: 33 | - run 34 | - mage.go 35 | - buildStaging 36 | 37 | substitutions: 38 | # _GIT_TAG will be filled with a git-based tag for the image, of the form vYYYYMMDD-hash, and 39 | # can be used as a substitution 40 | _GIT_TAG: '12345' 41 | _PULL_BASE_REF: 'dev' 42 | -------------------------------------------------------------------------------- /cmd/tejolote/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "github.com/sirupsen/logrus" 21 | "sigs.k8s.io/tejolote/internal/cmd" 22 | ) 23 | 24 | func main() { 25 | if err := cmd.Execute(); err != nil { 26 | logrus.Fatal(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /docs/molcajete.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubernetes-sigs/tejolote/870cdf491249ccd5e65fc5beee40f4b44397a557/docs/molcajete.jpg -------------------------------------------------------------------------------- /docs/pubsub.md: -------------------------------------------------------------------------------- 1 | # PubSub Support 2 | 3 | Tejolote supports starting an attestation and messaging itself via pubsub 4 | to collect data after a run is done. 5 | 6 | ## Sleeping and Resuming 7 | 8 | TBD 9 | 10 | ## Recieving Data When Attestting 11 | 12 | Data communicated from the `tejolote start attestation` invocation will 13 | arrive base64 encoded. In order to rebuild the data, tejolote includes two 14 | hidden flags: 15 | 16 | ``` 17 | --encoded-attestation="" 18 | --encoded-snapshots="" 19 | ``` 20 | 21 | These two flags get base base64 encoded data, the first flag (`--encoded-attestation`) 22 | is the partial in-toto attestation to be completed with the finalized 23 | run data. 24 | 25 | The second one (`--encoded-snapshots=""`) includes the initial state of the 26 | artifact stores as seen by tejolote before the run. 27 | 28 | The flags are intended to be used by automation driving tejolote and therefore 29 | are not visible in the CLI help. -------------------------------------------------------------------------------- /docs/slsa-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubernetes-sigs/tejolote/870cdf491249ccd5e65fc5beee40f4b44397a557/docs/slsa-logo.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module sigs.k8s.io/tejolote 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | chainguard.dev/apko v0.27.9 9 | cloud.google.com/go/pubsub v1.49.0 10 | cloud.google.com/go/storage v1.55.0 11 | github.com/go-git/go-git/v5 v5.16.1 12 | github.com/google/go-containerregistry v0.20.5 13 | github.com/in-toto/attestation v1.1.1 14 | github.com/in-toto/in-toto-golang v0.9.0 15 | github.com/magefile/mage v1.15.0 16 | github.com/sigstore/cosign/v2 v2.5.0 17 | github.com/sigstore/sigstore v1.9.4 18 | github.com/sirupsen/logrus v1.9.3 19 | github.com/spf13/cobra v1.9.1 20 | github.com/stretchr/testify v1.10.0 21 | github.com/uwu-tools/magex v0.10.1 22 | golang.org/x/sync v0.14.0 23 | google.golang.org/api v0.236.0 24 | sigs.k8s.io/bom v0.6.0 25 | sigs.k8s.io/release-sdk v0.12.3 26 | sigs.k8s.io/release-utils v0.11.1 27 | ) 28 | 29 | require ( 30 | cel.dev/expr v0.20.0 // indirect 31 | cloud.google.com/go v0.121.1 // indirect 32 | cloud.google.com/go/auth v0.16.1 // indirect 33 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 34 | cloud.google.com/go/compute/metadata v0.7.0 // indirect 35 | cloud.google.com/go/iam v1.5.2 // indirect 36 | cloud.google.com/go/monitoring v1.24.2 // indirect 37 | dario.cat/mergo v1.0.1 // indirect 38 | github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/provider v0.16.1 // indirect 39 | github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect 40 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 41 | github.com/Azure/go-autorest/autorest v0.11.30 // indirect 42 | github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect 43 | github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect 44 | github.com/Azure/go-autorest/autorest/azure/cli v0.4.7 // indirect 45 | github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect 46 | github.com/Azure/go-autorest/logger v0.2.2 // indirect 47 | github.com/Azure/go-autorest/tracing v0.6.1 // indirect 48 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect 49 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect 50 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect 51 | github.com/MakeNowJust/heredoc/v2 v2.0.1 // indirect 52 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 53 | github.com/Microsoft/go-winio v0.6.2 // indirect 54 | github.com/ProtonMail/go-crypto v1.1.6 // indirect 55 | github.com/ThalesIgnite/crypto11 v1.2.5 // indirect 56 | github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect 57 | github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect 58 | github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect 59 | github.com/alibabacloud-go/darabonba-openapi v0.2.1 // indirect 60 | github.com/alibabacloud-go/debug v1.0.1 // indirect 61 | github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect 62 | github.com/alibabacloud-go/openapi-util v0.1.1 // indirect 63 | github.com/alibabacloud-go/tea v1.3.2 // indirect 64 | github.com/alibabacloud-go/tea-utils v1.4.5 // indirect 65 | github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect 66 | github.com/alibabacloud-go/tea-xml v1.1.3 // indirect 67 | github.com/aliyun/credentials-go v1.4.3 // indirect 68 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 69 | github.com/avast/retry-go/v4 v4.6.1 // indirect 70 | github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect 71 | github.com/aws/aws-sdk-go-v2/config v1.29.10 // indirect 72 | github.com/aws/aws-sdk-go-v2/credentials v1.17.63 // indirect 73 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 74 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 75 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 76 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 77 | github.com/aws/aws-sdk-go-v2/service/ecr v1.42.0 // indirect 78 | github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.32.0 // indirect 79 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 80 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 81 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect 82 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.2 // indirect 83 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect 84 | github.com/aws/smithy-go v1.22.3 // indirect 85 | github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.9.1 // indirect 86 | github.com/blang/semver v3.5.1+incompatible // indirect 87 | github.com/blang/semver/v4 v4.0.0 // indirect 88 | github.com/buildkite/agent/v3 v3.95.1 // indirect 89 | github.com/buildkite/go-pipeline v0.13.3 // indirect 90 | github.com/buildkite/interpolate v0.1.5 // indirect 91 | github.com/buildkite/roko v1.3.1 // indirect 92 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 93 | github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect 94 | github.com/clbanning/mxj/v2 v2.7.0 // indirect 95 | github.com/cloudflare/circl v1.6.1 // indirect 96 | github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect 97 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect 98 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect 99 | github.com/coreos/go-oidc/v3 v3.14.1 // indirect 100 | github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect 101 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 102 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 103 | github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect 104 | github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect 105 | github.com/dimchansky/utfbom v1.1.1 // indirect 106 | github.com/docker/cli v28.1.1+incompatible // indirect 107 | github.com/docker/distribution v2.8.3+incompatible // indirect 108 | github.com/docker/docker-credential-helpers v0.9.3 // indirect 109 | github.com/dustin/go-humanize v1.0.1 // indirect 110 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 111 | github.com/emirpasic/gods v1.18.1 // indirect 112 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect 113 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 114 | github.com/felixge/httpsnoop v1.0.4 // indirect 115 | github.com/fsnotify/fsnotify v1.8.0 // indirect 116 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 117 | github.com/glebarez/go-sqlite v1.22.0 // indirect 118 | github.com/go-chi/chi v4.1.2+incompatible // indirect 119 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 120 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 121 | github.com/go-jose/go-jose/v3 v3.0.4 // indirect 122 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 123 | github.com/go-logr/logr v1.4.2 // indirect 124 | github.com/go-logr/stdr v1.2.2 // indirect 125 | github.com/go-openapi/analysis v0.23.0 // indirect 126 | github.com/go-openapi/errors v0.22.1 // indirect 127 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 128 | github.com/go-openapi/jsonreference v0.21.0 // indirect 129 | github.com/go-openapi/loads v0.22.0 // indirect 130 | github.com/go-openapi/runtime v0.28.0 // indirect 131 | github.com/go-openapi/spec v0.21.0 // indirect 132 | github.com/go-openapi/strfmt v0.23.0 // indirect 133 | github.com/go-openapi/swag v0.23.1 // indirect 134 | github.com/go-openapi/validate v0.24.0 // indirect 135 | github.com/go-piv/piv-go/v2 v2.3.0 // indirect 136 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 137 | github.com/gogo/protobuf v1.3.2 // indirect 138 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 139 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 140 | github.com/golang/protobuf v1.5.4 // indirect 141 | github.com/golang/snappy v0.0.4 // indirect 142 | github.com/google/certificate-transparency-go v1.3.1 // indirect 143 | github.com/google/gnostic-models v0.6.9 // indirect 144 | github.com/google/go-cmp v0.7.0 // indirect 145 | github.com/google/go-github/v55 v55.0.0 // indirect 146 | github.com/google/go-github/v72 v72.0.0 // indirect 147 | github.com/google/go-querystring v1.1.0 // indirect 148 | github.com/google/licenseclassifier/v2 v2.0.0 // indirect 149 | github.com/google/s2a-go v0.1.9 // indirect 150 | github.com/google/uuid v1.6.0 // indirect 151 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 152 | github.com/googleapis/gax-go/v2 v2.14.2 // indirect 153 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 154 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 155 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 156 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 157 | github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 // indirect 158 | github.com/josharian/intern v1.0.0 // indirect 159 | github.com/json-iterator/go v1.1.12 // indirect 160 | github.com/kevinburke/ssh_config v1.2.0 // indirect 161 | github.com/klauspost/compress v1.18.0 // indirect 162 | github.com/knqyf263/go-rpmdb v0.1.1 // indirect 163 | github.com/letsencrypt/boulder v0.0.0-20250303232957-28b49a82d48a // indirect 164 | github.com/mailru/easyjson v0.9.0 // indirect 165 | github.com/mattn/go-isatty v0.0.20 // indirect 166 | github.com/miekg/pkcs11 v1.1.1 // indirect 167 | github.com/mitchellh/go-homedir v1.1.0 // indirect 168 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect 169 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 170 | github.com/modern-go/reflect2 v1.0.2 // indirect 171 | github.com/mozillazg/docker-credential-acr-helper v0.4.0 // indirect 172 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 173 | github.com/ncruces/go-strftime v0.1.9 // indirect 174 | github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect 175 | github.com/oklog/ulid v1.3.1 // indirect 176 | github.com/oleiade/reflections v1.1.0 // indirect 177 | github.com/opencontainers/go-digest v1.0.0 // indirect 178 | github.com/opencontainers/image-spec v1.1.1 // indirect 179 | github.com/opentracing/opentracing-go v1.2.0 // indirect 180 | github.com/package-url/packageurl-go v0.1.3 // indirect 181 | github.com/pborman/uuid v1.2.1 // indirect 182 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 183 | github.com/pjbgf/sha1cd v0.3.2 // indirect 184 | github.com/pkg/errors v0.9.1 // indirect 185 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 186 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 187 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 188 | github.com/sagikazarmark/locafero v0.7.0 // indirect 189 | github.com/sassoftware/relic v7.2.1+incompatible // indirect 190 | github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect 191 | github.com/segmentio/ksuid v1.0.4 // indirect 192 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 193 | github.com/shibumi/go-pathspec v1.3.0 // indirect 194 | github.com/sigstore/fulcio v1.6.6 // indirect 195 | github.com/sigstore/protobuf-specs v0.4.1 // indirect 196 | github.com/sigstore/rekor v1.3.10 // indirect 197 | github.com/sigstore/sigstore-go v0.7.1 // indirect 198 | github.com/sigstore/timestamp-authority v1.2.5 // indirect 199 | github.com/skeema/knownhosts v1.3.1 // indirect 200 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect 201 | github.com/sourcegraph/conc v0.3.0 // indirect 202 | github.com/spf13/afero v1.12.0 // indirect 203 | github.com/spf13/cast v1.7.1 // indirect 204 | github.com/spf13/pflag v1.0.6 // indirect 205 | github.com/spf13/viper v1.20.1 // indirect 206 | github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect 207 | github.com/subosito/gotenv v1.6.0 // indirect 208 | github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect 209 | github.com/thales-e-security/pool v0.0.2 // indirect 210 | github.com/theupdateframework/go-tuf v0.7.0 // indirect 211 | github.com/theupdateframework/go-tuf/v2 v2.0.2 // indirect 212 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect 213 | github.com/tjfoc/gmsm v1.4.1 // indirect 214 | github.com/transparency-dev/merkle v0.0.2 // indirect 215 | github.com/vbatts/tar-split v0.12.1 // indirect 216 | github.com/x448/float16 v0.8.4 // indirect 217 | github.com/xanzy/ssh-agent v0.3.3 // indirect 218 | github.com/zeebo/errs v1.4.0 // indirect 219 | gitlab.alpinelinux.org/alpine/go v0.10.1 // indirect 220 | gitlab.com/gitlab-org/api/client-go v0.127.0 // indirect 221 | go.mongodb.org/mongo-driver v1.17.3 // indirect 222 | go.opencensus.io v0.24.0 // indirect 223 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 224 | go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect 225 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 226 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 227 | go.opentelemetry.io/otel v1.36.0 // indirect 228 | go.opentelemetry.io/otel/metric v1.36.0 // indirect 229 | go.opentelemetry.io/otel/sdk v1.36.0 // indirect 230 | go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect 231 | go.opentelemetry.io/otel/trace v1.36.0 // indirect 232 | go.uber.org/multierr v1.11.0 // indirect 233 | go.uber.org/zap v1.27.0 // indirect 234 | golang.org/x/crypto v0.38.0 // indirect 235 | golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 // indirect 236 | golang.org/x/mod v0.24.0 // indirect 237 | golang.org/x/net v0.40.0 // indirect 238 | golang.org/x/oauth2 v0.30.0 // indirect 239 | golang.org/x/sys v0.33.0 // indirect 240 | golang.org/x/term v0.32.0 // indirect 241 | golang.org/x/text v0.25.0 // indirect 242 | golang.org/x/time v0.11.0 // indirect 243 | golang.org/x/tools/go/vcs v0.1.0-deprecated // indirect 244 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 245 | google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect 246 | google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect 247 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect 248 | google.golang.org/grpc v1.72.2 // indirect 249 | google.golang.org/protobuf v1.36.6 // indirect 250 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 251 | gopkg.in/inf.v0 v0.9.1 // indirect 252 | gopkg.in/ini.v1 v1.67.0 // indirect 253 | gopkg.in/warnings.v0 v0.1.2 // indirect 254 | gopkg.in/yaml.v2 v2.4.0 // indirect 255 | gopkg.in/yaml.v3 v3.0.1 // indirect 256 | k8s.io/api v0.32.2 // indirect 257 | k8s.io/apimachinery v0.33.1 // indirect 258 | k8s.io/client-go v0.32.2 // indirect 259 | k8s.io/klog/v2 v2.130.1 // indirect 260 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 261 | k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect 262 | modernc.org/libc v1.61.13 // indirect 263 | modernc.org/mathutil v1.7.1 // indirect 264 | modernc.org/memory v1.8.2 // indirect 265 | modernc.org/sqlite v1.36.2 // indirect 266 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 267 | sigs.k8s.io/randfill v1.0.0 // indirect 268 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 269 | sigs.k8s.io/yaml v1.4.0 // indirect 270 | ) 271 | -------------------------------------------------------------------------------- /internal/cmd/attest.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "encoding/base64" 21 | "errors" 22 | "fmt" 23 | "os" 24 | 25 | "github.com/sirupsen/logrus" 26 | "github.com/spf13/cobra" 27 | "sigs.k8s.io/release-utils/util" 28 | "sigs.k8s.io/tejolote/pkg/watcher" 29 | ) 30 | 31 | type attestOptions struct { 32 | waitForBuild bool 33 | sign bool 34 | continueExisting string 35 | vcsurl string 36 | encodedExisting string 37 | encodedSnapshots string 38 | artifacts []string 39 | } 40 | 41 | func (o *attestOptions) Verify() error { 42 | if o.encodedExisting != "" && o.continueExisting != "" { 43 | return errors.New("only --encoded-existing or --continue can be set at a time") 44 | } 45 | return nil 46 | } 47 | 48 | func addAttest(parentCmd *cobra.Command) { 49 | attestOpts := attestOptions{} 50 | var outputOpts *outputOptions 51 | 52 | attestCmd := &cobra.Command{ 53 | Short: "Attest to a build system run", 54 | Long: `tejolote attest buildsys://build-run/identifier 55 | 56 | The run subcommand os tejolote executes a process intended to 57 | transform files. Generally this happens as part of a build, patching 58 | or cloning repositories. 59 | 60 | Tejolote will monitor for changes that occurred during the command 61 | execution and will attest to them to generate provenance data of 62 | where they came from. 63 | 64 | `, 65 | Use: "attest", 66 | SilenceUsage: false, 67 | PersistentPreRunE: initLogging, 68 | RunE: func(_ *cobra.Command, args []string) (err error) { 69 | if len(args) == 0 { 70 | return errors.New("build run spec URL not specified") 71 | } 72 | 73 | if err := attestOpts.Verify(); err != nil { 74 | return fmt.Errorf("verifying options: %w", err) 75 | } 76 | 77 | w, err := watcher.New(args[0]) 78 | if err != nil { 79 | return fmt.Errorf("building watcher") 80 | } 81 | 82 | w.Builder.VCSURL = attestOpts.vcsurl 83 | 84 | w.Options.WaitForBuild = attestOpts.waitForBuild 85 | if !attestOpts.waitForBuild { 86 | logrus.Warn("watcher will not wait for build, data may be incomplete") 87 | } 88 | 89 | // Add artifact monitors to the watcher 90 | for _, uri := range attestOpts.artifacts { 91 | if err := w.AddArtifactSource(uri); err != nil { 92 | return fmt.Errorf("adding artifacts source: %w", err) 93 | } 94 | } 95 | 96 | // Get the run from the build system 97 | r, err := w.GetRun(args[0]) 98 | if err != nil { 99 | return fmt.Errorf("fetching run: %w", err) 100 | } 101 | 102 | // Watch the run run :) 103 | if err := w.Watch(r); err != nil { 104 | return fmt.Errorf("generating attestation: %w", err) 105 | } 106 | 107 | if attestOpts.encodedExisting != "" { 108 | f, err := os.CreateTemp("", "attestation-*.intoto.json") 109 | if err != nil { 110 | return fmt.Errorf("marshallling encoded attestation: %w", err) 111 | } 112 | defer f.Close() 113 | decodedAtt, err := base64.StdEncoding.DecodeString(attestOpts.encodedExisting) 114 | if err != nil { 115 | return fmt.Errorf("decoding existing attestation") 116 | } 117 | if err := os.WriteFile(f.Name(), decodedAtt, os.FileMode(0o644)); err != nil { 118 | return fmt.Errorf("writing encoded attestation to disk") 119 | } 120 | attestOpts.continueExisting = f.Name() 121 | } 122 | 123 | if attestOpts.encodedSnapshots != "" { 124 | f, err := os.CreateTemp("", "snapshots-*.intoto.json") 125 | if err != nil { 126 | return fmt.Errorf("marshallling encoded snapshots: %w", err) 127 | } 128 | defer f.Close() 129 | decodedSnaps, err := base64.StdEncoding.DecodeString(attestOpts.encodedSnapshots) 130 | if err != nil { 131 | return fmt.Errorf("decoding received snapshots: %w", err) 132 | } 133 | if err := os.WriteFile(f.Name(), decodedSnaps, os.FileMode(0o644)); err != nil { 134 | return fmt.Errorf("writing encoded attestation to disk") 135 | } 136 | outputOpts.SnapshotStatePath = f.Name() 137 | } 138 | 139 | if err = w.LoadAttestation(attestOpts.continueExisting); err != nil { 140 | return fmt.Errorf("loading previous attestation") 141 | } 142 | 143 | if util.Exists(outputOpts.FinalSnapshotStatePath(attestOpts.continueExisting)) { 144 | if err := w.LoadSnapshots( 145 | outputOpts.FinalSnapshotStatePath(attestOpts.continueExisting), 146 | ); err != nil { 147 | return fmt.Errorf("loading storage snapshots: %w", err) 148 | } 149 | } 150 | 151 | if err := w.CollectArtifacts(r); err != nil { 152 | return fmt.Errorf("while collecting run artifacts: %w", err) 153 | } 154 | 155 | attestation, err := w.AttestRun(r) 156 | if err != nil { 157 | return fmt.Errorf("generating run attestation: %w", err) 158 | } 159 | 160 | var json []byte 161 | 162 | if attestOpts.sign { 163 | json, err = attestation.Sign() 164 | } else { 165 | json, err = attestation.ToJSON() 166 | } 167 | 168 | if err != nil { 169 | return fmt.Errorf("serializing attestation: %w", err) 170 | } 171 | 172 | if outputOpts.OutputPath != "" { 173 | if err := os.WriteFile(outputOpts.OutputPath, json, os.FileMode(0o644)); err != nil { 174 | return fmt.Errorf("writing attestation file: %w", err) 175 | } 176 | return nil 177 | } 178 | 179 | fmt.Println(string(json)) 180 | return nil 181 | }, 182 | } 183 | 184 | outputOpts = addOutputFlags(attestCmd) 185 | 186 | attestCmd.PersistentFlags().StringVar( 187 | &attestOpts.continueExisting, 188 | "continue", 189 | "", 190 | "path to a previously started attestation to continue", 191 | ) 192 | 193 | attestCmd.PersistentFlags().BoolVar( 194 | &attestOpts.sign, 195 | "sign", 196 | false, 197 | "sign the attestation", 198 | ) 199 | 200 | attestCmd.PersistentFlags().StringSliceVar( 201 | &attestOpts.artifacts, 202 | "artifacts", 203 | []string{}, 204 | "a storage URL to monitor for files", 205 | ) 206 | attestCmd.PersistentFlags().BoolVar( 207 | &attestOpts.waitForBuild, 208 | "wait", 209 | true, 210 | "when watrching the run, wait for the build to finish", 211 | ) 212 | attestCmd.PersistentFlags().StringVar( 213 | &attestOpts.vcsurl, 214 | "vcs-url", 215 | "", 216 | "append a vcs URL to the atetstation materials", 217 | ) 218 | attestCmd.PersistentFlags().StringVar( 219 | &attestOpts.encodedExisting, 220 | "encoded-attestation", 221 | "", 222 | "encoded attestation to continue", 223 | ) 224 | attestCmd.PersistentFlags().StringVar( 225 | &attestOpts.encodedSnapshots, 226 | "encoded-snapshots", 227 | "", 228 | "encoded snapshots to continue", 229 | ) 230 | 231 | _ = attestCmd.PersistentFlags().MarkHidden("encoded-attestation") 232 | _ = attestCmd.PersistentFlags().MarkHidden("encoded-snapshots") 233 | 234 | parentCmd.AddCommand(attestCmd) 235 | } 236 | -------------------------------------------------------------------------------- /internal/cmd/flags.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "strings" 21 | 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | type outputOptions struct { 26 | OutputPath string 27 | SnapshotStatePath string 28 | Workspace string 29 | } 30 | 31 | // FinalSnapshotStatePath returns the final path to store/read the storage 32 | // snapshots. The default mode is to store it by appending '.storage-snap.json' 33 | // to the defaultSeed filename. 34 | // It will always return a preset path in SnapshotStatePath 35 | // A blank seed means do not store the data. 36 | func (oo *outputOptions) FinalSnapshotStatePath(defaultSeed string) string { 37 | snapshotState := oo.SnapshotStatePath 38 | if oo.SnapshotStatePath == "default" { 39 | if defaultSeed == "" { 40 | return "" 41 | } 42 | snapshotState = strings.TrimSuffix(defaultSeed, ".json") + ".storage-snap.json" 43 | } 44 | return snapshotState 45 | } 46 | 47 | func addOutputFlags(command *cobra.Command) *outputOptions { 48 | opts := &outputOptions{} 49 | command.PersistentFlags().StringVar( 50 | &opts.OutputPath, 51 | "output", 52 | "", 53 | "file to store the partial attestation (instead of STDOUT)", 54 | ) 55 | command.PersistentFlags().StringVar( 56 | &opts.SnapshotStatePath, 57 | "snapshots", 58 | "default", 59 | "path to store the storage snapshots state", 60 | ) 61 | return opts 62 | } 63 | -------------------------------------------------------------------------------- /internal/cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/sirupsen/logrus" 23 | "github.com/spf13/cobra" 24 | "sigs.k8s.io/release-utils/log" 25 | "sigs.k8s.io/release-utils/version" 26 | ) 27 | 28 | func Execute() error { 29 | rootCmd := &cobra.Command{ 30 | Short: "A tool for working with SPDX manifests", 31 | Long: `tejolote (the handle of a molcajete, where you make salsa) 32 | 33 | 🌶 tejolote is a utility that allows a developer to execute a 34 | process - ideally a builder - and record its inputs and outputs. 35 | The main goal is to obtain provenance information of builds 36 | and other transformations when building and shipping software. 37 | 38 | In its simplest form, you can precede your existing build 39 | command with tejolote run and it will make its best to create a 40 | meaningful attestation. For example: 41 | 42 | If your build command is: 43 | make build 44 | 45 | Change it with: 46 | tejolote run make build 47 | 48 | Tejolote will try to make sane asumptions but for best results, it 49 | allows for full control of the process you run. 50 | 51 | `, 52 | Use: "tejolote", 53 | SilenceUsage: false, 54 | PersistentPreRunE: initLogging, 55 | } 56 | 57 | rootCmd.PersistentFlags().StringVar( 58 | &commandLineOpts.logLevel, 59 | "log-level", 60 | "info", 61 | fmt.Sprintf("the logging verbosity, either %s", log.LevelNames()), 62 | ) 63 | 64 | addRun(rootCmd) 65 | addAttest(rootCmd) 66 | addStart(rootCmd) 67 | rootCmd.AddCommand(version.WithFont("larry3d")) 68 | 69 | if err := rootCmd.Execute(); err != nil { 70 | logrus.Fatal(err) 71 | return err 72 | } 73 | return nil 74 | } 75 | 76 | type commandLineOptions struct { 77 | logLevel string 78 | } 79 | 80 | var commandLineOpts = &commandLineOptions{} 81 | 82 | func initLogging(*cobra.Command, []string) error { 83 | return log.SetupGlobalLogger(commandLineOpts.logLevel) 84 | } 85 | -------------------------------------------------------------------------------- /internal/cmd/run.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | gexec "os/exec" 23 | "time" 24 | 25 | "github.com/sirupsen/logrus" 26 | "github.com/spf13/cobra" 27 | "sigs.k8s.io/tejolote/pkg/exec" 28 | "sigs.k8s.io/tejolote/pkg/run" 29 | ) 30 | 31 | type runOptions struct { 32 | Verbose bool 33 | CWD string 34 | OutputDirs []string 35 | } 36 | 37 | func addRun(parentCmd *cobra.Command) { 38 | runOpts := runOptions{} 39 | runCmd := &cobra.Command{ 40 | Short: "Execute one or more builder steps", 41 | Long: `tejolote run [command] 42 | 43 | The run subcommand os tejolote executes a process intended to 44 | transform files. Generally this happens as part of a build, patching 45 | or cloning repositories. 46 | 47 | Tejolote will monitor for changes that occurred during the command 48 | execution and will attest to them to generate provenance data of 49 | where they came from. 50 | 51 | `, 52 | Use: "run", 53 | SilenceUsage: false, 54 | PersistentPreRunE: initLogging, 55 | RunE: func(_ *cobra.Command, args []string) (err error) { 56 | runner := buildRunner(runOpts) 57 | 58 | step := &run.Step{} 59 | if len(args) > 0 { 60 | step, err = syntheticStepFromArgs(args...) 61 | if err != nil { 62 | return fmt.Errorf("generating step from arguments: %w", err) 63 | } 64 | } 65 | 66 | if step == nil { 67 | logrus.Warn("💣 Error. Nothing to execute.") 68 | logrus.Warn("Define something to run in the command line or define one or more steps") 69 | logrus.Warn("in a configuration file.") 70 | 71 | return errors.New("no step to run") 72 | } 73 | 74 | // What do we do with the run? 75 | run, err2 := runner.RunStep(step) 76 | if err2 != nil { 77 | return fmt.Errorf("executing step: %w", err) 78 | } 79 | 80 | logrus.Infof("Run produced %d artifacts", len(run.Artifacts)) 81 | return nil 82 | }, 83 | } 84 | 85 | runCmd.PersistentFlags().StringSliceVar( 86 | &runOpts.OutputDirs, 87 | "dir", 88 | []string{"."}, 89 | "list of directories that tejolote will monitor for output", 90 | ) 91 | 92 | runCmd.PersistentFlags().StringVarP( 93 | &runOpts.CWD, 94 | "cwd", 95 | "C", 96 | "", 97 | "directory to change when running the build", 98 | ) 99 | 100 | runCmd.PersistentFlags().BoolVar( 101 | &runOpts.Verbose, 102 | "verbose", 103 | false, 104 | "verbose output (prints commands and output)", 105 | ) 106 | 107 | parentCmd.AddCommand(runCmd) 108 | } 109 | 110 | // buildRunner returns a configured runner 111 | func buildRunner(opts runOptions) *exec.Runner { 112 | runner := exec.NewRunner() 113 | runner.Options.CWD = opts.CWD 114 | 115 | // TODO: review this 116 | //nolint: gocritic 117 | /* 118 | for _, dir := range opts.OutputDirs { 119 | store, err := store.New(dir) 120 | logrus.Infof("Watching directory: %s", dir) 121 | runner.Watchers = append(runner.Watchers, store) 122 | } 123 | */ 124 | 125 | return runner 126 | } 127 | 128 | // syntheticStepFromArgs evaluates the arguments passed to see if 129 | // they correspond to an executable which may be contrued into 130 | // a tejolote run 131 | func syntheticStepFromArgs(args ...string) (*run.Step, error) { 132 | if len(args) == 0 { 133 | return nil, errors.New("no arguments ") 134 | } 135 | 136 | // Check for executable 137 | if _, err := gexec.LookPath(args[0]); err != nil { 138 | return nil, fmt.Errorf("executable '%s' not found", args[0]) 139 | } 140 | 141 | params := []string{} 142 | if len(args) > 1 { 143 | params = args[1:] 144 | } 145 | 146 | step := run.Step{ 147 | Command: args[0], 148 | IsSuccess: false, 149 | Params: params, 150 | StartTime: time.Time{}, 151 | EndTime: time.Time{}, 152 | Environment: map[string]string{}, 153 | } 154 | 155 | return &step, nil 156 | } 157 | -------------------------------------------------------------------------------- /internal/cmd/start.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "encoding/base64" 21 | "errors" 22 | "fmt" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | 27 | "chainguard.dev/apko/pkg/vcs" 28 | "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" 29 | "github.com/sirupsen/logrus" 30 | "github.com/spf13/cobra" 31 | "sigs.k8s.io/release-utils/util" 32 | "sigs.k8s.io/tejolote/pkg/attestation" 33 | "sigs.k8s.io/tejolote/pkg/watcher" 34 | ) 35 | 36 | type startAttestationOptions struct { 37 | clone bool 38 | repo string 39 | repoPath string 40 | pubsub string 41 | vcsURL string 42 | builder string 43 | configSrcEntry string 44 | configSrcURI string 45 | configSrcDigest string 46 | artifacts []string 47 | } 48 | 49 | func (opts *startAttestationOptions) Validate() error { 50 | if opts.clone && opts.repo == "" { 51 | return errors.New("repository clone requested but no repository was specified") 52 | } 53 | 54 | if opts.clone && opts.repoPath == "" { 55 | return errors.New("repository clone requested but no repository path was specified") 56 | } 57 | return nil 58 | } 59 | 60 | func addStart(parentCmd *cobra.Command) { 61 | startAttestationOpts := &startAttestationOptions{} 62 | var outputOps *outputOptions 63 | 64 | // Verb 65 | startCmd := &cobra.Command{ 66 | Short: "Start a partial document", 67 | Use: "start", 68 | SilenceUsage: false, 69 | PersistentPreRunE: initLogging, 70 | } 71 | 72 | // Noun 73 | startAttestationCmd := &cobra.Command{ 74 | Short: "Attest to a build system run", 75 | Long: `tejolote start attestation 76 | 77 | The start command of tejolte writes a partial attestation 78 | containing initial data that can be observed before launching a 79 | build. The partial attestation is meant to be completed by 80 | tejolote once it has finished observing a build run. 81 | 82 | Whe starting an attestation, tejolote will snapshot the artifact 83 | storage locations and retake them when finishing building the 84 | provenance metadata. This allows it to "remember" the storage 85 | states to notice new artifacts. By default tejolote will store the 86 | storage state in a file with the same name as the partial 87 | attestation but with ".storage-snap.json" appended. 88 | 89 | `, 90 | Use: "attestation", 91 | SilenceUsage: false, 92 | PersistentPreRunE: initLogging, 93 | RunE: func(_ *cobra.Command, args []string) (err error) { 94 | if err := startAttestationOpts.Validate(); err != nil { 95 | return fmt.Errorf("validating options: %w", err) 96 | } 97 | 98 | if len(args) == 0 { 99 | return errors.New("build run spec URL not specified") 100 | } 101 | 102 | w, err := watcher.New(args[0]) 103 | if err != nil { 104 | return fmt.Errorf("building watcher") 105 | } 106 | 107 | // Add artifact monitors to the watcher 108 | for _, uri := range startAttestationOpts.artifacts { 109 | if err := w.AddArtifactSource(uri); err != nil { 110 | return fmt.Errorf("adding artifacts source: %w", err) 111 | } 112 | } 113 | 114 | if err := w.Snap(); err != nil { 115 | return fmt.Errorf("snapshotting the artifact repositories: %w", err) 116 | } 117 | 118 | if outputOps.FinalSnapshotStatePath(outputOps.OutputPath) == "" { 119 | if len(w.Snapshots) > 0 { 120 | logrus.Warning("Not saving storage state but artifact sources defined") 121 | } 122 | } else { 123 | if err := w.SaveSnapshots(outputOps.FinalSnapshotStatePath(outputOps.OutputPath)); err != nil { 124 | return fmt.Errorf("saving storage snapshots: %w", err) 125 | } 126 | } 127 | 128 | att := attestation.New() 129 | predicate := attestation.NewSLSAPredicate() 130 | 131 | if startAttestationOpts.clone { 132 | // TODO: Implement 133 | return fmt.Errorf("repository cloning not yet implemented") 134 | } 135 | 136 | vcsURL := startAttestationOpts.vcsURL 137 | if vcsURL == "" { 138 | vcsURL, err = readVCSURL(outputOps, startAttestationOpts) 139 | if err != nil { 140 | return fmt.Errorf("fetching VCS URL: %w", err) 141 | } 142 | } 143 | 144 | if vcsURL != "" { 145 | material := common.ProvenanceMaterial{ 146 | URI: vcsURL, 147 | Digest: map[string]string{}, 148 | } 149 | commithash := map[string]string{} 150 | if repoURL, repoDigest, ok := strings.Cut(vcsURL, "@"); ok { 151 | // The thing after the @ may not be a commit 152 | if len(repoDigest) == 40 { 153 | commithash["sha1"] = repoDigest 154 | } else { 155 | repoURL = vcsURL 156 | } 157 | material.URI = repoURL 158 | material.Digest = commithash 159 | } 160 | predicate.Materials = append(predicate.Materials, material) 161 | } 162 | 163 | att.Predicate = predicate 164 | 165 | att.Predicate.Builder.ID = startAttestationOpts.builder 166 | att.Predicate.Invocation.ConfigSource.EntryPoint = startAttestationOpts.configSrcEntry 167 | att.Predicate.Invocation.ConfigSource.URI = startAttestationOpts.configSrcURI 168 | algo, val, ok := strings.Cut(startAttestationOpts.configSrcDigest, ":") 169 | if ok { 170 | att.Predicate.Invocation.ConfigSource.Digest = common.DigestSet{ 171 | algo: val, 172 | } 173 | } 174 | 175 | json, err := att.ToJSON() 176 | if err != nil { 177 | return fmt.Errorf("serializing attestation json: %w", err) 178 | } 179 | 180 | if outputOps.OutputPath == "" { 181 | fmt.Println(string(json)) 182 | } else { 183 | err = os.WriteFile(outputOps.OutputPath, json, os.FileMode(0o644)) 184 | if err != nil { 185 | return fmt.Errorf("writing output data: %w", err) 186 | } 187 | } 188 | 189 | if startAttestationOpts.pubsub != "" { 190 | var sdata []byte 191 | if util.Exists(outputOps.FinalSnapshotStatePath(outputOps.OutputPath)) { 192 | sdata, err = os.ReadFile(outputOps.FinalSnapshotStatePath(outputOps.OutputPath)) 193 | if err != nil { 194 | return fmt.Errorf("reading snapshot data: %w", err) 195 | } 196 | } 197 | message := watcher.StartMessage{ 198 | SpecURL: w.Builder.SpecURL, 199 | Attestation: base64.StdEncoding.EncodeToString(json), 200 | Artifacts: startAttestationOpts.artifacts, 201 | ArtifactList: strings.Join(startAttestationOpts.artifacts, ","), 202 | } 203 | if sdata != nil { 204 | message.Snapshots = base64.StdEncoding.EncodeToString(sdata) 205 | } 206 | 207 | if err := w.PublishToTopic(startAttestationOpts.pubsub, message); err != nil { 208 | return fmt.Errorf("publishing message to pubsub topic: %w", err) 209 | } 210 | } 211 | 212 | return nil 213 | }, 214 | } 215 | 216 | outputOps = addOutputFlags(startAttestationCmd) 217 | 218 | startAttestationCmd.PersistentFlags().StringVar( 219 | &startAttestationOpts.repo, 220 | "repository", 221 | "", 222 | "url of repository containing the main project source", 223 | ) 224 | 225 | startAttestationCmd.PersistentFlags().StringVar( 226 | &startAttestationOpts.repoPath, 227 | "repo-path", 228 | ".", 229 | "path to the main code repository (relative to workspace)", 230 | ) 231 | 232 | startAttestationCmd.PersistentFlags().BoolVar( 233 | &startAttestationOpts.clone, 234 | "clone", 235 | false, 236 | "clone the repository", 237 | ) 238 | 239 | startAttestationCmd.PersistentFlags().StringSliceVar( 240 | &startAttestationOpts.artifacts, 241 | "artifacts", 242 | []string{}, 243 | "artifact storage locations", 244 | ) 245 | 246 | startAttestationCmd.PersistentFlags().StringVar( 247 | &startAttestationOpts.pubsub, 248 | "pubsub", 249 | "", 250 | "publish event to a pubsub topic", 251 | ) 252 | 253 | startAttestationCmd.PersistentFlags().StringVar( 254 | &startAttestationOpts.vcsURL, 255 | "vcs-url", 256 | "", 257 | "VCS locator to add to SLSA materials (if empty will be probed)", 258 | ) 259 | 260 | startAttestationCmd.PersistentFlags().StringVar( 261 | &startAttestationOpts.builder, 262 | "builder", 263 | "", 264 | "URI indicating the builder’s identity", 265 | ) 266 | 267 | startAttestationCmd.PersistentFlags().StringVar( 268 | &startAttestationOpts.configSrcEntry, 269 | "config-entrypoint", 270 | "", 271 | "path to the build configuration", 272 | ) 273 | 274 | startAttestationCmd.PersistentFlags().StringVar( 275 | &startAttestationOpts.configSrcURI, 276 | "config-uri", 277 | "", 278 | "vcs URL pointing to the build-as-code configuration driving the build", 279 | ) 280 | 281 | startAttestationCmd.PersistentFlags().StringVar( 282 | &startAttestationOpts.configSrcDigest, 283 | "config-digest", 284 | "", 285 | "commit hash of the source configutarion commit (eg sha1:14d87563d4...)", 286 | ) 287 | 288 | startCmd.AddCommand(startAttestationCmd) 289 | parentCmd.AddCommand(startCmd) 290 | } 291 | 292 | // readVCSURL checks the repository path to get the VCS url for the 293 | // materials 294 | func readVCSURL(outputOpts *outputOptions, opts *startAttestationOptions) (string, error) { 295 | if opts.repoPath == "" { 296 | return "", nil 297 | } 298 | 299 | repoPath := opts.repoPath 300 | 301 | // If its a relative URL, append the workspace 302 | if !strings.HasPrefix(opts.repoPath, string(filepath.Separator)) { 303 | repoPath = filepath.Join(outputOpts.Workspace, opts.repoPath) 304 | } 305 | 306 | repoPath, err := filepath.Abs(repoPath) 307 | if err != nil { 308 | return "", fmt.Errorf("resolving absolute path to repo: %w", err) 309 | } 310 | 311 | urlString, err := vcs.ProbeDirForVCSUrl(repoPath, repoPath) 312 | if err != nil { 313 | return "", fmt.Errorf("probing VCS URL: %w", err) 314 | } 315 | return urlString, nil 316 | } 317 | -------------------------------------------------------------------------------- /mage.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | /* 5 | Copyright 2022 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "os" 24 | 25 | "github.com/magefile/mage/mage" 26 | ) 27 | 28 | func main() { os.Exit(mage.Main()) } 29 | -------------------------------------------------------------------------------- /magefile.go: -------------------------------------------------------------------------------- 1 | //go:build mage 2 | // +build mage 3 | 4 | /* 5 | Copyright 2022 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package main 21 | 22 | import ( 23 | "errors" 24 | "fmt" 25 | "os" 26 | "path/filepath" 27 | 28 | "github.com/magefile/mage/sh" 29 | "github.com/uwu-tools/magex/pkg" 30 | 31 | "sigs.k8s.io/release-utils/mage" 32 | ) 33 | 34 | // Default target to run when none is specified 35 | // If not set, running mage will list available targets 36 | var Default = Verify 37 | 38 | const ( 39 | binDir = "bin" 40 | scriptDir = "scripts" 41 | ) 42 | 43 | var boilerplateDir = filepath.Join(scriptDir, "boilerplate") 44 | 45 | // All runs all targets for this repository 46 | func All() error { 47 | if err := Verify(); err != nil { 48 | return err 49 | } 50 | 51 | if err := Test(); err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // Test runs various test functions 59 | func Test() error { 60 | if err := mage.TestGo(true); err != nil { 61 | return err 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // Verify runs repository verification scripts 68 | func Verify() error { 69 | fmt.Println("Ensuring mage is available...") 70 | if err := pkg.EnsureMage(""); err != nil { 71 | return err 72 | } 73 | 74 | fmt.Println("Running copyright header checks...") 75 | if err := mage.VerifyBoilerplate("v0.2.5", binDir, boilerplateDir, false); err != nil { 76 | return err 77 | } 78 | 79 | fmt.Println("Running go module linter...") 80 | if err := mage.VerifyGoMod(); err != nil { 81 | return err 82 | } 83 | 84 | if err := Build(); err != nil { 85 | return err 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func GolangCILint() error { 92 | fmt.Println("Running golangci-lint...") 93 | if err := mage.RunGolangCILint("", false); err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | } 99 | 100 | // Build runs go build 101 | func Build() error { 102 | fmt.Println("Running go build...") 103 | 104 | ldFlag, err := mage.GenerateLDFlags() 105 | if err != nil { 106 | return err 107 | } 108 | 109 | os.Setenv("TEJOLOTE_LDFLAGS", ldFlag) 110 | 111 | if err := mage.VerifyBuild(scriptDir); err != nil { 112 | return err 113 | } 114 | 115 | fmt.Println("Binaries available in the output directory.") 116 | return nil 117 | } 118 | 119 | // BuildImages build tejolote image using ko 120 | func BuildImages() error { 121 | fmt.Println("Building images with ko...") 122 | 123 | gitVersion := getVersion() 124 | gitCommit := getCommit() 125 | 126 | ldFlag, err := mage.GenerateLDFlags() 127 | if err != nil { 128 | return err 129 | } 130 | os.Setenv("TEJOLOTE_LDFLAGS", ldFlag) 131 | os.Setenv("KOCACHE", "/tmp/ko") 132 | 133 | if os.Getenv("KO_DOCKER_REPO") == "" { 134 | return errors.New("missing KO_DOCKER_REPO environment variable") 135 | } 136 | 137 | return sh.RunV("ko", "build", "--bare", 138 | "--platform=all", "--tags", gitVersion, "--tags", gitCommit, 139 | "sigs.k8s.io/tejolote/cmd/tejolote") 140 | } 141 | 142 | // BuildImagesLocal build images locally and not push 143 | func BuildImagesLocal() error { 144 | fmt.Println("Building image with ko for local test...") 145 | if err := mage.EnsureKO("0.13.0"); err != nil { 146 | return err 147 | } 148 | 149 | ldFlag, err := mage.GenerateLDFlags() 150 | if err != nil { 151 | return err 152 | } 153 | 154 | os.Setenv("TEJOLOTE_LDFLAGS", ldFlag) 155 | os.Setenv("KOCACHE", "/tmp/ko") 156 | 157 | return sh.RunV("ko", "build", "--bare", 158 | "--local", "--platform=linux/amd64", 159 | "sigs.k8s.io/tejolote/cmd/tejolote") 160 | } 161 | 162 | func BuildStaging() error { 163 | fmt.Println("Ensuring mage is available...") 164 | if err := pkg.EnsureMage(""); err != nil { 165 | return err 166 | } 167 | 168 | if err := mage.EnsureKO(""); err != nil { 169 | return err 170 | } 171 | 172 | if err := BuildImages(); err != nil { 173 | return fmt.Errorf("building the images: %w", err) 174 | } 175 | 176 | return nil 177 | } 178 | 179 | func BuildBinaries() error { 180 | fmt.Println("Building binaries with goreleaser...") 181 | 182 | ldFlag, err := mage.GenerateLDFlags() 183 | if err != nil { 184 | return err 185 | } 186 | 187 | os.Setenv("TEJOLOTE_LDFLAGS", ldFlag) 188 | 189 | return sh.RunV("goreleaser", "release", "--clean") 190 | } 191 | 192 | func BuildBinariesSnapshot() error { 193 | fmt.Println("Building binaries with goreleaser in snapshot mode...") 194 | 195 | ldFlag, err := mage.GenerateLDFlags() 196 | if err != nil { 197 | return err 198 | } 199 | 200 | os.Setenv("TEJOLOTE_LDFLAGS", ldFlag) 201 | 202 | return sh.RunV("goreleaser", "release", "--clean", 203 | "--snapshot", "--skip=sign") 204 | } 205 | 206 | func Clean() { 207 | fmt.Println("Cleaning workspace...") 208 | toClean := []string{"output"} 209 | 210 | for _, clean := range toClean { 211 | sh.Rm(clean) 212 | } 213 | 214 | fmt.Println("Done.") 215 | } 216 | 217 | // getVersion gets a description of the commit, e.g. v0.30.1 (latest) or v0.30.1-32-gfe72ff73 (canary) 218 | func getVersion() string { 219 | version, _ := sh.Output("git", "describe", "--tags", "--always") 220 | if version != "" { 221 | return version 222 | } 223 | 224 | // repo without any tags in it 225 | return "v0.0.0" 226 | } 227 | 228 | // getCommit gets the hash of the current commit 229 | func getCommit() string { 230 | commit, _ := sh.Output("git", "rev-parse", "--short", "HEAD") 231 | return commit 232 | } 233 | 234 | // getGitState gets the state of the git repository 235 | func getGitState() string { 236 | _, err := sh.Output("git", "diff", "--quiet") 237 | if err != nil { 238 | return "dirty" 239 | } 240 | 241 | return "clean" 242 | } 243 | 244 | // getBuildDateTime gets the build date and time 245 | func getBuildDateTime() string { 246 | result, _ := sh.Output("git", "log", "-1", "--pretty=%ct") 247 | if result != "" { 248 | sourceDateEpoch := fmt.Sprintf("@%s", result) 249 | date, _ := sh.Output("date", "-u", "-d", sourceDateEpoch, "+%Y-%m-%dT%H:%M:%SZ") 250 | return date 251 | } 252 | 253 | date, _ := sh.Output("date", "+%Y-%m-%dT%H:%M:%SZ") 254 | return date 255 | } 256 | -------------------------------------------------------------------------------- /pkg/attestation/attestation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package attestation 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | 24 | intoto "github.com/in-toto/in-toto-golang/in_toto" 25 | "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" 26 | slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" 27 | "github.com/sirupsen/logrus" 28 | ) 29 | 30 | type ( 31 | Attestation struct { 32 | intoto.StatementHeader 33 | Predicate SLSAPredicate `json:"predicate"` 34 | } 35 | SLSAPredicate slsa.ProvenancePredicate 36 | ) 37 | 38 | func New() *Attestation { 39 | attestation := &Attestation{ 40 | StatementHeader: intoto.StatementHeader{ 41 | Type: intoto.StatementInTotoV01, 42 | PredicateType: slsa.PredicateSLSAProvenance, 43 | Subject: []intoto.Subject{}, 44 | }, 45 | } 46 | return attestation 47 | } 48 | 49 | func (att *Attestation) SLSA() *Attestation { 50 | att.Predicate = NewSLSAPredicate() 51 | return att 52 | } 53 | 54 | // NewSLSAPredicate returns a new SLSA predicate fully initialized 55 | func NewSLSAPredicate() SLSAPredicate { 56 | predicate := SLSAPredicate{ 57 | Builder: common.ProvenanceBuilder{ 58 | ID: "", // TODO: Read builder from trusted environment 59 | }, 60 | BuildType: "", 61 | Invocation: slsa.ProvenanceInvocation{ 62 | ConfigSource: slsa.ConfigSource{ 63 | URI: "", 64 | Digest: map[string]string{}, 65 | EntryPoint: "", 66 | }, 67 | Parameters: nil, 68 | Environment: nil, 69 | }, 70 | BuildConfig: nil, 71 | Metadata: &slsa.ProvenanceMetadata{ 72 | BuildInvocationID: "", 73 | BuildStartedOn: nil, 74 | BuildFinishedOn: nil, 75 | Completeness: slsa.ProvenanceComplete{ 76 | Parameters: true, 77 | Environment: false, 78 | Materials: false, 79 | }, 80 | Reproducible: false, 81 | }, 82 | Materials: []common.ProvenanceMaterial{}, 83 | } 84 | 85 | return predicate 86 | } 87 | 88 | func (att *Attestation) ToJSON() ([]byte, error) { 89 | var b bytes.Buffer 90 | enc := json.NewEncoder(&b) 91 | enc.SetIndent("", " ") 92 | enc.SetEscapeHTML(false) 93 | 94 | if err := enc.Encode(att); err != nil { 95 | return nil, fmt.Errorf("encoding spdx sbom: %w", err) 96 | } 97 | return b.Bytes(), nil 98 | } 99 | 100 | // AddMaterial add an entry to the materials 101 | func (pred *SLSAPredicate) AddMaterial(uri string, hashes map[string]string) { 102 | if pred.Materials == nil { 103 | pred.Materials = []common.ProvenanceMaterial{} 104 | } 105 | for _, m := range pred.Materials { 106 | if m.URI == uri { 107 | logrus.Warnf( 108 | "specified material %s is already in the attestation", uri, 109 | ) 110 | return 111 | } 112 | } 113 | pred.Materials = append(pred.Materials, common.ProvenanceMaterial{ 114 | URI: uri, 115 | Digest: hashes, 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /pkg/attestation/sign.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package attestation 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "fmt" 23 | "time" 24 | 25 | "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" 26 | "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" 27 | "github.com/sigstore/sigstore/pkg/signature/dsse" 28 | signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" 29 | "github.com/sigstore/sigstore/pkg/tuf" 30 | ) 31 | 32 | func (att *Attestation) Sign() ([]byte, error) { 33 | var certPath, certChainPath string 34 | 35 | ctx := context.Background() 36 | var timeout time.Duration // TODO: move to options 37 | if timeout != 0 { 38 | var cancelFn context.CancelFunc 39 | ctx, cancelFn = context.WithTimeout(ctx, timeout) 40 | defer cancelFn() 41 | } 42 | 43 | // Initialize the TUF cache to ensure we have the 44 | // latests root, otherwise proof of inclusion may fail. 45 | if err := tuf.Initialize(ctx, tuf.DefaultRemoteRoot, nil); err != nil { 46 | return nil, fmt.Errorf("initializing TUF client: %w", err) 47 | } 48 | 49 | ko := options.KeyOpts{ 50 | // KeyRef: s.options.PrivateKeyPath, 51 | // IDToken: identityToken, 52 | FulcioURL: options.DefaultFulcioURL, 53 | RekorURL: options.DefaultRekorURL, 54 | OIDCIssuer: options.DefaultOIDCIssuerURL, 55 | OIDCClientID: "sigstore", 56 | 57 | InsecureSkipFulcioVerify: false, 58 | SkipConfirmation: true, 59 | // FulcioAuthFlow: "", //nolint: gocritic 60 | } 61 | 62 | sv, err := sign.SignerFromKeyOpts(ctx, certPath, certChainPath, ko) 63 | if err != nil { 64 | return nil, fmt.Errorf("getting signer: %w", err) 65 | } 66 | defer sv.Close() 67 | 68 | // Wrap the attestation in the DSSE envelope 69 | wrapped := dsse.WrapSigner(sv, "application/vnd.in-toto+json") 70 | 71 | json, err := att.ToJSON() 72 | if err != nil { 73 | return nil, fmt.Errorf("serializing attestation to json: %w", err) 74 | } 75 | 76 | signedPayload, err := wrapped.SignMessage( 77 | bytes.NewReader(json), signatureoptions.WithContext(ctx), 78 | ) 79 | if err != nil { 80 | return nil, fmt.Errorf("signing attestation: %w", err) 81 | } 82 | 83 | fmt.Println(string(signedPayload)) 84 | return signedPayload, nil 85 | 86 | // TODO: review this 87 | //nolint: gocritic 88 | /* 89 | opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} 90 | if sv.Cert != nil { 91 | opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) 92 | } 93 | */ 94 | // Should we upload? 95 | /* 96 | // Check whether we should be uploading to the transparency log 97 | if sign.ShouldUploadToTlog(ctx, digest, force, noTlogUpload, ko.RekorURL) { 98 | bundle, err := uploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { 99 | return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) 100 | }) 101 | if err != nil { 102 | return err 103 | } 104 | opts = append(opts, static.WithBundle(bundle)) 105 | } 106 | */ 107 | } 108 | -------------------------------------------------------------------------------- /pkg/builder/builder.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package builder 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/sirupsen/logrus" 24 | "sigs.k8s.io/tejolote/pkg/attestation" 25 | "sigs.k8s.io/tejolote/pkg/builder/driver" 26 | "sigs.k8s.io/tejolote/pkg/run" 27 | "sigs.k8s.io/tejolote/pkg/store" 28 | ) 29 | 30 | type Builder struct { 31 | SpecURL string 32 | VCSURL string 33 | driver driver.BuildSystem 34 | } 35 | 36 | // New returns a new builder loaded with the driver derived from 37 | // the spec URL 38 | func New(spec string) (bldr Builder, err error) { 39 | bldr = Builder{ 40 | SpecURL: spec, 41 | } 42 | 43 | d, err := driver.NewFromSpecURL(spec) 44 | if err != nil { 45 | return bldr, fmt.Errorf("getting driver: %w", err) 46 | } 47 | 48 | bldr.driver = d 49 | return bldr, nil 50 | } 51 | 52 | func (b *Builder) Snap() error { 53 | return nil 54 | } 55 | 56 | func (b *Builder) GetRun(identifier string) (*run.Run, error) { 57 | return b.driver.GetRun(identifier) 58 | } 59 | 60 | // RefreshRun refreshes a run with the latest data from 61 | // the build system 62 | func (b *Builder) RefreshRun(r *run.Run) error { 63 | return b.driver.RefreshRun(r) 64 | } 65 | 66 | func (b *Builder) BuildPredicate(r *run.Run, draft *attestation.SLSAPredicate) (*attestation.SLSAPredicate, error) { 67 | pred, err := b.driver.BuildPredicate(r, draft) 68 | if err != nil { 69 | return nil, err 70 | } 71 | // If there is a VCS URL set, add it to the predicate 72 | if b.VCSURL != "" { 73 | commithash := map[string]string{} 74 | u, commit, ok := strings.Cut(b.VCSURL, "@") 75 | if ok { 76 | // The thing after the @ may not be a commit 77 | if len(commit) == 40 { 78 | commithash["sha1"] = commit 79 | } else { 80 | u = b.VCSURL 81 | } 82 | pred.AddMaterial(u, commithash) 83 | } else { 84 | logrus.Warn("unable to read commit from vcs url") 85 | pred.AddMaterial(u, commithash) 86 | } 87 | } 88 | 89 | if r.BuildPoint != nil { 90 | pred.AddMaterial(r.BuildPoint.GetUri(), r.BuildPoint.GetDigest()) 91 | } 92 | return pred, nil 93 | } 94 | 95 | func (b *Builder) ArtifactStores() []store.Store { 96 | return b.driver.ArtifactStores() 97 | } 98 | -------------------------------------------------------------------------------- /pkg/builder/driver/driver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "fmt" 21 | "net/url" 22 | 23 | "sigs.k8s.io/tejolote/pkg/attestation" 24 | "sigs.k8s.io/tejolote/pkg/run" 25 | "sigs.k8s.io/tejolote/pkg/store" 26 | ) 27 | 28 | const ( 29 | GITHUB = "github" 30 | ) 31 | 32 | // BuildSystemDriver is an interface to a type that can query a buildsystem 33 | // for data required to build a provenance attestation 34 | type BuildSystem interface { 35 | GetRun(string) (*run.Run, error) 36 | RefreshRun(*run.Run) error 37 | BuildPredicate(*run.Run, *attestation.SLSAPredicate) (*attestation.SLSAPredicate, error) 38 | ArtifactStores() []store.Store 39 | } 40 | 41 | func NewFromSpecURL(specURL string) (BuildSystem, error) { 42 | u, err := url.Parse(specURL) 43 | if err != nil { 44 | return nil, fmt.Errorf("parsing run spec URL: %w", err) 45 | } 46 | 47 | var driver BuildSystem 48 | switch u.Scheme { 49 | case "gcb": 50 | driver, err = NewGCB(specURL) 51 | if err != nil { 52 | return nil, fmt.Errorf("creating GCB driver: %w", err) 53 | } 54 | case GITHUB: 55 | driver = &GitHubWorkflow{} 56 | default: 57 | return nil, fmt.Errorf("unable to get driver from url %s", specURL) 58 | } 59 | return driver, nil 60 | } 61 | 62 | func NewFromMoniker(moniker string) (BuildSystem, error) { 63 | var driver BuildSystem 64 | switch moniker { 65 | case "gcb": 66 | driver = &GCB{} 67 | case GITHUB: 68 | driver = &GitHubWorkflow{} 69 | default: 70 | return nil, fmt.Errorf("unable to get driver from moniker %s", moniker) 71 | } 72 | return driver, nil 73 | } 74 | -------------------------------------------------------------------------------- /pkg/builder/driver/gcb.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | /* 20 | This is not yet implemented, but probably we should use the full 21 | URL as handled internally in the GCP API, eg: 22 | projects/648026197307/locations/global/builds/ba067a55-6090-4080-bc1a-6d1ff944fd60 23 | 24 | */ 25 | 26 | import ( 27 | "context" 28 | "fmt" 29 | "net/url" 30 | "strings" 31 | "time" 32 | 33 | "github.com/sirupsen/logrus" 34 | "google.golang.org/api/cloudbuild/v1" 35 | "sigs.k8s.io/tejolote/pkg/attestation" 36 | "sigs.k8s.io/tejolote/pkg/run" 37 | "sigs.k8s.io/tejolote/pkg/store" 38 | ) 39 | 40 | type GCB struct { 41 | ProjectID string 42 | BuildID string 43 | } 44 | 45 | func NewGCB(specURL string) (*GCB, error) { 46 | project, build, err := parseGCBURL(specURL) 47 | if err != nil { 48 | return nil, fmt.Errorf("parsing gcb url: %w", err) 49 | } 50 | 51 | return &GCB{ 52 | ProjectID: project, 53 | BuildID: build, 54 | }, nil 55 | } 56 | 57 | func (gcb *GCB) GetRun(specURL string) (*run.Run, error) { 58 | r := &run.Run{ 59 | SpecURL: specURL, 60 | IsSuccess: false, 61 | Steps: []run.Step{}, 62 | Artifacts: []run.Artifact{}, 63 | StartTime: time.Time{}, 64 | EndTime: time.Time{}, 65 | } 66 | if err := gcb.RefreshRun(r); err != nil { 67 | return nil, fmt.Errorf("doing initial refresh of run data: %w", err) 68 | } 69 | return r, nil 70 | 71 | // TODO: review this 72 | //nolint: gocritic 73 | /* 74 | req, err := cloudbuildService.Projects.Builds.List(project).Filter(tagsFilter).PageSize(50).Do() 75 | if err != nil { 76 | return nil, fmt.Errorf("failed to listing the builds: %w", err) 77 | } 78 | */ 79 | } 80 | 81 | func parseGCBURL(gcbURL string) (host, path string, err error) { 82 | // Fetch the required data to get the build from the URL 83 | u, err := url.Parse(gcbURL) 84 | if err != nil { 85 | return "", "", fmt.Errorf("parsing GCB spec URL: %w", err) 86 | } 87 | return u.Hostname(), strings.TrimPrefix(u.Path, "/"), nil 88 | } 89 | 90 | // RefreshRun queries the API from the build system and 91 | // updates the run metadata. 92 | func (gcb *GCB) RefreshRun(r *run.Run) error { 93 | project, buildID, err := parseGCBURL(r.SpecURL) 94 | if err != nil { 95 | return fmt.Errorf("parsing GCB spec URL: %w", err) 96 | } 97 | 98 | ctx := context.Background() 99 | cloudbuildService, err := cloudbuild.NewService(ctx) 100 | if err != nil { 101 | return fmt.Errorf("creating cloudbuild client: %w", err) 102 | } 103 | build, err := cloudbuildService.Projects.Builds.Get(project, buildID).Do() 104 | if err != nil { 105 | return fmt.Errorf("getting build %s from GCB: %w", buildID, err) 106 | } 107 | logrus.Debugf("%+v", build) 108 | r.Params = []string{} 109 | for k, v := range build.Substitutions { 110 | r.Params = append(r.Params, fmt.Sprintf("%s=%s", k, v)) 111 | } 112 | 113 | for i, s := range build.Steps { 114 | logrus.Debugf("Step #%d %+v", i, s) 115 | if len(r.Steps) <= i { 116 | r.Steps = append(r.Steps, run.Step{ 117 | Params: []string{}, 118 | Environment: map[string]string{}, 119 | }) 120 | } 121 | // 122 | r.Steps[i].Image = s.Name 123 | r.Steps[i].Params = s.Args 124 | if s.Timing != nil { 125 | if s.Timing.StartTime == "" { 126 | stime, err := time.Parse(time.RFC3339Nano, s.Timing.StartTime) 127 | if s.Timing.EndTime != "" && err != nil { 128 | return fmt.Errorf("parsing step start time") 129 | } 130 | r.Steps[i].StartTime = stime 131 | } else { 132 | etime, err := time.Parse(time.RFC3339Nano, s.Timing.EndTime) 133 | if s.Timing.EndTime != "" && err != nil { 134 | return fmt.Errorf("parsing step end time") 135 | } 136 | r.Steps[i].EndTime = etime 137 | } 138 | 139 | if s.Timing.EndTime == "" { 140 | r.Steps[i].EndTime = time.Time{} 141 | } else { 142 | etime, err := time.Parse(time.RFC3339Nano, s.Timing.EndTime) 143 | if s.Timing.EndTime != "" && err != nil { 144 | return fmt.Errorf("parsing step endtime") 145 | } 146 | r.Steps[i].EndTime = etime 147 | } 148 | } 149 | } 150 | // Set the status and the running flag. Possible values here are 151 | // Possible values: 152 | // "STATUS_UNKNOWN" - Status of the build is unknown. 153 | // "PENDING" - Build has been created and is pending execution and queuing. It has not been queued. 154 | // "QUEUED" - Build or step is queued; work has not yet begun. 155 | // "WORKING" - Build or step is being executed. 156 | // "SUCCESS" - Build or step finished successfully. 157 | // "FAILURE" - Build or step failed to complete successfully. 158 | // "INTERNAL_ERROR" - Build or step failed due to an internal cause. 159 | // "TIMEOUT" - Build or step took longer than was allowed. 160 | // "CANCELLED" - Build or step was canceled by a user. 161 | // "EXPIRED" - Build was enqueued for longer than the value of 162 | switch build.Status { 163 | case "SUCCESS": 164 | r.IsSuccess = true 165 | r.IsRunning = false 166 | case "PENDING", "QUEUED", "WORKING": 167 | r.IsSuccess = false 168 | r.IsRunning = true 169 | case "FAILURE", "INTERNAL_ERROR", "TIMEOUT", "CANCELLED", "EXPIRED": 170 | r.IsSuccess = false 171 | r.IsRunning = false 172 | } 173 | 174 | r.SystemData = build 175 | 176 | return nil 177 | } 178 | 179 | // BuildPredicate returns a SLSA predicate populated with the GCB 180 | // run data as recommended by the SLSA 0.2 spec 181 | func (gcb *GCB) BuildPredicate(r *run.Run, draft *attestation.SLSAPredicate) (predicate *attestation.SLSAPredicate, err error) { 182 | type stepData struct { 183 | Image string `json:"image"` 184 | Arguments []string `json:"arguments"` 185 | } 186 | 187 | if draft == nil { 188 | pred := attestation.NewSLSAPredicate() 189 | predicate = &pred 190 | } else { 191 | logrus.Debug("Reusing existing slsa predicate") 192 | predicate = draft 193 | } 194 | predicate.BuildType = "https://cloudbuild.googleapis.com/CloudBuildYaml@v1" 195 | buildconfig := map[string][]stepData{} 196 | 197 | buildconfig["steps"] = []stepData{} 198 | 199 | for _, s := range r.Steps { 200 | buildconfig["steps"] = append(buildconfig["steps"], stepData{ 201 | Image: s.Image, 202 | Arguments: s.Params, 203 | }) 204 | } 205 | 206 | predicate.BuildConfig = buildconfig 207 | 208 | // Get the platform specific data 209 | build, ok := r.SystemData.(*cloudbuild.Build) 210 | if ok { 211 | if build.Substitutions != nil { 212 | if c, ok := build.Substitutions["COMMIT_SHA"]; ok { 213 | predicate.Invocation.ConfigSource.Digest["sha1"] = c 214 | } 215 | if t, ok := build.Substitutions["TRIGGER_BUILD_CONFIG_PATH"]; ok { 216 | predicate.Invocation.ConfigSource.EntryPoint = t 217 | } 218 | if _, ok := build.Substitutions["REPO_NAME"]; ok { 219 | predicate.Invocation.ConfigSource.URI = fmt.Sprintf( 220 | "git+https://source.developers.google.com/p/%s/r/%s", 221 | gcb.ProjectID, build.Substitutions["REPO_NAME"], 222 | ) 223 | } 224 | } 225 | 226 | if build.ServiceAccount != "" && predicate.Builder.ID == "" { 227 | predicate.Builder.ID = build.ServiceAccount 228 | } 229 | 230 | // Check if we can extract the original repository from the trigger 231 | if build.BuildTriggerId != "" { 232 | repo, err := gcb.TriggerDetails(build.BuildTriggerId) 233 | if err == nil { 234 | predicate.Invocation.ConfigSource.URI = repo 235 | } else { 236 | logrus.Error(fmt.Errorf("fetching trigger details: %w", err)) 237 | } 238 | } 239 | } 240 | 241 | // TODO: review this 242 | // (*predicate).Invocation.ConfigSource.Digest = build.Substitutions["COMMI"] 243 | return predicate, nil 244 | } 245 | 246 | // TriggerDetails 247 | func (gcb *GCB) TriggerDetails(triggerID string) (repoURL string, err error) { 248 | cloudbuildService, err := cloudbuild.NewService(context.Background()) 249 | if err != nil { 250 | return repoURL, fmt.Errorf("creating cloudbuild client: %w", err) 251 | } 252 | trigger, err := cloudbuildService.Projects.Triggers.Get(gcb.ProjectID, triggerID).Do() 253 | if err != nil { 254 | return repoURL, fmt.Errorf("getting trigger %s from GCB: %w", triggerID, err) 255 | } 256 | 257 | // If it's a GitHub repository,return the repo URL 258 | if trigger.Github != nil { 259 | return fmt.Sprintf("git+https://github.com/%s/%s", trigger.Github.Owner, trigger.Github.Name), nil 260 | } 261 | 262 | return "", fmt.Errorf("repository not found in trigger") 263 | } 264 | 265 | // ArtifactStores returns the native artifact store of cloud build 266 | func (gcb *GCB) ArtifactStores() []store.Store { 267 | if gcb.ProjectID == "" && gcb.BuildID == "" { 268 | logrus.Error("incomplete build data to create artifact store") 269 | return []store.Store{} 270 | } 271 | d, err := store.New(fmt.Sprintf("gcb://%s/%s", gcb.ProjectID, gcb.BuildID)) 272 | if err != nil { 273 | logrus.Error(err) 274 | } 275 | return []store.Store{d} 276 | } 277 | -------------------------------------------------------------------------------- /pkg/builder/driver/gcb_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestReadStep(t *testing.T) { 26 | gcb := GCB{} 27 | 28 | r, err := gcb.GetRun("") 29 | require.Error(t, err) 30 | require.Nil(t, r) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/builder/driver/github.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "net/http" 25 | "net/url" 26 | "strconv" 27 | "strings" 28 | "time" 29 | 30 | intoto "github.com/in-toto/attestation/go/v1" 31 | "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" 32 | "github.com/sirupsen/logrus" 33 | "sigs.k8s.io/tejolote/pkg/attestation" 34 | "sigs.k8s.io/tejolote/pkg/github" 35 | "sigs.k8s.io/tejolote/pkg/run" 36 | "sigs.k8s.io/tejolote/pkg/store" 37 | ) 38 | 39 | const ghRunURL string = "https://api.github.com/repos/%s/%s/actions/runs/%d" 40 | 41 | type GitHubWorkflow struct { 42 | Organization string 43 | Repository string 44 | RunID int 45 | } 46 | 47 | func parseGitHubURL(specURL string) (org, repo string, runID int64, err error) { 48 | u, err := url.Parse(specURL) 49 | if u.Scheme != GITHUB { 50 | return org, repo, runID, errors.New("URL is not a github URL") 51 | } 52 | if err != nil { 53 | return org, repo, runID, fmt.Errorf("parsing spec url: %w", err) 54 | } 55 | parts := strings.SplitN(u.Path, "/", 3) 56 | if len(parts) != 3 { 57 | return "", "", 0, fmt.Errorf("invalid run URI") 58 | } 59 | rID, err := strconv.Atoi(strings.TrimSuffix(parts[2], "/")) 60 | if err != nil { 61 | return org, repo, runID, fmt.Errorf("parsing run ID from URL: %w", err) 62 | } 63 | 64 | return u.Hostname(), parts[1], int64(rID), nil 65 | } 66 | 67 | func (ghw *GitHubWorkflow) GetRun(specURL string) (*run.Run, error) { 68 | r := &run.Run{ 69 | SpecURL: specURL, 70 | IsSuccess: false, 71 | Steps: []run.Step{}, 72 | Artifacts: []run.Artifact{}, 73 | StartTime: time.Time{}, 74 | EndTime: time.Time{}, 75 | } 76 | if err := ghw.RefreshRun(r); err != nil { 77 | return nil, fmt.Errorf("doing initial refresh of run data: %w", err) 78 | } 79 | return r, nil 80 | } 81 | 82 | // RefreshRun queries the github API to get the latest data 83 | func (ghw *GitHubWorkflow) RefreshRun(r *run.Run) error { 84 | // https://api.github.com/repos/distroless/static/actions/runs/2858064062 85 | // https://api.github.com/repos/distroless/static/actions/runs/7492361110 (failure) 86 | org, repo, id, err := parseGitHubURL(r.SpecURL) 87 | if err != nil { 88 | return fmt.Errorf("parsing spec url: %w", err) 89 | } 90 | ghw.Organization = org 91 | ghw.Repository = repo 92 | ghw.RunID = int(id) 93 | 94 | res, err := github.APIGetRequest(fmt.Sprintf(ghRunURL, ghw.Organization, ghw.Repository, ghw.RunID)) 95 | if err != nil { 96 | return fmt.Errorf("querying github api: %w", err) 97 | } 98 | 99 | if res.StatusCode != http.StatusOK { 100 | return fmt.Errorf("got https error %d from github API", res.StatusCode) 101 | } 102 | 103 | rawData, err := io.ReadAll(res.Body) 104 | defer res.Body.Close() 105 | if err != nil { 106 | return fmt.Errorf("reading api response data: %w", err) 107 | } 108 | 109 | logrus.Debugf("Rawdata: %s", string(rawData)) 110 | 111 | runData := &github.Run{} 112 | if err := json.Unmarshal(rawData, runData); err != nil { 113 | return fmt.Errorf("unmarshalling GitHub response: %w", err) 114 | } 115 | 116 | if runData.Status == "completed" { 117 | r.IsRunning = false 118 | } 119 | 120 | switch runData.Conclusion { 121 | case "failure", "cancelled": 122 | r.IsSuccess = false 123 | case "success": 124 | r.IsSuccess = true 125 | } 126 | 127 | r.SystemData = runData 128 | 129 | r.BuildPoint = &intoto.ResourceDescriptor{ 130 | Uri: fmt.Sprintf("git+ssh://github.com/%s/%s@%s", org, repo, runData.HeadSHA), 131 | Digest: map[string]string{ 132 | "sha1": runData.HeadSHA, 133 | }, 134 | } 135 | 136 | // TODO: Consider pulling the job data if specified and the workflow yaml. 137 | // Using those we can populate the entry point better to the job, the label of 138 | // the runner 139 | 140 | return nil 141 | } 142 | 143 | // BuildPredicate builds a predicate from the run data 144 | func (ghw *GitHubWorkflow) BuildPredicate( 145 | r *run.Run, draft *attestation.SLSAPredicate, 146 | ) (predicate *attestation.SLSAPredicate, err error) { 147 | type githubEnvironment struct { 148 | // The architecture of the runner. 149 | Arch string `json:"arch"` 150 | Env map[string]string `json:"env"` 151 | // The context values that were referenced in the workflow definition. 152 | // Secrets are set to the empty string. 153 | Context struct { 154 | GitHub map[string]string `json:"github"` 155 | Runner map[string]string `json:"runner"` 156 | } `json:"context"` 157 | } 158 | org, repo, runID, err := parseGitHubURL(r.SpecURL) 159 | if err != nil { 160 | return nil, fmt.Errorf("parsing run spec URL: %w", nil) 161 | } 162 | if draft == nil { 163 | pred := attestation.NewSLSAPredicate() 164 | predicate = &pred 165 | } else { 166 | predicate = draft 167 | } 168 | 169 | predicate.Builder.ID = "https://github.com/Attestations/GitHubHostedActions@v1" 170 | predicate.BuildType = "https://github.com/Attestations/GitHubActionsWorkflow@v1" 171 | 172 | if ghrun, ok := r.SystemData.(*github.Run); ok { 173 | predicate.Invocation.ConfigSource.Digest = common.DigestSet{ 174 | "sha1": ghrun.HeadSHA, 175 | } 176 | predicate.Invocation.ConfigSource.EntryPoint = ghrun.Path 177 | } 178 | 179 | predicate.Invocation.ConfigSource.URI = fmt.Sprintf( 180 | "git+https://github.com/%s/%s.git", org, repo, 181 | ) 182 | // TODO: I think we need to checkout the file from git to fill 183 | predicate.Invocation.Environment = githubEnvironment{ 184 | Arch: "", 185 | Env: map[string]string{}, 186 | Context: struct { 187 | GitHub map[string]string `json:"github"` 188 | Runner map[string]string `json:"runner"` 189 | }{ 190 | GitHub: map[string]string{ 191 | "run_id": fmt.Sprintf("%d", runID), 192 | }, 193 | }, 194 | } 195 | return predicate, nil 196 | } 197 | 198 | // ArtifactStores returns the native artifact store of github actions 199 | func (ghw *GitHubWorkflow) ArtifactStores() []store.Store { 200 | d, err := store.New( 201 | fmt.Sprintf( 202 | "actions://%s/%s/%d", 203 | ghw.Organization, ghw.Repository, ghw.RunID, 204 | ), 205 | ) 206 | if err != nil { 207 | logrus.Error(err) 208 | return []store.Store{} 209 | } 210 | return []store.Store{d} 211 | } 212 | -------------------------------------------------------------------------------- /pkg/config/build.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | type Step struct { 20 | CommandString string `json:"command"` 21 | WorkDir string `json:"workdir"` 22 | ParamList []string `json:"params"` 23 | Env map[string]string `json:"env"` 24 | } 25 | -------------------------------------------------------------------------------- /pkg/exec/artifact.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package exec 18 | 19 | type Artifact struct{} 20 | -------------------------------------------------------------------------------- /pkg/exec/run.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package exec 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | "strings" 24 | "time" 25 | 26 | intoto "github.com/in-toto/in-toto-golang/in_toto" 27 | "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" 28 | slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" 29 | "sigs.k8s.io/release-utils/command" 30 | "sigs.k8s.io/tejolote/pkg/git" 31 | "sigs.k8s.io/tejolote/pkg/run" 32 | ) 33 | 34 | type Run struct { 35 | Executable *command.Command 36 | ExitCode int 37 | Artifacts []run.Artifact 38 | Output *command.Stream 39 | Status command.Status 40 | Command string 41 | Params []string 42 | StartTime time.Time 43 | EndTime time.Time 44 | Environment RunEnvironment 45 | } 46 | 47 | const TejoloteURI = "http://github.com/kubernetes-sigs/tejolote" 48 | 49 | type RunEnvironment struct { 50 | Variables map[string]string 51 | Directory string 52 | } 53 | 54 | // InvocationData return the invocation of the command in SLSA strcut 55 | func (r *Run) InvocationData() (slsa.ProvenanceInvocation, error) { 56 | // Invocation 57 | invocation := slsa.ProvenanceInvocation{ 58 | ConfigSource: slsa.ConfigSource{}, 59 | } 60 | 61 | // Parameters 62 | params := []string{r.Command} 63 | params = append(params, r.Params...) 64 | invocation.Parameters = params 65 | 66 | // Environment 67 | env := map[string]string{} 68 | for _, e := range os.Environ() { 69 | varData := strings.SplitN(e, "=", 2) 70 | if len(varData) == 2 { 71 | env[varData[0]] = varData[1] 72 | } 73 | } 74 | invocation.Environment = map[string]string{} 75 | 76 | // Read the git repo data 77 | if git.IsRepo(r.Environment.Directory) { 78 | repo, err := git.NewRepository(r.Environment.Directory) 79 | if err != nil { 80 | return invocation, fmt.Errorf("opening build repo: %w", err) 81 | } 82 | url, err := repo.SourceURL() 83 | if err != nil { 84 | return invocation, fmt.Errorf("opening project repository: %w", err) 85 | } 86 | 87 | commit, err := repo.HeadCommitSHA() 88 | if err != nil { 89 | return invocation, fmt.Errorf("fetching build point commit") 90 | } 91 | invocation.ConfigSource.URI = url + "@" + commit 92 | } 93 | 94 | return invocation, nil 95 | } 96 | 97 | // WriteAttestation writes the provenance attestation describing the build 98 | func (r *Run) WriteAttestation(path string) error { 99 | // Get the predicate 100 | predicate, err := r.Predicate() 101 | if err != nil { 102 | return fmt.Errorf("generating attestation: %w", err) 103 | } 104 | 105 | attestation := intoto.Statement{ 106 | StatementHeader: intoto.StatementHeader{ 107 | Type: intoto.StatementInTotoV01, 108 | PredicateType: slsa.PredicateSLSAProvenance, 109 | Subject: []intoto.Subject{}, 110 | }, 111 | Predicate: predicate, 112 | } 113 | 114 | // Add the artifacts to the attestation 115 | for _, m := range r.Artifacts { 116 | attestation.Subject = append(attestation.Subject, intoto.Subject{ 117 | Name: m.Path, 118 | Digest: m.Checksum, 119 | }) 120 | } 121 | 122 | // Create the file 123 | out, err := os.Create(path) 124 | if err != nil { 125 | return fmt.Errorf("opening attestation path %s for writing: %w", path, err) 126 | } 127 | defer out.Close() 128 | 129 | enc := json.NewEncoder(out) 130 | enc.SetIndent("", " ") 131 | enc.SetEscapeHTML(false) 132 | 133 | if err := enc.Encode(attestation); err != nil { 134 | return fmt.Errorf("encoding spdx sbom: %w", err) 135 | } 136 | return nil 137 | } 138 | 139 | func (r *Run) Predicate() (*slsa.ProvenancePredicate, error) { 140 | invocation, err := r.InvocationData() 141 | if err != nil { 142 | return nil, fmt.Errorf("reading invocation data: %w", err) 143 | } 144 | predicate := slsa.ProvenancePredicate{ 145 | Builder: common.ProvenanceBuilder{ 146 | ID: "", // TODO: Read builder from trsuted environment 147 | }, 148 | BuildType: TejoloteURI, 149 | Invocation: invocation, 150 | BuildConfig: nil, 151 | Metadata: &slsa.ProvenanceMetadata{ 152 | BuildInvocationID: "", 153 | BuildStartedOn: &r.StartTime, 154 | BuildFinishedOn: &r.EndTime, 155 | Completeness: slsa.ProvenanceComplete{ 156 | Parameters: true, 157 | Environment: false, 158 | Materials: false, 159 | }, 160 | Reproducible: false, 161 | }, 162 | Materials: []common.ProvenanceMaterial{}, 163 | } 164 | 165 | return &predicate, nil 166 | } 167 | -------------------------------------------------------------------------------- /pkg/exec/runner.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package exec 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/sirupsen/logrus" 23 | "sigs.k8s.io/tejolote/pkg/run" 24 | "sigs.k8s.io/tejolote/pkg/watcher" 25 | ) 26 | 27 | func NewRunner() *Runner { 28 | return &Runner{ 29 | Options: Options{ 30 | Logger: logrus.New(), 31 | }, 32 | implementation: &defaultRunnerImplementation{}, 33 | Watchers: []watcher.Watcher{}, 34 | } 35 | } 36 | 37 | type Runner struct { 38 | Options Options 39 | implementation RunnerImplementation 40 | Watchers []watcher.Watcher 41 | } 42 | 43 | type Options struct { 44 | Verbose bool 45 | CWD string 46 | AttestationPath string 47 | Logger *logrus.Logger 48 | } 49 | 50 | // RunStep executes a step 51 | func (r *Runner) RunStep(step *run.Step) (runner *Run, err error) { 52 | // Create the command 53 | runner, err = r.implementation.CreateRun(&r.Options, step) 54 | if err != nil { 55 | return nil, fmt.Errorf("getting step command and arguments: %w", err) 56 | } 57 | 58 | // Call the watcher to snapshot everything 59 | if err := r.implementation.Snapshot(&r.Options, &r.Watchers); err != nil { 60 | return runner, fmt.Errorf("running initial snapshots: %w", err) 61 | } 62 | 63 | if err := r.implementation.Execute(&r.Options, runner); err != nil { 64 | return nil, fmt.Errorf("executing run: %w", err) 65 | } 66 | 67 | // Call the watcher to snapshot the results 68 | if err := r.implementation.Snapshot(&r.Options, &r.Watchers); err != nil { 69 | return runner, fmt.Errorf("running final snapshots: %w", err) 70 | } 71 | 72 | // TODO: review this 73 | //nolint: gocritic 74 | /* 75 | for _, w := range r.Watchers { 76 | run.Artifacts = append(run.Artifacts, w.(*watcher.Directory).Snapshots[0].Delta(&w.(*watcher.Directory).Snapshots[1])...) 77 | } 78 | */ 79 | if err := r.implementation.WriteAttestation(&r.Options, runner); err != nil { 80 | return runner, fmt.Errorf("writing provenance attestation: %w", err) 81 | } 82 | 83 | return runner, err 84 | } 85 | -------------------------------------------------------------------------------- /pkg/exec/runner_implementation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package exec 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "strings" 23 | "time" 24 | 25 | "github.com/sirupsen/logrus" 26 | "sigs.k8s.io/release-utils/command" 27 | "sigs.k8s.io/tejolote/pkg/run" 28 | "sigs.k8s.io/tejolote/pkg/watcher" 29 | ) 30 | 31 | type RunnerImplementation interface { 32 | CreateRun(*Options, *run.Step) (*Run, error) 33 | Snapshot(*Options, *[]watcher.Watcher) error 34 | Execute(*Options, *Run) error 35 | WriteAttestation(*Options, *Run) error 36 | } 37 | 38 | type defaultRunnerImplementation struct{} 39 | 40 | // CreateRun creates a run from the data defined in the step 41 | func (ri *defaultRunnerImplementation) CreateRun(opts *Options, step *run.Step) (r *Run, err error) { 42 | var cmd *command.Command 43 | cwd := opts.CWD 44 | if opts.CWD == "" { 45 | cwd, err = os.Getwd() 46 | if err != nil { 47 | return nil, fmt.Errorf("getting current directory: %w", err) 48 | } 49 | } 50 | cmd = command.NewWithWorkDir( 51 | cwd, 52 | step.Command, 53 | step.Params..., 54 | ) 55 | 56 | r = &Run{ 57 | Executable: cmd, 58 | ExitCode: 0, 59 | Artifacts: []run.Artifact{}, 60 | Output: &command.Stream{}, 61 | Status: command.Status{}, 62 | Command: step.Command, 63 | Params: step.Params, 64 | Environment: RunEnvironment{ 65 | Directory: cwd, 66 | Variables: map[string]string{}, 67 | }, 68 | } // command.Command 69 | 70 | opts.Logger.Infof( 71 | "Executing command: %s %s", step.Command, strings.Join(step.Params, " "), 72 | ) 73 | return r, nil 74 | } 75 | 76 | func (ri *defaultRunnerImplementation) Execute(opts *Options, runner *Run) (err error) { 77 | var output *command.Stream 78 | 79 | runner.StartTime = time.Now() 80 | // Execute the run's command 81 | if opts.Verbose { 82 | output, err = runner.Executable.RunSuccessOutput() 83 | } else { 84 | output, err = runner.Executable.RunSilentSuccessOutput() 85 | } 86 | runner.EndTime = time.Now() 87 | if err != nil { 88 | return fmt.Errorf("executing run: %w", err) 89 | } 90 | 91 | runner.Output = output 92 | if opts.Verbose { 93 | logrus.Info(runner.Output) 94 | } 95 | return nil 96 | } 97 | 98 | func (ri *defaultRunnerImplementation) Snapshot(_ *Options, _ *[]watcher.Watcher) error { 99 | // TODO: review this 100 | // Take the initial snapshots 101 | //nolint: gocritic 102 | /* 103 | for i := range *watchers { 104 | if err := (*watchers)[i].Snap(); err != nil { 105 | return fmt.Errorf("snapshotting watcher: %w", err) 106 | } 107 | } 108 | */ 109 | return nil 110 | } 111 | 112 | func (ri *defaultRunnerImplementation) WriteAttestation(opts *Options, runner *Run) error { 113 | path := opts.AttestationPath 114 | if path == "" { 115 | f, err := os.CreateTemp("", "provenance-*.json") 116 | if err != nil { 117 | return fmt.Errorf("creating temp file to write attestation: %w", err) 118 | } 119 | path = f.Name() 120 | opts.Logger.Debugf("Writing attestation to temp file: %s", path) 121 | } 122 | 123 | if err := runner.WriteAttestation(path); err != nil { 124 | return fmt.Errorf("writing attestation path: %w", err) 125 | } 126 | 127 | opts.Logger.Infof("Wrote provenance attestation to %s", path) 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /pkg/exec/step.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package exec 18 | -------------------------------------------------------------------------------- /pkg/git/repository.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package git 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "path/filepath" 23 | "strings" 24 | 25 | gogit "github.com/go-git/go-git/v5" 26 | "sigs.k8s.io/release-utils/util" 27 | ) 28 | 29 | const defaultRemote = "origin" 30 | 31 | type Repository struct { 32 | repo *gogit.Repository 33 | Options Options 34 | } 35 | 36 | // IsRepo return true is a directory is a git repo 37 | func IsRepo(path string) bool { 38 | return util.Exists(filepath.Join(path, ".git")) 39 | } 40 | 41 | // NewRepository opens a git repository from the specified directory 42 | func NewRepository(dir string) (*Repository, error) { 43 | gorepo, err := gogit.PlainOpen(dir) 44 | if err != nil { 45 | return nil, fmt.Errorf("opening git repo at %s: %w", dir, err) 46 | } 47 | 48 | repo := &Repository{ 49 | repo: gorepo, 50 | Options: Options{ 51 | CWD: dir, 52 | }, 53 | } 54 | return repo, nil 55 | } 56 | 57 | type Options struct { 58 | CWD string 59 | } 60 | 61 | // SourceURL returns the repository URL 62 | func (r *Repository) SourceURL() (string, error) { 63 | remote, err := r.repo.Remote(defaultRemote) 64 | if err != nil { 65 | return "", fmt.Errorf("getting repository remote: %w", err) 66 | } 67 | 68 | if len(remote.Config().URLs) == 0 { 69 | return "", errors.New("repo remote does not have URLs") 70 | } 71 | 72 | // TODO(important) sanitize credentials in the URL 73 | url := strings.Replace(remote.Config().URLs[0], ":", "/", 1) 74 | if strings.Contains(url, "github.com") { 75 | url = strings.TrimSuffix(url, ".git") 76 | } 77 | url = fmt.Sprintf("git+ssh://%s", url) 78 | 79 | return url, nil 80 | } 81 | 82 | // Return the SHA of the commita at HEAD 83 | func (r *Repository) HeadCommitSHA() (string, error) { 84 | hash, err := r.repo.ResolveRevision("HEAD") 85 | if err != nil { 86 | return "", fmt.Errorf("fetching commit at HEAD: %w", err) 87 | } 88 | return hash.String(), err 89 | } 90 | -------------------------------------------------------------------------------- /pkg/git/repository_unit_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package git 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | func TestSourceURL(t *testing.T) { 28 | configData := `[core] 29 | repositoryformatversion = 0 30 | filemode = true 31 | bare = false 32 | logallrefupdates = true 33 | 34 | [remote "origin"] 35 | url = git@github.com:kubernetes-sigs/tejolote.git 36 | fetch = +refs/heads/*:refs/remotes/origin/* 37 | ` 38 | tmpdir := t.TempDir() 39 | 40 | // Write a minimal git config to check the remote 41 | require.NoError(t, os.Mkdir(filepath.Join(tmpdir, ".git"), os.FileMode(0o755))) 42 | require.NoError(t, os.WriteFile( 43 | filepath.Join(tmpdir, ".git", "config"), []byte(configData), os.FileMode(0o644), 44 | )) 45 | require.NoError(t, os.WriteFile( 46 | filepath.Join(tmpdir, ".git", "HEAD"), []byte("ref: refs/heads/main\n"), os.FileMode(0o644), 47 | )) 48 | 49 | repo, err := NewRepository(tmpdir) 50 | require.NoError(t, err) 51 | url, err := repo.SourceURL() 52 | require.NoError(t, err) 53 | require.Equal(t, "git+ssh://git@github.com/kubernetes-sigs/tejolote", url) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/github/github.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package github 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "os" 25 | "strings" 26 | 27 | "github.com/sirupsen/logrus" 28 | ) 29 | 30 | // TokenScopes returns the scopes of token in the eviroment 31 | func TokenScopes() ([]string, error) { 32 | res, err := APIGetRequest("https://api.github.com/repos/github/docs") 33 | if err != nil { 34 | return nil, fmt.Errorf("making request to API: %w", err) 35 | } 36 | defer res.Body.Close() 37 | 38 | header := res.Header.Get("X-Oauth-Scopes") 39 | scopes := strings.Split(header, ", ") 40 | logrus.Debugf("GitHub Token scopes: %+v", scopes) 41 | return scopes, nil 42 | } 43 | 44 | // TokenHas returns a bool if the token in use has the scope passed 45 | func TokenHas(scope string) (bool, error) { 46 | scopes, err := TokenScopes() 47 | if err != nil { 48 | return false, fmt.Errorf("reading scopes: %w", err) 49 | } 50 | for _, s := range scopes { 51 | if s == scope { 52 | return true, nil 53 | } 54 | } 55 | return false, nil 56 | } 57 | 58 | func APIGetRequest(url string) (*http.Response, error) { 59 | logrus.Debugf("GitHubAPI[GET]: %s", url) 60 | client := &http.Client{} 61 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 62 | if err != nil { 63 | return nil, fmt.Errorf("creating http request: %w", err) 64 | } 65 | req.Header.Set("Accept", "application/vnd.github+json") 66 | if os.Getenv("GITHUB_TOKEN") != "" { 67 | req.Header.Set("Authorization", fmt.Sprintf("token %s", os.Getenv("GITHUB_TOKEN"))) 68 | } else { 69 | logrus.Warn("making unauthenticated request to github") 70 | } 71 | res, err := client.Do(req) 72 | if err != nil { 73 | return res, fmt.Errorf("executing http request to GitHub API: %w", err) 74 | } 75 | if res.StatusCode != http.StatusOK { 76 | return nil, fmt.Errorf( 77 | "http error %d making request to GitHub API", res.StatusCode, 78 | ) 79 | } 80 | return res, nil 81 | } 82 | 83 | func Download(url string, f io.Writer) error { 84 | client := &http.Client{} 85 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 86 | if err != nil { 87 | return fmt.Errorf("creating http request: %w", err) 88 | } 89 | 90 | if os.Getenv("GITHUB_TOKEN") != "" { 91 | req.Header.Set("Authorization", fmt.Sprintf("token %s", os.Getenv("GITHUB_TOKEN"))) 92 | } else { 93 | logrus.Warn("making unauthenticated request to github") 94 | } 95 | 96 | resp, err := client.Do(req) 97 | if err != nil { 98 | return fmt.Errorf("executing http request to GitHub API: %w", err) 99 | } 100 | 101 | // Check server response 102 | if resp.StatusCode != http.StatusOK { 103 | return fmt.Errorf("http error when downloading: %s", resp.Status) 104 | } 105 | 106 | defer resp.Body.Close() 107 | 108 | // Writer the body to file 109 | numBytes, err := io.Copy(f, resp.Body) 110 | if err != nil { 111 | return fmt.Errorf("writing http response to disk: %w", err) 112 | } 113 | logrus.Infof("%d MB downloaded from %s", (numBytes / 1024 / 1024), url) 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/github/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package github 18 | 19 | import "time" 20 | 21 | // Artifact is the artifact structure returned by the API 22 | type Artifact struct { 23 | ID int `json:"id"` 24 | Name string `json:"name"` 25 | Size int `json:"size_in_bytes"` 26 | URL string `json:"archive_download_url"` 27 | Expired bool `json:"expired"` 28 | UpdatedAt time.Time `json:"updated_at"` 29 | } 30 | 31 | type Run struct { 32 | ID int64 `json:"id"` 33 | Status string `json:"status"` 34 | Conclusion string `json:"conclusion"` 35 | HeadBranch string `json:"head_branch"` 36 | HeadSHA string `json:"head_sha"` 37 | Path string `json:"path"` 38 | RunNumber int64 `json:"run_number"` 39 | WorkFlowID int64 `json:"workflow_id"` 40 | CreatedAt string `json:"created_at"` 41 | UpdatedAt string `json:"updated_at"` 42 | LogsURL string `json:"logs_url"` 43 | Actor Actor `json:"actor"` 44 | TriggeringActor Actor `json:"triggering_actor"` 45 | } 46 | 47 | type Actor struct { 48 | Login string `json:"login"` 49 | ID int64 `json:"id"` 50 | Type string `json:"type"` 51 | URL string `json:"url"` 52 | } 53 | -------------------------------------------------------------------------------- /pkg/run/run.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package run 18 | 19 | import ( 20 | "time" 21 | 22 | intoto "github.com/in-toto/attestation/go/v1" 23 | ) 24 | 25 | type Run struct { 26 | SpecURL string 27 | IsSuccess bool 28 | IsRunning bool 29 | Params []string 30 | Steps []Step 31 | Artifacts []Artifact 32 | StartTime time.Time 33 | EndTime time.Time 34 | BuildPoint *intoto.ResourceDescriptor 35 | SystemData any 36 | } 37 | 38 | // Step is the interface that defines the behaviour of a build step 39 | // the exec runner can execute 40 | type Step struct { 41 | Command string // Command run 42 | Image string // Container image used for the step 43 | IsSuccess bool 44 | Params []string 45 | StartTime time.Time // Start time of the step 46 | EndTime time.Time 47 | Environment map[string]string 48 | } 49 | 50 | // Artifact abstracts a file with the items we're interested in monitoring 51 | type Artifact struct { 52 | Path string 53 | Checksum map[string]string 54 | Time time.Time 55 | } 56 | -------------------------------------------------------------------------------- /pkg/sbom/sbom.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package sbom 18 | 19 | import ( 20 | "fmt" 21 | "path/filepath" 22 | "time" 23 | 24 | "sigs.k8s.io/bom/pkg/spdx" 25 | "sigs.k8s.io/release-utils/util" 26 | "sigs.k8s.io/tejolote/pkg/run" 27 | ) 28 | 29 | type Parser struct { 30 | Options Options 31 | } 32 | 33 | type Options struct { 34 | CWD string 35 | } 36 | 37 | func (parser *Parser) ReadArtifacts(path string) (*[]run.Artifact, error) { 38 | doc, err := spdx.OpenDoc(path) 39 | if err != nil { 40 | return nil, fmt.Errorf("opening doc: %w", err) 41 | } 42 | 43 | list := []run.Artifact{} 44 | 45 | for _, p := range doc.Packages { 46 | artifactPath := filepath.Join(parser.Options.CWD, p.FileName) 47 | // Only add files if the file exists 48 | if !util.Exists(artifactPath) { 49 | continue 50 | } 51 | 52 | // Prefer sha256 to match 53 | 54 | list = append(list, run.Artifact{ 55 | Path: p.FileName, 56 | Checksum: p.Checksum, 57 | Time: time.Time{}, 58 | }) 59 | } 60 | return &list, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/store/driver/actions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "net/url" 25 | "os" 26 | "path/filepath" 27 | "strconv" 28 | "strings" 29 | 30 | intoto "github.com/in-toto/attestation/go/v1" 31 | "github.com/sirupsen/logrus" 32 | "sigs.k8s.io/release-utils/hash" 33 | "sigs.k8s.io/tejolote/pkg/github" 34 | "sigs.k8s.io/tejolote/pkg/run" 35 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 36 | ) 37 | 38 | const actionsArtifactsURL = "https://api.github.com/repos/%s/%s/actions/runs/%d/artifacts" 39 | 40 | // const actionsArtifactsURL = "https://api.github.com/repos/%s/%s/actions/artifacts/%d" 41 | 42 | type Actions struct { 43 | Organization string 44 | Repository string 45 | RunID int 46 | } 47 | 48 | var ErrNoWorkflowToken = errors.New("token does not have workflow scope") 49 | 50 | func NewActions(specURL string) (*Actions, error) { 51 | u, err := url.Parse(specURL) 52 | if err != nil { 53 | return nil, fmt.Errorf("parsing SpecURL %s: %w", specURL, err) 54 | } 55 | if u.Scheme != "actions" { 56 | return nil, errors.New("spec url is not an actions run") 57 | } 58 | repo, runids, _ := strings.Cut(strings.TrimPrefix(u.Path, "/"), "/") 59 | runid, err := strconv.Atoi(runids) 60 | if err != nil { 61 | return nil, fmt.Errorf("unable to read runid from %s", u.Path) 62 | } 63 | 64 | a := &Actions{ 65 | Organization: u.Hostname(), 66 | Repository: repo, 67 | RunID: runid, 68 | } 69 | return a, nil 70 | } 71 | 72 | // readArtifacts gets the artiofacts from the run 73 | func (a *Actions) readArtifacts() ([]run.Artifact, error) { 74 | runURL := fmt.Sprintf( 75 | actionsArtifactsURL, 76 | a.Organization, a.Repository, a.RunID, 77 | ) 78 | 79 | res, err := github.APIGetRequest(runURL) 80 | if err != nil { 81 | return nil, fmt.Errorf("querying GitHub api for artifacts: %w", err) 82 | } 83 | rawData, err := io.ReadAll(res.Body) 84 | defer res.Body.Close() 85 | if err != nil { 86 | return nil, fmt.Errorf("reading api response data: %w", err) 87 | } 88 | 89 | artifacts := struct { 90 | Artifacts []github.Artifact `json:"artifacts"` 91 | }{ 92 | Artifacts: []github.Artifact{}, 93 | } 94 | 95 | if err := json.Unmarshal(rawData, &artifacts); err != nil { 96 | return nil, fmt.Errorf("unmarshalling GitHub response: %w", err) 97 | } 98 | 99 | // Now we need to download the artifacts to hash them 100 | tmpdir, err := os.MkdirTemp("", "artifacts-") 101 | if err != nil { 102 | return nil, fmt.Errorf("creating temp dir: %w", err) 103 | } 104 | 105 | ret := []run.Artifact{} 106 | 107 | for _, a := range artifacts.Artifacts { 108 | f, err := os.Create(filepath.Join(tmpdir, a.Name)) 109 | if err != nil { 110 | return nil, fmt.Errorf("creating artifact file: %w", err) 111 | } 112 | defer f.Close() 113 | if err := github.Download(a.URL, f); err != nil { 114 | return nil, fmt.Errorf( 115 | "downloading artifact from %s: %w", a.URL, err, 116 | ) 117 | } 118 | shaVal, err := hash.SHA256ForFile(f.Name()) 119 | if err != nil { 120 | return nil, fmt.Errorf("hashing file: %w", err) 121 | } 122 | ret = append(ret, run.Artifact{ 123 | Path: runURL + "/" + a.Name, 124 | Checksum: map[string]string{ 125 | string(intoto.AlgorithmSHA256): shaVal, 126 | }, 127 | Time: a.UpdatedAt, 128 | }) 129 | } 130 | logrus.Infof("%d artifacts collected from run %d", len(ret), a.RunID) 131 | return ret, nil 132 | } 133 | 134 | // Snap returns a snapshot of the current state 135 | func (a *Actions) Snap() (*snapshot.Snapshot, error) { 136 | artifacts, err := a.readArtifacts() 137 | if err != nil { 138 | return nil, fmt.Errorf("collecting artifacts: %w", err) 139 | } 140 | snap := snapshot.Snapshot{} 141 | for _, a := range artifacts { 142 | snap[a.Path] = a 143 | } 144 | return &snap, nil 145 | } 146 | -------------------------------------------------------------------------------- /pkg/store/driver/actions_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestActions(t *testing.T) { 26 | t.Skip("Need to review this test, actions expire the logs or we have a fake data or get always a fresh workflow") 27 | a, err := NewActions("actions://puerco/tejolote-test/2969514606") 28 | require.NoError(t, err) 29 | 30 | snap, err := a.Snap() 31 | require.NoError(t, err) 32 | require.Nil(t, snap) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/store/driver/attestation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/json" 23 | "fmt" 24 | "io" 25 | "net/http" 26 | "net/url" 27 | "os" 28 | "strings" 29 | 30 | intoto "github.com/in-toto/in-toto-golang/in_toto" 31 | "github.com/sirupsen/logrus" 32 | "sigs.k8s.io/tejolote/pkg/run" 33 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 34 | ) 35 | 36 | type Attestation struct { 37 | URL string 38 | } 39 | 40 | func NewAttestation(specURL string) (*Attestation, error) { 41 | u, err := url.Parse(specURL) 42 | if err != nil { 43 | return nil, fmt.Errorf("parsing attestation spec url: %w", err) 44 | } 45 | if !strings.HasPrefix(u.Scheme, "intoto+") { 46 | return nil, fmt.Errorf("spec URL %s is not an attestation url", u.Scheme) 47 | } 48 | logrus.Infof( 49 | "Initialized new in-toto attestation storage backend (%s)", specURL, 50 | ) 51 | // TODO: Check scheme to make sure it is valid 52 | return &Attestation{ 53 | URL: strings.TrimPrefix(specURL, "intoto+"), 54 | }, nil 55 | } 56 | 57 | // downloadURL universal download function 58 | // TODO: Move these to methods in each driver 59 | func downloadURL(sourceURL string, w io.Writer) error { 60 | u, err := url.Parse(sourceURL) 61 | if err != nil { 62 | return fmt.Errorf("parsing url %w", err) 63 | } 64 | ctx := context.Background() 65 | switch u.Scheme { 66 | case "gs": 67 | client, err := newGCSClient(ctx) 68 | if err != nil { 69 | return fmt.Errorf("creating GCS client: %w", err) 70 | } 71 | return downloadGCSObject(client, sourceURL, w) 72 | case "http", "https": 73 | return downloadHTTP(sourceURL, w) 74 | case "file": 75 | f, err := os.Open(strings.TrimPrefix(sourceURL, "file://")) 76 | if err != nil { 77 | return fmt.Errorf("opening file: %w", err) 78 | } 79 | defer f.Close() 80 | if _, err := io.Copy(w, f); err != nil { 81 | return fmt.Errorf("reading file data: %w", err) 82 | } 83 | return nil 84 | default: 85 | return fmt.Errorf("scheme not supported for single downloads") 86 | } 87 | } 88 | 89 | func (att *Attestation) Snap() (*snapshot.Snapshot, error) { 90 | inTotoAtt := intoto.Statement{} 91 | // Parse the attestation 92 | rawData, err := att.downloadAttestation() 93 | if err != nil { 94 | return nil, fmt.Errorf("downloading attestation data: %w", err) 95 | } 96 | 97 | // Parse the json data 98 | if err := json.Unmarshal(rawData, &inTotoAtt); err != nil { 99 | return nil, fmt.Errorf("unmarshalling attestation data: %w", err) 100 | } 101 | snap := snapshot.Snapshot{} 102 | if inTotoAtt.Subject == nil { 103 | return &snap, nil 104 | } 105 | 106 | for _, s := range inTotoAtt.Subject { 107 | snap[s.Name] = run.Artifact{ 108 | Path: s.Name, 109 | Checksum: map[string]string{}, 110 | } 111 | for h, val := range s.Digest { 112 | snap[s.Name].Checksum[h] = val 113 | } 114 | } 115 | return &snap, nil 116 | } 117 | 118 | func (att *Attestation) downloadAttestation() ([]byte, error) { 119 | var b bytes.Buffer 120 | if err := downloadURL(att.URL, &b); err != nil { 121 | return nil, fmt.Errorf("downloading attestation data: %w", err) 122 | } 123 | return b.Bytes(), nil 124 | } 125 | 126 | func downloadHTTP(urlPath string, f io.Writer) error { 127 | client := &http.Client{} 128 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, urlPath, nil) 129 | if err != nil { 130 | return fmt.Errorf("creating http request: %w", err) 131 | } 132 | 133 | resp, err := client.Do(req) 134 | if err != nil { 135 | return fmt.Errorf("executing http request to GitHub API: %w", err) 136 | } 137 | 138 | // Check server response 139 | if resp.StatusCode != http.StatusOK { 140 | return fmt.Errorf("http error when downloading: %s", resp.Status) 141 | } 142 | 143 | defer resp.Body.Close() 144 | 145 | // Writer the body to file 146 | numBytes, err := io.Copy(f, resp.Body) 147 | if err != nil { 148 | return fmt.Errorf("writing http response to disk: %w", err) 149 | } 150 | logrus.Debugf("%d MB downloaded from %s", (numBytes / 1024 / 1024), urlPath) 151 | return nil 152 | } 153 | -------------------------------------------------------------------------------- /pkg/store/driver/directory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "fmt" 21 | "net/url" 22 | "os" 23 | "path/filepath" 24 | "strings" 25 | 26 | intoto "github.com/in-toto/attestation/go/v1" 27 | "sigs.k8s.io/release-utils/hash" 28 | "sigs.k8s.io/tejolote/pkg/run" 29 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 30 | ) 31 | 32 | func NewDirectory(specURL string) (*Directory, error) { 33 | u, err := url.Parse(specURL) 34 | if err != nil { 35 | return nil, fmt.Errorf("parsing SpecURL %s: %w", specURL, err) 36 | } 37 | return &Directory{ 38 | Path: u.Path, 39 | }, nil 40 | } 41 | 42 | type Directory struct { 43 | Path string 44 | } 45 | 46 | // Snap takes a snapshot of the directory 47 | func (d *Directory) Snap() (*snapshot.Snapshot, error) { 48 | if d.Path == "" { 49 | return nil, fmt.Errorf("directory watcher has no path defined") 50 | } 51 | 52 | snap := snapshot.Snapshot{} 53 | 54 | // Walk the files in the directory 55 | if err := filepath.Walk(d.Path, 56 | func(path string, info os.FileInfo, err error) error { 57 | if err != nil { 58 | return err 59 | } 60 | if info.IsDir() { 61 | return nil 62 | } 63 | 64 | // Hash the file 65 | sha, err := hash.SHA256ForFile(path) 66 | if err != nil { 67 | return fmt.Errorf("hashing %s: %w", path, err) 68 | } 69 | 70 | // Normalize the path.... 71 | path, err = filepath.Abs(path) 72 | if err != nil { 73 | return fmt.Errorf("normalizing path %s: %w", path, err) 74 | } 75 | 76 | // .. and trim the working directory to make it relative 77 | path = strings.TrimPrefix(path, d.Path+"/") 78 | 79 | // Register the file with the path normalized 80 | snap[path] = run.Artifact{ 81 | Path: path, 82 | Checksum: map[string]string{string(intoto.AlgorithmSHA256): sha}, 83 | Time: info.ModTime(), 84 | } 85 | return nil 86 | }); err != nil { 87 | return nil, fmt.Errorf("walking directory: %w", err) 88 | } 89 | 90 | return &snap, nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/store/driver/directory_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | "time" 24 | 25 | "github.com/stretchr/testify/require" 26 | "sigs.k8s.io/tejolote/pkg/run" 27 | ) 28 | 29 | func TestDirectorySnap(t *testing.T) { 30 | // Create a fixed time to make the times deterministic 31 | fixedTime := time.Date(1976, time.Month(2), 10, 23, 30, 30, 0, time.Local) 32 | 33 | // Create some files in the directory 34 | for _, tc := range []struct { 35 | prepare func(path string) error 36 | mutate func(path string) error 37 | expect []run.Artifact 38 | }{ 39 | // Two empty directories. No error, no change 40 | { 41 | func(_ string) error { return nil }, 42 | func(_ string) error { return nil }, 43 | []run.Artifact{}, 44 | }, 45 | // One file, unchanged at mutation time 46 | { 47 | func(path string) error { 48 | return os.WriteFile(filepath.Join(path, "test.txt"), []byte("test"), os.FileMode(0o644)) 49 | }, 50 | func(_ string) error { return nil }, 51 | []run.Artifact{}, 52 | }, 53 | // One file, rewritten should be reported 54 | { 55 | func(path string) error { 56 | return os.WriteFile(filepath.Join(path, "test.txt"), []byte("test"), os.FileMode(0o644)) 57 | }, 58 | func(path string) error { 59 | filePath := filepath.Join(path, "test.txt") 60 | if err := os.WriteFile( 61 | filePath, []byte("test"), os.FileMode(0o644), 62 | ); err != nil { 63 | return err 64 | } 65 | 66 | return os.Chtimes(filePath, fixedTime, fixedTime) 67 | }, 68 | []run.Artifact{ 69 | { 70 | Path: "test.txt", 71 | Time: fixedTime, 72 | Checksum: map[string]string{"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"}, 73 | }, 74 | }, 75 | }, 76 | // One file, with contents changed 77 | { 78 | func(path string) error { 79 | filePath := filepath.Join(path, "test.txt") 80 | if err := os.WriteFile( 81 | filePath, []byte("test"), os.FileMode(0o644), 82 | ); err != nil { 83 | return err 84 | } 85 | 86 | return os.Chtimes(filePath, fixedTime, fixedTime) 87 | }, 88 | func(path string) error { 89 | filePath := filepath.Join(path, "test.txt") 90 | if err := os.WriteFile( 91 | filePath, []byte("test, but with a change!"), os.FileMode(0o644), 92 | ); err != nil { 93 | return err 94 | } 95 | 96 | return os.Chtimes(filePath, fixedTime, fixedTime) 97 | }, 98 | []run.Artifact{ 99 | { 100 | Path: "test.txt", 101 | Time: fixedTime, 102 | Checksum: map[string]string{"sha256": "76aad9c1d52e424d0dd6c6b8e07169d5d5f9001a06fe5343d4bfa13c804788f0"}, 103 | }, 104 | }, 105 | }, 106 | } { 107 | // Create a temp directory to operate in 108 | dir := t.TempDir() 109 | 110 | // Create the directory watcher 111 | sut := Directory{ 112 | Path: dir, 113 | } 114 | 115 | require.NoError(t, tc.prepare(dir)) 116 | 117 | snap1, err := sut.Snap() 118 | require.NoError(t, err, "creating first snapshot") 119 | 120 | require.NoError(t, tc.mutate(dir)) 121 | 122 | snap2, err := sut.Snap() 123 | require.NoError(t, err, "creating mutated fs snapshot") 124 | 125 | delta := snap1.Delta(snap2) 126 | require.Equal(t, delta, tc.expect) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /pkg/store/driver/gcb.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "io" 26 | "log" 27 | "net/url" 28 | "os" 29 | "strings" 30 | "sync" 31 | 32 | "cloud.google.com/go/storage" 33 | intoto "github.com/in-toto/attestation/go/v1" 34 | "github.com/sirupsen/logrus" 35 | "golang.org/x/sync/errgroup" 36 | "google.golang.org/api/cloudbuild/v1" 37 | "sigs.k8s.io/release-utils/hash" 38 | "sigs.k8s.io/tejolote/pkg/run" 39 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 40 | ) 41 | 42 | type GCB struct { 43 | ProjectID string 44 | BuildID string 45 | client *storage.Client 46 | } 47 | 48 | func NewGCB(specURL string) (*GCB, error) { 49 | u, err := url.Parse(specURL) 50 | if err != nil { 51 | return nil, fmt.Errorf("parsing GCB spec URL: %w", err) 52 | } 53 | 54 | ctx := context.Background() 55 | client, err := newGCSClient(ctx) 56 | if err != nil { 57 | return nil, fmt.Errorf("creating storage client: %w", err) 58 | } 59 | 60 | return &GCB{ 61 | ProjectID: u.Hostname(), 62 | BuildID: strings.TrimPrefix(u.Path, "/"), 63 | client: client, 64 | }, nil 65 | } 66 | 67 | func (gcb *GCB) readArtifacts() ([]run.Artifact, error) { 68 | ctx := context.Background() 69 | cloudbuildService, err := cloudbuild.NewService(ctx) 70 | if err != nil { 71 | return nil, fmt.Errorf("creating cloudbuild client: %w", err) 72 | } 73 | build, err := cloudbuildService.Projects.Builds.Get(gcb.ProjectID, gcb.BuildID).Do() 74 | if err != nil { 75 | return nil, fmt.Errorf("getting build %s from GCB: %w", gcb.BuildID, err) 76 | } 77 | manifest := build.Results.ArtifactManifest 78 | if manifest == "" { 79 | logrus.Info("no artifact manifest in run, assuming no artifacts") 80 | return []run.Artifact{}, nil 81 | } 82 | 83 | logrus.Infof("pulling artifact manifest from %s", manifest) 84 | 85 | // Get the artifacts list from th build service 86 | gcbArtifacts, err := gcb.readArtifactManifest(manifest) 87 | if err != nil { 88 | return nil, fmt.Errorf("reading build artifact manifest: %w", err) 89 | } 90 | logrus.Debugf("%+v", gcbArtifacts) 91 | 92 | // Hash the artifacts list 93 | var wg errgroup.Group 94 | var mtx sync.Mutex 95 | artifacts := []run.Artifact{} 96 | for _, artifactData := range gcbArtifacts { 97 | wg.Go(func() error { 98 | f, err := os.CreateTemp("", "artifact-temp-") 99 | if err != nil { 100 | return fmt.Errorf("creating temporary artifact file") 101 | } 102 | defer os.Remove(f.Name()) 103 | 104 | if err := downloadGCSObject(gcb.client, artifactData.Location, f); err != nil { 105 | return fmt.Errorf("downloading artifact: %w", err) 106 | } 107 | 108 | attrs, err := readGCSObjectAttributes(gcb.client, artifactData.Location) 109 | if err != nil { 110 | return fmt.Errorf("reading object artifacts: %w", err) 111 | } 112 | 113 | hashValue, err := hash.SHA256ForFile(f.Name()) 114 | if err != nil { 115 | return fmt.Errorf("hashing artifact: %w", err) 116 | } 117 | mtx.Lock() 118 | artifacts = append(artifacts, run.Artifact{ 119 | Path: artifactData.Location, 120 | Checksum: map[string]string{ 121 | string(intoto.AlgorithmSHA256): hashValue, 122 | }, 123 | Time: attrs.Updated, 124 | }) 125 | mtx.Unlock() 126 | return nil 127 | }) 128 | } 129 | if err := wg.Wait(); err != nil { 130 | return nil, fmt.Errorf("hashing artifacts: %w", err) 131 | } 132 | return artifacts, nil 133 | } 134 | 135 | func parseGCSObjectURL(objectURL string) (bucket, path string, err error) { 136 | u, err := url.Parse(objectURL) 137 | if err != nil { 138 | return bucket, path, fmt.Errorf("parsing GCS object URL: %w", err) 139 | } 140 | if u.Scheme != "gs" { 141 | return bucket, path, errors.New("url is not a cloud storage URL") 142 | } 143 | return u.Hostname(), u.Path, nil 144 | } 145 | 146 | // Abstract object data as found in the GCB artifact manifest 147 | type ghcsManifestArtifact struct { 148 | Location string `json:"location"` 149 | FileHash []struct { 150 | FileHash []struct { 151 | Type int `json:"type"` 152 | Value string `json:"value"` 153 | } `json:"file_hash"` 154 | } `json:"file_hash"` 155 | } 156 | 157 | func readGCSObjectAttributes(client *storage.Client, objectURL string) (*storage.ObjectAttrs, error) { 158 | bucket, path, err := parseGCSObjectURL(objectURL) 159 | if err != nil { 160 | return nil, fmt.Errorf("parsing GCS url: %w", err) 161 | } 162 | 163 | // Create the reader to copy data 164 | attrs, err := client.Bucket(bucket).Object(strings.TrimPrefix(path, "/")).Attrs(context.Background()) 165 | if err != nil { 166 | return nil, fmt.Errorf("creating bucket reader: %w", err) 167 | } 168 | logrus.Debugf("%+v", attrs) 169 | return attrs, nil 170 | } 171 | 172 | func downloadGCSObject(client *storage.Client, objectURL string, f io.Writer) error { 173 | bucket, path, err := parseGCSObjectURL(objectURL) 174 | if err != nil { 175 | return fmt.Errorf("parsing GCS url: %w", err) 176 | } 177 | 178 | // Create the reader to copy data 179 | rc, err := client.Bucket(bucket).Object(strings.TrimPrefix(path, "/")).NewReader(context.Background()) 180 | if err != nil { 181 | return fmt.Errorf("creating bucket reader: %w", err) 182 | } 183 | defer rc.Close() 184 | var b int64 185 | if b, err = io.Copy(f, rc); err != nil { 186 | return fmt.Errorf("copying data: %w", err) 187 | } 188 | logrus.Debugf("Wrote %d bytes from %s", b, objectURL) 189 | return nil 190 | } 191 | 192 | // Downloads the manifest from the bucket 193 | func (gcb *GCB) readArtifactManifest(manifestURL string) ([]ghcsManifestArtifact, error) { 194 | var b bytes.Buffer 195 | 196 | if err := downloadGCSObject(gcb.client, manifestURL, &b); err != nil { 197 | return nil, fmt.Errorf("reading manifest from GCS: %w", err) 198 | } 199 | 200 | dec := json.NewDecoder(strings.NewReader(b.String())) 201 | ret := []ghcsManifestArtifact{} 202 | logrus.Infof("JSON: %s", b.String()) 203 | for { 204 | var a ghcsManifestArtifact 205 | if err := dec.Decode(&a); err == io.EOF { 206 | break 207 | } else if err != nil { 208 | log.Fatal(err) 209 | } 210 | fmt.Printf("%s\n", a.Location) 211 | ret = append(ret, a) 212 | } 213 | return ret, nil 214 | } 215 | 216 | func (gcb *GCB) Snap() (*snapshot.Snapshot, error) { 217 | snap := snapshot.Snapshot{} 218 | artifacts, err := gcb.readArtifacts() 219 | if err != nil { 220 | return nil, fmt.Errorf("reading artifacts: %w", err) 221 | } 222 | 223 | for _, a := range artifacts { 224 | snap[a.Path] = a 225 | } 226 | 227 | return &snap, err 228 | } 229 | -------------------------------------------------------------------------------- /pkg/store/driver/gcb_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "testing" 21 | 22 | "cloud.google.com/go/storage" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | func TestGCB(t *testing.T) { 27 | t.Skip("Review this test") 28 | gcb, err := NewGCB("gcb://puerco-chainguard/5dda8a10-abff-4c32-b003-758eea81ac83") 29 | require.NoError(t, err) 30 | 31 | artifacts, err := gcb.readArtifacts() 32 | require.NoError(t, err) 33 | require.Nil(t, artifacts) 34 | } 35 | 36 | func TestGCSAttrs(t *testing.T) { 37 | t.Skip("Review this test") 38 | client, err := storage.NewClient(t.Context()) 39 | require.NoError(t, err) 40 | 41 | attrs, err := readGCSObjectAttributes(client, "gs://puerco-chainguard-public/test-build/7a3bd0e/README.md") 42 | require.Error(t, err) 43 | require.NotNil(t, attrs) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/store/driver/gcs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "log" 24 | "net/url" 25 | "os" 26 | "path/filepath" 27 | "strings" 28 | "time" 29 | 30 | "cloud.google.com/go/storage" 31 | "github.com/sirupsen/logrus" 32 | "golang.org/x/sync/errgroup" 33 | "google.golang.org/api/iterator" 34 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 35 | ) 36 | 37 | func NewGCS(specURL string) (*GCS, error) { 38 | u, err := url.Parse(specURL) 39 | if err != nil { 40 | return nil, fmt.Errorf("parsing SpecURL %s: %w", specURL, err) 41 | } 42 | 43 | ctx := context.Background() 44 | client, err := newGCSClient(ctx) 45 | if err != nil { 46 | return nil, fmt.Errorf("creating storage client: %w", err) 47 | } 48 | 49 | tmpdir, err := os.MkdirTemp("", "tejolote-gcs") 50 | if err != nil { 51 | return nil, fmt.Errorf("creating temporary directory") 52 | } 53 | logrus.Infof("GCS driver init: Bucket: %s Path: %s", u.Hostname(), u.Path) 54 | return &GCS{ 55 | Bucket: u.Hostname(), 56 | Path: u.Path, 57 | WorkDir: tmpdir, 58 | client: client, 59 | }, nil 60 | } 61 | 62 | func newGCSClient(ctx context.Context) (*storage.Client, error) { 63 | client, err := storage.NewClient(ctx) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return client, nil 68 | } 69 | 70 | type GCS struct { 71 | Bucket string 72 | Path string 73 | WorkDir string 74 | client *storage.Client 75 | } 76 | 77 | // syncGCSPrefix synchs a prefix in the bucket (a directory) and 78 | // calls itself recursively for internal prefixes 79 | func (gcs *GCS) syncGCSPrefix(ctx context.Context, prefix string, seen map[string]struct{}) error { 80 | logrus.WithField("driver", "gcs").Debugf("Synching bucket prefix %s", prefix) 81 | it := gcs.client.Bucket(gcs.Bucket).Objects(ctx, &storage.Query{ 82 | Delimiter: "/", 83 | Prefix: strings.TrimPrefix(prefix, "/"), 84 | }) 85 | seen[prefix] = struct{}{} 86 | filesToSync := []string{} 87 | for { 88 | attrs, err := it.Next() 89 | if errors.Is(err, iterator.Done) { 90 | logrus.WithField("driver", "gcs").Debugf("Done listing %s", gcs.Bucket) 91 | break 92 | } 93 | if err != nil { 94 | log.Fatal(err) 95 | } 96 | 97 | // If name is empty, then it is a new prefix, lets index it: 98 | if _, ok := seen[attrs.Prefix]; !ok && attrs.Name == "" { 99 | _ = gcs.syncGCSPrefix(ctx, attrs.Prefix, seen) 100 | continue 101 | } 102 | 103 | // The other is the marker file 104 | // If name is empty, then it is a new prefix, lets index it: 105 | if strings.HasSuffix(attrs.Name, "/") { 106 | trimmed := strings.TrimSuffix(attrs.Name, "/") 107 | if _, ok := seen[trimmed]; !ok { 108 | _ = gcs.syncGCSPrefix(ctx, trimmed, seen) 109 | continue 110 | } 111 | } 112 | 113 | // GCS marks "directories" by creating a zero length text file. 114 | // If we did not catch it before as a directory, then 115 | // we need to skip these or the fs sync will not work. It may 116 | // be worth saving these and synching them if there is not a 117 | // directory with the same name. 118 | if attrs.Name != "" && attrs.Size > 0 && attrs.ContentType == "text/plain" { 119 | continue 120 | } 121 | 122 | // If there is a name, it is a file 123 | if attrs.Name != "" { 124 | // TODO: Check file md5 to see if it needs sync 125 | filesToSync = append(filesToSync, attrs.Prefix+attrs.Name) 126 | } 127 | } 128 | 129 | var wg errgroup.Group 130 | for _, filename := range filesToSync { 131 | wg.Go(func() error { 132 | if err := gcs.syncGSFile(filename); err != nil { 133 | return fmt.Errorf("synching file: %w", err) 134 | } 135 | return nil 136 | }) 137 | } 138 | if err := wg.Wait(); err != nil { 139 | return fmt.Errorf("synching files: %w", err) 140 | } 141 | return nil 142 | } 143 | 144 | // syncGSFile copies a file from the bucket to local workdir 145 | func (gcs *GCS) syncGSFile(filePath string) error { 146 | logrus.WithField("driver", "gcs").Debugf("Copying file from bucket: %s", filePath) 147 | localpath := filepath.Join(gcs.WorkDir, filePath) 148 | // Ensure the directory exists 149 | _ = os.MkdirAll(filepath.Dir(localpath), os.FileMode(0o755)) 150 | 151 | // Open the local file 152 | f, err := os.OpenFile(localpath, os.O_RDWR|os.O_CREATE, 0o644) //nolint:gosec // This file should be read by others 153 | if err != nil { 154 | return fmt.Errorf("opening localfile: %w", err) 155 | } 156 | defer f.Close() 157 | 158 | objectURL := fmt.Sprintf("gs://%s/%s", gcs.Bucket, filePath) 159 | if err := downloadGCSObject(gcs.client, objectURL, f); err != nil { 160 | return fmt.Errorf("downloading object: %w", err) 161 | } 162 | 163 | attrs, err := readGCSObjectAttributes(gcs.client, objectURL) 164 | if err != nil { 165 | return fmt.Errorf("reading file attributes: %w", err) 166 | } 167 | 168 | // Set the local file time to match 169 | if err := os.Chtimes(f.Name(), time.Now(), attrs.Updated); err != nil { 170 | return fmt.Errorf("updating local file modification time: %w", err) 171 | } 172 | 173 | return nil 174 | } 175 | 176 | // Snap takes a snapshot of the directory 177 | func (gcs *GCS) Snap() (*snapshot.Snapshot, error) { 178 | if gcs.Path == "" { 179 | return nil, fmt.Errorf("gcs store has no path defined") 180 | } 181 | 182 | if gcs.Bucket == "" { 183 | return nil, fmt.Errorf("gcs store has no bucket defined") 184 | } 185 | 186 | if err := gcs.syncGCSPrefix( 187 | context.Background(), strings.TrimPrefix(gcs.Path, "/"), map[string]struct{}{}, 188 | ); err != nil { 189 | return nil, fmt.Errorf("synching bucket: %w", err) 190 | } 191 | 192 | // To snapshot the directory, we reuse the directory 193 | // store and use its artifacts 194 | dir, err := NewDirectory(fmt.Sprintf("file://%s", gcs.WorkDir)) 195 | if err != nil { 196 | return nil, fmt.Errorf("creating temp directory store: %w", err) 197 | } 198 | snapDir, err := dir.Snap() 199 | if err != nil { 200 | return nil, fmt.Errorf("snapshotting work directory: %w", err) 201 | } 202 | snap := snapshot.Snapshot{} 203 | 204 | for _, a := range *snapDir { 205 | path := "gs://" + filepath.Join(gcs.Bucket, strings.TrimPrefix(a.Path, gcs.WorkDir)) 206 | a.Path = path 207 | // Perhaps we should null the artifact dates 208 | snap[path] = a 209 | } 210 | return &snap, nil 211 | } 212 | -------------------------------------------------------------------------------- /pkg/store/driver/gcs_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestGCSSnap(t *testing.T) { 26 | t.Skip("Review this test") 27 | gcs, err := NewGCS("gs://kubernetes-release/release/v1.24.4/bin/windows/386/") 28 | require.NoError(t, err) 29 | 30 | snap, err := gcs.Snap() 31 | require.Error(t, err) 32 | require.NotNil(t, snap) 33 | } 34 | 35 | func TestSyncGSFile(t *testing.T) { 36 | t.Skip("Review this test") 37 | gcs, err := NewGCS("gs://kubernetes-release/release/v1.24.4/bin/") 38 | require.NoError(t, err) 39 | require.NoError(t, gcs.syncGSFile("release/v1.24.4/bin/windows/386/kubectl.exe.sha256")) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/store/driver/github.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "io/fs" 23 | "net/url" 24 | "os" 25 | "path/filepath" 26 | "strings" 27 | "sync" 28 | "time" 29 | 30 | "sigs.k8s.io/release-sdk/github" 31 | "sigs.k8s.io/release-utils/hash" 32 | "sigs.k8s.io/tejolote/pkg/run" 33 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 34 | ) 35 | 36 | type GitHubRelease struct { 37 | Owner string 38 | Repository string 39 | Tag string 40 | Options GitHubReleaseOptions 41 | gh *github.GitHub 42 | } 43 | 44 | type GitHubReleaseOptions struct { 45 | IgnoreExtensions []string 46 | } 47 | 48 | var DefaultGitHubReleaseOptions = GitHubReleaseOptions{ 49 | IgnoreExtensions: []string{".pem", ".sig", ".cert"}, 50 | } 51 | 52 | func NewGithub(specURL string) (*GitHubRelease, error) { 53 | u, err := url.Parse(specURL) 54 | if err != nil { 55 | return nil, fmt.Errorf("parsing github spec url: %w", err) 56 | } 57 | 58 | if u.Scheme != "github" { 59 | return nil, errors.New("spec url is not a github release url") 60 | } 61 | 62 | repoTag := strings.TrimSuffix(strings.TrimPrefix(u.Path, "/"), "/") 63 | parts := strings.Split(repoTag, "/") 64 | if len(parts) < 2 { 65 | return nil, fmt.Errorf("unable to find repo/tag in %s", u.Path) 66 | } 67 | 68 | ghr := &GitHubRelease{ 69 | Owner: u.Hostname(), 70 | Repository: parts[0], 71 | Tag: parts[1], 72 | Options: DefaultGitHubReleaseOptions, 73 | gh: github.New(), 74 | } 75 | 76 | return ghr, nil 77 | } 78 | 79 | func (ghr *GitHubRelease) Snap() (*snapshot.Snapshot, error) { 80 | // Download assets to temporary directory 81 | tmp, err := os.MkdirTemp("", "github-assets-") 82 | if err != nil { 83 | return nil, fmt.Errorf("creating temp dir: %w", err) 84 | } 85 | defer os.RemoveAll(tmp) 86 | 87 | if err := ghr.gh.DownloadReleaseAssets( 88 | ghr.Owner, ghr.Repository, []string{ghr.Tag}, tmp, 89 | ); err != nil { 90 | return nil, fmt.Errorf("downloading release assets: %w", err) 91 | } 92 | 93 | // Hash EVERYTHING 94 | snap := snapshot.Snapshot{} 95 | var mtx sync.Mutex 96 | if err := filepath.WalkDir(tmp, func(path string, d fs.DirEntry, err error) error { 97 | if err != nil { 98 | return err 99 | } 100 | if d.IsDir() { 101 | return nil 102 | } 103 | hashValue, err := hash.SHA256ForFile(path) 104 | if err != nil { 105 | return fmt.Errorf("hashing artifact: %w", err) 106 | } 107 | 108 | for _, ext := range ghr.Options.IgnoreExtensions { 109 | if strings.HasSuffix(path, ext) { 110 | return nil 111 | } 112 | } 113 | 114 | mtx.Lock() 115 | snap[filepath.Base(path)] = run.Artifact{ 116 | Path: filepath.Base(path), 117 | Checksum: map[string]string{ 118 | "sha256": hashValue, 119 | }, 120 | Time: time.Now(), // TODO: This needs to be set properly for future 121 | } 122 | mtx.Unlock() 123 | return nil 124 | }); err != nil { 125 | return nil, fmt.Errorf("walking path: %w", err) 126 | } 127 | return &snap, nil 128 | } 129 | -------------------------------------------------------------------------------- /pkg/store/driver/github_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/sirupsen/logrus" 23 | "github.com/stretchr/testify/require" 24 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 25 | ) 26 | 27 | func TestGitHubRelease(t *testing.T) { 28 | gh, err := NewGithub("github://puerco/hello/v0.0.1") 29 | require.NoError(t, err) 30 | snap, err := gh.Snap() 31 | require.NoError(t, err) 32 | require.NotNil(t, snap) 33 | ns := snapshot.Snapshot{} 34 | require.Len(t, ns.Delta(snap), 1) 35 | logrus.Infof("%+v", snap) 36 | require.Equal( 37 | t, "2dcb1895edab89c32a356e437d3c94e83fc6cde2d5a052f2e7b4051326f9ba44", 38 | (*snap)["sbom.spdx"].Checksum["sha256"], 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/store/driver/oci.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "net/url" 23 | "strings" 24 | "time" 25 | 26 | "github.com/google/go-containerregistry/pkg/authn" 27 | "github.com/google/go-containerregistry/pkg/crane" 28 | "sigs.k8s.io/tejolote/pkg/run" 29 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 30 | ) 31 | 32 | type OCI struct { 33 | Repository string 34 | Image string 35 | } 36 | 37 | func NewOCI(specURL string) (*OCI, error) { 38 | u, err := url.Parse(specURL) 39 | if err != nil { 40 | return nil, fmt.Errorf("parsing SpecURL %s: %w", specURL, err) 41 | } 42 | if u.Path == "" { 43 | return nil, errors.New("spec url is not wel formed") 44 | } 45 | oci := &OCI{} 46 | parts := strings.Split(u.Path, "/") 47 | oci.Image = parts[len(parts)-1] 48 | oci.Repository = u.Hostname() 49 | if len(parts) > 1 { 50 | oci.Repository += strings.Join(parts[0:len(parts)-1], "/") 51 | } 52 | return oci, nil 53 | } 54 | 55 | // Snap 56 | func (oci *OCI) Snap() (*snapshot.Snapshot, error) { 57 | tags, err := crane.ListTags( 58 | oci.Repository+"/"+oci.Image, crane.WithAuthFromKeychain(authn.DefaultKeychain), 59 | ) 60 | if err != nil { 61 | return nil, fmt.Errorf("fetching tags from registry: %w", err) 62 | } 63 | snap := &snapshot.Snapshot{} 64 | for _, t := range tags { 65 | (*snap)["oci://"+t] = run.Artifact{ 66 | Path: "oci://" + oci.Repository + "/" + oci.Image + ":" + t, 67 | Checksum: map[string]string{}, 68 | Time: time.Time{}, 69 | } 70 | } 71 | return snap, nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/store/driver/oci_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestOCISnapshot(t *testing.T) { 26 | oci, err := NewOCI("oci://ghcr.io/uservers/miniprow/miniprow") 27 | require.NoError(t, err) 28 | require.Equal(t, "miniprow", oci.Image) 29 | require.Equal(t, "ghcr.io/uservers/miniprow", oci.Repository) 30 | 31 | snap, err := oci.Snap() 32 | require.NoError(t, err) 33 | require.Len(t, *snap, 5) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/store/driver/spdx.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "fmt" 21 | "net/url" 22 | "os" 23 | "strings" 24 | 25 | "github.com/sirupsen/logrus" 26 | "sigs.k8s.io/bom/pkg/spdx" 27 | "sigs.k8s.io/tejolote/pkg/run" 28 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 29 | ) 30 | 31 | type SPDX struct { 32 | URL string 33 | } 34 | 35 | func NewSPDX(specURL string) (*SPDX, error) { 36 | u, err := url.Parse(specURL) 37 | if err != nil { 38 | return nil, fmt.Errorf("parsing attestation spec url: %w", err) 39 | } 40 | if !strings.HasPrefix(u.Scheme, "spdx+") { 41 | return nil, fmt.Errorf("spec URL %s is not an attestation url", u.Scheme) 42 | } 43 | 44 | logrus.Infof( 45 | "Initialized new SPDX SBOM storage backend (%s)", specURL, 46 | ) 47 | 48 | // TODO: Check scheme to make sure it is valid 49 | return &SPDX{ 50 | URL: strings.TrimPrefix(specURL, "spdx+"), 51 | }, nil 52 | } 53 | 54 | func (s *SPDX) Snap() (*snapshot.Snapshot, error) { 55 | f, err := os.CreateTemp("", "temp-sbom-") 56 | if err != nil { 57 | return nil, fmt.Errorf("creating temporary sbom file: %w", err) 58 | } 59 | defer os.Remove(f.Name()) 60 | 61 | if err := downloadURL(s.URL, f); err != nil { 62 | return nil, fmt.Errorf("downloading sbom to temp file: %w", err) 63 | } 64 | 65 | doc, err := spdx.OpenDoc(f.Name()) 66 | if err != nil { 67 | return nil, fmt.Errorf("parsing spdx sbom: %w", err) 68 | } 69 | 70 | snap := snapshot.Snapshot{} 71 | 72 | // Add the spdx packages 73 | for _, p := range doc.Packages { 74 | // First, check to see if the SBOM has a purl 75 | identifier := "" 76 | for _, ref := range p.ExternalRefs { 77 | if ref.Type == "purl" { 78 | identifier = ref.Locator 79 | break 80 | } 81 | } 82 | 83 | // If not, try download location 84 | if identifier == "" && p.DownloadLocation != "" { 85 | identifier = p.DownloadLocation 86 | } 87 | 88 | // If else fails, use the package name 89 | // TODO: Think if this works 90 | if identifier == "" { 91 | identifier = p.Name 92 | } 93 | 94 | // Should we list packages without checksums? 95 | // Leaving this commented because it breaks with the kubernetes sboms 96 | // but perhaps we should be stricter here 97 | if len(p.Checksum) == 0 { 98 | logrus.Warnf("SPDX package %s has no checksum", identifier) 99 | continue 100 | } 101 | 102 | artifact := run.Artifact{ 103 | Path: identifier, 104 | Checksum: map[string]string{}, 105 | } 106 | for algo, c := range p.Checksum { 107 | artifact.Checksum[algo] = c 108 | } 109 | 110 | snap[identifier] = artifact 111 | } 112 | return &snap, nil 113 | } 114 | -------------------------------------------------------------------------------- /pkg/store/snapshot/snapshot.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package snapshot 18 | 19 | import "sigs.k8s.io/tejolote/pkg/run" 20 | 21 | type Snapshot map[string]run.Artifact 22 | 23 | // Delta takes a snapshot, assumed to be later in time and returns 24 | // a directed delta, the files which were created or modified. 25 | func (snap *Snapshot) Delta(post *Snapshot) []run.Artifact { 26 | results := []run.Artifact{} 27 | for path, f := range *post { 28 | // If the file was not there in the first snap, add it 29 | if _, ok := (*snap)[path]; !ok { 30 | results = append(results, f) 31 | continue 32 | } 33 | 34 | // Check the file attributes to if they were changed 35 | if (*snap)[path].Time != f.Time { 36 | results = append(results, f) 37 | continue 38 | } 39 | 40 | checksum := (*snap)[path].Checksum 41 | for algo, val := range checksum { 42 | if fv, ok := f.Checksum[algo]; ok { 43 | if fv != val { 44 | results = append(results, f) 45 | break 46 | } 47 | } 48 | } 49 | } 50 | return results 51 | } 52 | -------------------------------------------------------------------------------- /pkg/store/snapshot/snapshot_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package snapshot 18 | 19 | import ( 20 | "testing" 21 | "time" 22 | 23 | "github.com/stretchr/testify/require" 24 | "sigs.k8s.io/tejolote/pkg/run" 25 | ) 26 | 27 | func TestDelta(t *testing.T) { 28 | testFile := run.Artifact{ 29 | Path: "test.txt", 30 | Checksum: map[string]string{"sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4"}, 31 | Time: time.Now(), 32 | } 33 | modHashFile := run.Artifact{ 34 | Path: "test.txt", 35 | Checksum: map[string]string{"sha256": "25b89320221dda5abe3df4624d246d22d0c820ee3598e97553611d7c80abbd36"}, 36 | Time: testFile.Time, 37 | } 38 | modTimeFile := run.Artifact{ 39 | Path: "test.txt", 40 | Checksum: map[string]string{"sha256": "25b89320221dda5abe3df4624d246d22d0c820ee3598e97553611d7c80abbd36"}, 41 | Time: time.Date(1976, time.Month(2), 10, 23, 30, 30, 0, time.Local), 42 | } 43 | for _, tc := range []struct { 44 | preSnap Snapshot 45 | postSnap Snapshot 46 | expect []run.Artifact 47 | }{ 48 | { 49 | // Empty snapshots, should be an empty list 50 | Snapshot{}, 51 | Snapshot{}, 52 | []run.Artifact{}, 53 | }, 54 | { 55 | // One removed file, should be empty list 56 | Snapshot{testFile.Path: testFile}, 57 | Snapshot{}, 58 | []run.Artifact{}, 59 | }, 60 | { 61 | // One added file, should be a list with that file 62 | Snapshot{}, 63 | Snapshot{testFile.Path: testFile}, 64 | []run.Artifact{testFile}, 65 | }, 66 | { 67 | // One file with time modded, should be a list with that file 68 | Snapshot{testFile.Path: testFile}, 69 | Snapshot{testFile.Path: modTimeFile}, 70 | []run.Artifact{modTimeFile}, 71 | }, 72 | { 73 | // One file with hash modded, should be a list with that file 74 | Snapshot{testFile.Path: testFile}, 75 | Snapshot{testFile.Path: modHashFile}, 76 | []run.Artifact{modHashFile}, 77 | }, 78 | } { 79 | require.Equal(t, tc.expect, tc.preSnap.Delta(&tc.postSnap)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/store/store.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package store 18 | 19 | import ( 20 | "fmt" 21 | "net/url" 22 | "strings" 23 | 24 | "sigs.k8s.io/tejolote/pkg/run" 25 | "sigs.k8s.io/tejolote/pkg/store/driver" 26 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 27 | ) 28 | 29 | type Store struct { 30 | SpecURL string 31 | Driver Implementation 32 | } 33 | 34 | type Implementation interface { 35 | Snap() (*snapshot.Snapshot, error) 36 | } 37 | 38 | func New(specURL string) (s Store, err error) { 39 | s = Store{} 40 | u, err := url.Parse(specURL) 41 | if err != nil { 42 | return s, fmt.Errorf("parsing storage spec URL %s: %w", specURL, err) 43 | } 44 | var impl Implementation 45 | switch u.Scheme { 46 | case "file": 47 | impl, err = driver.NewDirectory(specURL) 48 | case "gs": 49 | impl, err = driver.NewGCS(specURL) 50 | case "oci": 51 | impl, err = driver.NewOCI(specURL) 52 | case "actions": 53 | impl, err = driver.NewActions(specURL) 54 | case "gcb": 55 | impl, err = driver.NewGCB(specURL) 56 | case "github": 57 | impl, err = driver.NewGithub(specURL) 58 | default: 59 | // Attestation use a composed scheme 60 | format, _, ok := strings.Cut(u.Scheme, "+") 61 | if !ok { 62 | return s, fmt.Errorf("%s is not a storage URL", specURL) 63 | } 64 | switch format { 65 | case "intoto": 66 | impl, err = driver.NewAttestation(specURL) 67 | case "spdx": 68 | impl, err = driver.NewSPDX(specURL) 69 | default: 70 | err = fmt.Errorf("unknown storage backend %s", format) 71 | } 72 | } 73 | if err != nil { 74 | return s, fmt.Errorf("initializing storage backend: %w", err) 75 | } 76 | s.SpecURL = specURL 77 | s.Driver = impl 78 | 79 | return s, nil 80 | } 81 | 82 | // ReadArtifacts returns the combined list of artifacts from 83 | // every store attached to the watcher 84 | func (s *Store) ReadArtifacts() ([]run.Artifact, error) { 85 | artifacts := []run.Artifact{} 86 | snap, err := s.Driver.Snap() 87 | if err != nil { 88 | return artifacts, fmt.Errorf("snapshotting storage: %w", err) 89 | } 90 | for _, a := range *snap { 91 | artifacts = append(artifacts, a) 92 | } 93 | return artifacts, nil 94 | } 95 | 96 | // Snap calls the underlying driver's Snap method to capture 97 | // the current store's state into a snapshot 98 | func (s *Store) Snap() (*snapshot.Snapshot, error) { 99 | return s.Driver.Snap() 100 | } 101 | -------------------------------------------------------------------------------- /pkg/watcher/watcher.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package watcher 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "log" 26 | "os" 27 | "strings" 28 | "time" 29 | 30 | "cloud.google.com/go/pubsub" 31 | intoto "github.com/in-toto/in-toto-golang/in_toto" 32 | "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" 33 | "github.com/sirupsen/logrus" 34 | "sigs.k8s.io/tejolote/pkg/attestation" 35 | "sigs.k8s.io/tejolote/pkg/builder" 36 | "sigs.k8s.io/tejolote/pkg/run" 37 | "sigs.k8s.io/tejolote/pkg/store" 38 | "sigs.k8s.io/tejolote/pkg/store/snapshot" 39 | ) 40 | 41 | type Watcher struct { 42 | DraftAttestation *attestation.Attestation 43 | Builder builder.Builder 44 | ArtifactStores []store.Store 45 | Snapshots []map[string]*snapshot.Snapshot 46 | Options Options 47 | } 48 | 49 | type Options struct { 50 | WaitForBuild bool // When true, the watcher will keep observing the run until it's done 51 | } 52 | 53 | func New(uri string) (w *Watcher, err error) { 54 | w = &Watcher{ 55 | Options: Options{ 56 | WaitForBuild: true, // By default we watch the build run 57 | }, 58 | } 59 | 60 | // Get the builder 61 | b, err := builder.New(uri) 62 | if err != nil { 63 | return nil, fmt.Errorf("getting build watcher: %w", err) 64 | } 65 | w.Builder = b 66 | 67 | return w, nil 68 | } 69 | 70 | // GetRun returns a run from the build system 71 | func (w *Watcher) GetRun(specURL string) (*run.Run, error) { 72 | r, err := w.Builder.GetRun(specURL) 73 | if err != nil { 74 | return nil, fmt.Errorf("getting run: %w", err) 75 | } 76 | return r, nil 77 | } 78 | 79 | // Watch watches a run, updating the run data as it runs 80 | func (w *Watcher) Watch(r *run.Run) error { 81 | for { 82 | if !r.IsRunning { 83 | return nil 84 | } 85 | 86 | if !w.Options.WaitForBuild { 87 | logrus.Warn("run is still running but watcher won't wait (WaitForBuild = false)") 88 | } 89 | 90 | // Sleep to wait for a status change 91 | if err := w.Builder.RefreshRun(r); err != nil { 92 | return fmt.Errorf("refreshing run data: %w", err) 93 | } 94 | 95 | // Sleep 96 | time.Sleep(3 * time.Second) 97 | } 98 | } 99 | 100 | // LoadAttestation loads a partial attestation to complete 101 | // when a run finished running 102 | func (w *Watcher) LoadAttestation(path string) error { 103 | if path == "" { 104 | return nil 105 | } 106 | data, err := os.ReadFile(path) 107 | if err != nil { 108 | return fmt.Errorf("loading previous attestation: %w", err) 109 | } 110 | 111 | att := attestation.New().SLSA() 112 | 113 | if err := json.Unmarshal(data, &att); err != nil { 114 | return fmt.Errorf("unmarshaling attestation json: %w", err) 115 | } 116 | 117 | w.DraftAttestation = att 118 | logrus.Infof("Loaded draft attestation from %s", path) 119 | return nil 120 | } 121 | 122 | // AttestRun generates an attestation from a run tejolote can watch 123 | func (w *Watcher) AttestRun(r *run.Run) (att *attestation.Attestation, err error) { 124 | if r.IsRunning { 125 | logrus.Warn("run is still running, attestation may not capture en result") 126 | } 127 | 128 | att = attestation.New().SLSA() 129 | if w.DraftAttestation != nil { 130 | att = w.DraftAttestation 131 | } 132 | 133 | // Here, we need to check if its empty 134 | pred := &att.Predicate 135 | predicate, err := w.Builder.BuildPredicate(r, pred) 136 | if err != nil { 137 | return nil, fmt.Errorf("building predicate: %w", err) 138 | } 139 | 140 | // Add the run artifacts to the attestation 141 | for _, a := range r.Artifacts { 142 | s := intoto.Subject{ 143 | Name: a.Path, 144 | Digest: common.DigestSet{}, 145 | } 146 | for a, v := range a.Checksum { 147 | s.Digest[a] = v 148 | } 149 | att.Subject = append(att.Subject, s) 150 | } 151 | 152 | att.Predicate = *predicate 153 | return att, nil 154 | } 155 | 156 | // AddArtifactSource adds a new source to look for artifacts 157 | func (w *Watcher) AddArtifactSource(specURL string) error { 158 | s, err := store.New(specURL) 159 | if err != nil { 160 | return fmt.Errorf("getting artifact store: %w", err) 161 | } 162 | w.ArtifactStores = append(w.ArtifactStores, s) 163 | return nil 164 | } 165 | 166 | // CollectArtifacts queries the storage drivers attached to the run and 167 | // collects any artifacts found after the build is done 168 | func (w *Watcher) CollectArtifacts(r *run.Run) error { 169 | r.Artifacts = nil 170 | artifactStores := w.ArtifactStores 171 | // TODO: Support disabling the native driver 172 | artifactStores = append(artifactStores, w.Builder.ArtifactStores()...) 173 | for _, s := range artifactStores { 174 | logrus.Infof("Collecting artifacts from %s", s.SpecURL) 175 | artifacts, err := s.ReadArtifacts() 176 | if err != nil { 177 | return fmt.Errorf("collecting artfiacts from %s: %w", s.SpecURL, err) 178 | } 179 | r.Artifacts = append(r.Artifacts, artifacts...) 180 | } 181 | logrus.Infof( 182 | "Run produced %d artifacts collected from %d sources", 183 | len(r.Artifacts), len(w.ArtifactStores), 184 | ) 185 | return nil 186 | } 187 | 188 | // Snap adds a new snapshot set to the watcher by querying 189 | // each of the storage drivers 190 | func (w *Watcher) Snap() error { 191 | snaps := map[string]*snapshot.Snapshot{} 192 | for _, s := range w.ArtifactStores { 193 | if s.SpecURL == "" { 194 | return errors.New("artifact store has no spec url defined") 195 | } 196 | snap, err := s.Snap() 197 | if err != nil { 198 | return fmt.Errorf("snapshotting storage: %w", err) 199 | } 200 | snaps[s.SpecURL] = snap 201 | } 202 | // TODO: Add some metrics to measure snapshot time 203 | w.Snapshots = append(w.Snapshots, snaps) 204 | return nil 205 | } 206 | 207 | // SaveSnapshots stores the current state of the storage locations 208 | // to a file which can be reused when continuing an attestation 209 | func (w *Watcher) SaveSnapshots(path string) error { 210 | var b bytes.Buffer 211 | enc := json.NewEncoder(&b) 212 | enc.SetIndent("", " ") 213 | enc.SetEscapeHTML(false) 214 | if len(w.Snapshots) == 0 { 215 | logrus.Debug("no storage snapshots set, not saving file") 216 | return nil 217 | } 218 | if err := enc.Encode(w.Snapshots); err != nil { 219 | return fmt.Errorf("encoding snapshot data sbom: %w", err) 220 | } 221 | 222 | if err := os.WriteFile(path, b.Bytes(), os.FileMode(0o644)); err != nil { 223 | return fmt.Errorf("writing file store state: %w", err) 224 | } 225 | return nil 226 | } 227 | 228 | // LoadSnapshots loads saved snapshot state from a file to continue 229 | func (w *Watcher) LoadSnapshots(path string) error { 230 | if path == "" { 231 | return nil 232 | } 233 | rawData, err := os.ReadFile(path) 234 | if err != nil { 235 | return fmt.Errorf("opening saved snapshot data: %w", err) 236 | } 237 | snapData := []map[string]*snapshot.Snapshot{} 238 | if err := json.Unmarshal(rawData, &snapData); err != nil { 239 | return fmt.Errorf("unmarshaling snapshot data: %w", err) 240 | } 241 | 242 | // Check the loaded snapshots 243 | for i, snapset := range snapData { 244 | if err := w.checkSnapshotMatch(snapset); err != nil { 245 | return fmt.Errorf("checking restored storage state #%d: %w", i, err) 246 | } 247 | } 248 | w.Snapshots = snapData 249 | logrus.Infof("loaded %d snapshot sets from %s", len(w.Snapshots), path) 250 | 251 | return nil 252 | } 253 | 254 | // checkSnapshotMatch checks that a snapshot set matches the configured 255 | // storage backends in the watcher. The snapshots need to match in order 256 | // and in the SpecURL 257 | func (w *Watcher) checkSnapshotMatch(snapset map[string]*snapshot.Snapshot) error { 258 | if len(snapset) != len(w.ArtifactStores) { 259 | return fmt.Errorf( 260 | "the number of artifact stores in the watcher (%d) does not match the number in the stored set (%d)", 261 | len(w.ArtifactStores), len(snapset), 262 | ) 263 | } 264 | 265 | // Check that the SpecURLs match those in the configured stores: 266 | i := 0 267 | for specurl := range snapset { 268 | if w.ArtifactStores[i].SpecURL != specurl { 269 | return fmt.Errorf( 270 | "spec url #%d in stored state, does not match storage %s", 271 | i, w.ArtifactStores[i].SpecURL, 272 | ) 273 | } 274 | i++ 275 | } 276 | return nil 277 | } 278 | 279 | type StartMessage struct { 280 | SpecURL string `json:"spec"` 281 | Attestation string `json:"attestation"` 282 | Snapshots string `json:"snapshots"` 283 | ArtifactList string `json:"artifacts_list"` 284 | Artifacts []string `json:"artifacts"` 285 | } 286 | 287 | // PublishToTopic sends the data of a partial attestation to a Pub/Sub 288 | // topic. 289 | func (w *Watcher) PublishToTopic(topicString string, message interface{}) (err error) { 290 | // projects/puerco-chainguard/topics/slsa 291 | parts := strings.Split(topicString, "/") 292 | if len(parts) != 4 { 293 | return errors.New("invalid topic specifier, format: projects/PROJECTID/topics/TOPICNAME") 294 | } 295 | 296 | ctx := context.Background() 297 | 298 | client, err := pubsub.NewClient(ctx, parts[1]) 299 | if err != nil { 300 | log.Fatal(err) 301 | } 302 | defer client.Close() 303 | topic := client.Topic(parts[3]) 304 | 305 | var data []byte 306 | if m, ok := message.(StartMessage); ok { 307 | data, err = json.Marshal(m) 308 | } else { 309 | return errors.New("unknown message format") 310 | } 311 | 312 | if err != nil { 313 | return fmt.Errorf("marshalling message into json: %w", err) 314 | } 315 | logrus.Debugf("Message: %s", string(data)) 316 | if _, err := topic.Publish(ctx, &pubsub.Message{Data: data}).Get(ctx); err != nil { 317 | return fmt.Errorf("publishing to pubsub topic: %w", err) 318 | } 319 | logrus.Infof("pushed build data to topic %s", topicString) 320 | return nil 321 | } 322 | -------------------------------------------------------------------------------- /pkg/watcher/watcher_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package watcher 18 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.Dockerfile.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.Makefile.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.generatego.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright YEAR The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.py.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /scripts/boilerplate/boilerplate.sh.txt: -------------------------------------------------------------------------------- 1 | # Copyright YEAR The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /scripts/verify-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2022 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -o errexit 18 | set -o nounset 19 | set -o pipefail 20 | 21 | PLATFORMS=( 22 | linux/amd64 23 | linux/arm64 24 | darwin/amd64 25 | darwin/arm64 26 | windows/amd64 27 | ) 28 | 29 | for PLATFORM in "${PLATFORMS[@]}"; do 30 | OS="${PLATFORM%/*}" 31 | ARCH=$(basename "$PLATFORM") 32 | 33 | output_name=tejolote'-'$OS'-'$ARCH 34 | 35 | if [ "$OS" = "windows" ]; then 36 | output_name+='.exe' 37 | fi 38 | 39 | echo "Building project for $PLATFORM" 40 | CGO_ENABLED=0 GOARCH="$ARCH" GOOS="$OS" go build -trimpath -ldflags "${TEJOLOTE_LDFLAGS}" -o output/$output_name ./cmd/tejolote/main.go 41 | pushd output 42 | sha256sum $output_name >> checksums.txt 43 | popd 44 | done 45 | --------------------------------------------------------------------------------