diff --git a/attestation/jenkins/jenkins.go b/attestation/jenkins/jenkins.go new file mode 100644 index 00000000..9d5aa276 --- /dev/null +++ b/attestation/jenkins/jenkins.go @@ -0,0 +1,155 @@ +// Copyright 2024 The Witness Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jenkins + +import ( + "crypto" + "fmt" + "os" + "strings" + + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/log" + "github.com/invopop/jsonschema" +) + +const ( + Name = "jenkins" + Type = "https://witness.dev/attestations/jenkins/v0.1" + RunType = attestation.PreMaterialRunType +) + +// This is a hacky way to create a compile time error in case the attestor +// doesn't implement the expected interfaces. +var ( + _ attestation.Attestor = &Attestor{} + _ attestation.Subjecter = &Attestor{} + _ attestation.BackReffer = &Attestor{} + _ JenkinsAttestor = &Attestor{} +) + +type JenkinsAttestor interface { + // Attestor + Name() string + Type() string + RunType() attestation.RunType + Attest(ctx *attestation.AttestationContext) error + Data() *Attestor + + // Subjecter + Subjects() map[string]cryptoutil.DigestSet + + // Backreffer + BackRefs() map[string]cryptoutil.DigestSet +} + +func init() { + attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { + return New() + }) +} + +type ErrNotJenkins struct{} + +func (e ErrNotJenkins) Error() string { + return "not in a jenkins ci job" +} + +type Attestor struct { + BuildID string `json:"buildid"` + BuildNumber string `json:"buildnumber"` + BuildTag string `json:"buildtag"` + PipelineUrl string `json:"pipelineurl"` + ExecutorNumber string `json:"executornumber"` + JavaHome string `json:"javahome"` + JenkinsUrl string `json:"jenkinsurl"` + JobName string `json:"jobname"` + NodeName string `json:"nodename"` + Workspace string `json:"workspace"` +} + +func New() *Attestor { + return &Attestor{} +} + +func (a *Attestor) Name() string { + return Name +} + +func (a *Attestor) Type() string { + return Type +} + +func (a *Attestor) RunType() attestation.RunType { + return RunType +} + +func (a *Attestor) Schema() *jsonschema.Schema { + return jsonschema.Reflect(&a) +} + +func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { + if _, ok := os.LookupEnv("JENKINS_URL"); !ok { + return ErrNotJenkins{} + } + + a.BuildID = os.Getenv("BUILD_ID") + a.BuildNumber = os.Getenv("BUILD_NUMBER") + a.BuildTag = os.Getenv("BUILD_TAG") + a.PipelineUrl = os.Getenv("BUILD_URL") + a.ExecutorNumber = os.Getenv("EXECUTOR_NUMBER") + a.JavaHome = os.Getenv("JAVA_HOME") + a.JenkinsUrl = os.Getenv("JENKINS_URL") + a.JobName = os.Getenv("JOB_NAME") + a.NodeName = os.Getenv("NODE_NAME") + a.Workspace = os.Getenv("WORKSPACE") + + return nil +} + +func (a *Attestor) Data() *Attestor { + return a +} + +func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { + subjects := make(map[string]cryptoutil.DigestSet) + hashes := []cryptoutil.DigestValue{{Hash: crypto.SHA256}} + if ds, err := cryptoutil.CalculateDigestSetFromBytes([]byte(a.PipelineUrl), hashes); err == nil { + subjects[fmt.Sprintf("pipelineurl:%v", a.PipelineUrl)] = ds + } else { + log.Debugf("(attestation/jenkins) failed to record jenkins pipelineurl subject: %w", err) + } + + if ds, err := cryptoutil.CalculateDigestSetFromBytes([]byte(a.JenkinsUrl), hashes); err == nil { + subjects[fmt.Sprintf("jenkinsurl:%v", a.JenkinsUrl)] = ds + } else { + log.Debugf("(attestation/jenkins) failed to record jenkins jenkinsurl subject: %w", err) + } + + return subjects +} + +func (a *Attestor) BackRefs() map[string]cryptoutil.DigestSet { + backRefs := make(map[string]cryptoutil.DigestSet) + for subj, ds := range a.Subjects() { + if strings.HasPrefix(subj, "pipelineurl:") { + backRefs[subj] = ds + break + } + } + + return backRefs +} diff --git a/attestation/slsa/slsa.go b/attestation/slsa/slsa.go index 12b86213..61b32769 100644 --- a/attestation/slsa/slsa.go +++ b/attestation/slsa/slsa.go @@ -27,6 +27,7 @@ import ( "github.com/in-toto/go-witness/attestation/git" "github.com/in-toto/go-witness/attestation/github" "github.com/in-toto/go-witness/attestation/gitlab" + "github.com/in-toto/go-witness/attestation/jenkins" "github.com/in-toto/go-witness/attestation/material" "github.com/in-toto/go-witness/attestation/oci" "github.com/in-toto/go-witness/attestation/product" @@ -48,6 +49,7 @@ const ( DefaultBuilderId = "https://witness.dev/witness-default-builder@v0.1" GHABuilderId = "https://witness.dev/witness-github-action-builder@v0.1" GLCBuilderId = "https://witness.dev/witness-gitlab-component-builder@v0.1" + JenkinsBuilderId = "https://witness.dev/witness-jenkins-component-builder@v0.1" ) // This is a hacky way to create a compile time error in case the attestor @@ -185,6 +187,11 @@ func (p *Provenance) Attest(ctx *attestation.AttestationContext) error { log.Warn("No SHA found in GitLab JWT") } + case jenkins.Name: + jks := attestor.Attestor.(jenkins.JenkinsAttestor) + p.PbProvenance.RunDetails.Builder.Id = JenkinsBuilderId + p.PbProvenance.RunDetails.Metadata.InvocationId = jks.Data().PipelineUrl + // Material Attestors case material.Name: mats := attestor.Attestor.(material.MaterialAttestor).Materials() @@ -237,7 +244,7 @@ func (p *Provenance) Attest(ctx *attestation.AttestationContext) error { // NOTE: We want to warn users that they can use the github and gitlab attestors to enrich their provenance if p.PbProvenance.RunDetails.Builder.Id == DefaultBuilderId { - log.Warn("No build system attestor invoked. Consider using github or gitlab attestors (if appropriate) to enrich your SLSA provenance") + log.Warn("No build system attestor invoked. Consider using github, gitlab, or jenkins attestors (if appropriate) to enrich your SLSA provenance") } var err error diff --git a/imports.go b/imports.go index 27b4dc19..4db6e32c 100644 --- a/imports.go +++ b/imports.go @@ -24,6 +24,7 @@ import ( _ "github.com/in-toto/go-witness/attestation/git" _ "github.com/in-toto/go-witness/attestation/github" _ "github.com/in-toto/go-witness/attestation/gitlab" + _ "github.com/in-toto/go-witness/attestation/jenkins" _ "github.com/in-toto/go-witness/attestation/jwt" _ "github.com/in-toto/go-witness/attestation/link" _ "github.com/in-toto/go-witness/attestation/material" diff --git a/schemagen/jenkins.json b/schemagen/jenkins.json new file mode 100644 index 00000000..89359dc6 --- /dev/null +++ b/schemagen/jenkins.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "#/$defs/Attestor", + "$defs": { + "Attestor": { + "properties": { + "buildid": { + "type": "string" + }, + "buildnumber": { + "type": "string" + }, + "buildtag": { + "type": "string" + }, + "pipelineurl": { + "type": "string" + }, + "executornumber": { + "type": "string" + }, + "javahome": { + "type": "string" + }, + "jenkinsurl": { + "type": "string" + }, + "jobname": { + "type": "string" + }, + "nodename": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "buildid", + "buildnumber", + "buildtag", + "pipelineurl", + "executornumber", + "javahome", + "jenkinsurl", + "jobname", + "nodename", + "workspace" + ] + } + } +} \ No newline at end of file