diff --git a/Dockerfile b/Dockerfile index ff840ea4..caaced1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,7 @@ COPY internal/ internal/ COPY pkg/ pkg/ COPY version/ version/ COPY main.go main.go +RUN mkdir plugins/ # Build RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -ldflags "-w $CTIMEVAR" -o manager main.go @@ -29,5 +30,4 @@ FROM gcr.io/distroless/static:nonroot WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 - ENTRYPOINT ["/manager"] diff --git a/api/v1alpha2/jenkins_webhook.go b/api/v1alpha2/jenkins_webhook.go index 494fdb80..97af23c9 100644 --- a/api/v1alpha2/jenkins_webhook.go +++ b/api/v1alpha2/jenkins_webhook.go @@ -5,7 +5,7 @@ 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 + http://www.apache.org/lictenses/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, @@ -17,10 +17,13 @@ limitations under the License. package v1alpha2 import ( + "compress/gzip" "encoding/json" "errors" + "io" "io/ioutil" "net/http" + "os" "time" "github.com/jenkinsci/kubernetes-operator/pkg/plugins" @@ -32,8 +35,16 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" ) -// log is for logging in this package. -var jenkinslog = logf.Log.WithName("jenkins-resource") +var ( + jenkinslog = logf.Log.WithName("jenkins-resource") // log is for logging in this package. + isRetrieved bool = false // For checking whether the data file is downloaded and extracted or not +) + +const ( + hosturl string = "https://ci.jenkins.io/job/Infra/job/plugin-site-api/job/generate-data/lastSuccessfulBuild/artifact/plugins.json.gzip" // Url for downloading the plugins file + compressedFile string = "/tmp/plugins.json.gzip" // location where the gzip file will be downloaded + pluginDataFile string = "/tmp/plugins.json" // location where the file will be extracted +) func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). @@ -72,8 +83,13 @@ func (in *Jenkins) ValidateDelete() error { return nil } -type Warnings struct { - Warnings []Warning `json:"securityWarnings"` +type PluginsInfo struct { + Plugins []PluginInfo `json:"plugins"` +} + +type PluginInfo struct { + Name string `json:"name"` + SecurityWarnings []Warning `json:"securityWarnings"` } type Warning struct { @@ -83,18 +99,197 @@ type Warning struct { URL string `json:"url"` Active bool `json:"active"` } + type Version struct { FirstVersion string `json:"firstVersion"` LastVersion string `json:"lastVersion"` } -const APIURL string = "https://plugins.jenkins.io/api/plugin/" +type PluginData struct { + Version string + Kind string +} +// Validates security warnings for both updating and creating a Jenkins CR +func Validate(r Jenkins) error { + + pluginset := make(map[string]PluginData) + var warningmsg string + basePlugins := plugins.BasePlugins() + temp, err := NewPluginsInfo() + AllPluginData := *temp + if err != nil { + return err + } + + for _, plugin := range basePlugins { + // Only Update the map if the plugin is not present or a lower version is being used + if pluginData, ispresent := pluginset[plugin.Name]; !ispresent || semver.Compare(MakeSemanticVersion(plugin.Version), pluginData.Version) == 1 { + jenkinslog.Info("Validate", plugin.Name, plugin.Version) + pluginset[plugin.Name] = PluginData{Version: plugin.Version, Kind: "base"} + } + } + + for _, plugin := range r.Spec.Master.Plugins { + if pluginData, ispresent := pluginset[plugin.Name]; !ispresent || semver.Compare(MakeSemanticVersion(plugin.Version), pluginData.Version) == 1 { + jenkinslog.Info("Validate", plugin.Name, plugin.Version) + pluginset[plugin.Name] = PluginData{Version: plugin.Version, Kind: "user-defined"} + } + } + + jenkinslog.Info("Checking through all the warnings") + for _, plugin := range AllPluginData.Plugins { + + if pluginData, ispresent := pluginset[plugin.Name]; ispresent { + jenkinslog.Info("Checking for plugin", "name", plugin.Name) + for _, warning := range plugin.SecurityWarnings { + for _, version := range warning.Versions { + firstVersion := version.FirstVersion + lastVersion := version.LastVersion + if len(firstVersion) == 0 { + firstVersion = "0" // setting default value in case of empty string + } + if len(lastVersion) == 0 { + lastVersion = pluginData.Version // setting default value in case of empty string + } + + if CompareVersions(firstVersion, lastVersion, pluginData.Version) { + jenkinslog.Info("security Vulnerabilities detected", "message", warning.Message, "Check security Advisory", warning.URL) + warningmsg += "Security Vulnerabilities detected in " + pluginData.Kind + " plugin " + plugin.Name + "\n" + + } + + } + } + + } + + } + + if len(warningmsg) > 0 { + return errors.New(warningmsg) + } + + return nil + +} + +// Returns an object containing information of all the plugins present in the security center +func NewPluginsInfo() (*PluginsInfo, error) { + var AllPluginData PluginsInfo + for i := 0; i < 28; i++ { + if isRetrieved { + break + } + time.Sleep(1 * time.Second) + } + if !isRetrieved { + jenkinslog.Info("Plugins Data file hasn't been downloaded and extracted") + return &AllPluginData, errors.New("plugins data file not found") + } + + jsonFile, err := os.Open(pluginDataFile) + if err != nil { + jenkinslog.Info("Failed to open the Plugins Data File") + return &AllPluginData, err + } + defer jsonFile.Close() + + byteValue, err := ioutil.ReadAll(jsonFile) + if err != nil { + jenkinslog.Info("Failed to convert the JSON file into a byte array") + return &AllPluginData, err + } + err = json.Unmarshal(byteValue, &AllPluginData) + if err != nil { + jenkinslog.Info("Failed to decode the Plugin JSON data file") + return &AllPluginData, err + } + + return &AllPluginData, nil +} + +// Downloads and extracts the JSON file in every 12 hours +func RetrieveDataFile() { + for { + jenkinslog.Info("Retreiving file", "Host Url", hosturl) + err := Download() + if err != nil { + jenkinslog.Info("Retrieving File", "Error while downloading", err) + continue + } + + jenkinslog.Info("Retrieve File", "Successfully downloaded", compressedFile) + err = Extract() + if err != nil { + jenkinslog.Info("Retreive File", "Error while extracting", err) + continue + } + jenkinslog.Info("Retreive File", "Successfully extracted", pluginDataFile) + isRetrieved = true + time.Sleep(12 * time.Hour) + + } +} + +func Download() error { + + out, err := os.Create(compressedFile) + if err != nil { + return err + } + defer out.Close() + + client := http.Client{ + Timeout: 2000 * time.Second, + } + + resp, err := client.Get(hosturl) + if err != nil { + return err + } + defer resp.Body.Close() + jenkinslog.Info("Successfully Downloaded") + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + + return nil + +} + +func Extract() error { + reader, err := os.Open(compressedFile) + + if err != nil { + return err + } + defer reader.Close() + archive, err := gzip.NewReader(reader) + if err != nil { + return err + } + + defer archive.Close() + writer, err := os.Create(pluginDataFile) + if err != nil { + return err + } + defer writer.Close() + + _, err = io.Copy(writer, archive) + return err + +} + +// returns a semantic version that can be used for comparision func MakeSemanticVersion(version string) string { version = "v" + version return semver.Canonical(version) } +// Compare if the current version lies between first version and last version func CompareVersions(firstVersion string, lastVersion string, pluginVersion string) bool { firstSemVer := MakeSemanticVersion(firstVersion) lastSemVer := MakeSemanticVersion(lastVersion) @@ -104,88 +299,3 @@ func CompareVersions(firstVersion string, lastVersion string, pluginVersion stri } return true } - -func CheckSecurityWarnings(pluginName string, pluginVersion string) (bool, error) { - jenkinslog.Info("checking security warnings", "plugin: ", pluginName) - pluginURL := APIURL + pluginName - client := &http.Client{ - Timeout: time.Second * 30, - } - request, err := http.NewRequest("GET", pluginURL, nil) - if err != nil { - return false, err - } - request.Header.Add("Accept", "application/json") - request.Header.Add("Content-Type", "application/json") - response, err := client.Do(request) - if err != nil { - return false, err - } - defer response.Body.Close() - bodyBytes, err := ioutil.ReadAll(response.Body) - if err != nil { - return false, err - } - securityWarnings := Warnings{} - - jsonErr := json.Unmarshal(bodyBytes, &securityWarnings) - if jsonErr != nil { - return false, err - } - - jenkinslog.Info("Validate()", "warnings", securityWarnings) - - for _, warning := range securityWarnings.Warnings { - for _, version := range warning.Versions { - firstVersion := version.FirstVersion - lastVersion := version.LastVersion - if len(firstVersion) == 0 { - firstVersion = "0" // setting default value in case of empty string - } - if len(lastVersion) == 0 { - lastVersion = pluginVersion // setting default value in case of empty string - } - - if CompareVersions(firstVersion, lastVersion, pluginVersion) { - jenkinslog.Info("security Vulnerabilities detected", "message", warning.Message, "Check security Advisory", warning.URL) - return true, nil - } - } - } - - return false, nil -} - -func Validate(r Jenkins) error { - basePlugins := plugins.BasePlugins() - var warnings string = "" - - for _, plugin := range basePlugins { - name := plugin.Name - version := plugin.Version - hasWarnings, err := CheckSecurityWarnings(name, version) - if err != nil { - return err - } - if hasWarnings { - warnings += "Security Vulnerabilities detected in base plugin:" + name - } - } - - for _, plugin := range r.Spec.Master.Plugins { - name := plugin.Name - version := plugin.Version - hasWarnings, err := CheckSecurityWarnings(name, version) - if err != nil { - return err - } - if hasWarnings { - warnings += "Security Vulnerabilities detected in the user defined plugin: " + name - } - } - if len(warnings) > 0 { - return errors.New(warnings) - } - - return nil -} diff --git a/config/samples/jenkins.io_v1alpha2_jenkins.yaml b/config/samples/jenkins.io_v1alpha2_jenkins.yaml index b451d5da..be33732a 100644 --- a/config/samples/jenkins.io_v1alpha2_jenkins.yaml +++ b/config/samples/jenkins.io_v1alpha2_jenkins.yaml @@ -1,8 +1,8 @@ apiVersion: jenkins.io/v1alpha2 kind: Jenkins metadata: - name: example - namespace: default + name: example-8 + namespace: default spec: configurationAsCode: configurations: [] @@ -14,6 +14,7 @@ spec: name: "" jenkinsAPISettings: authorizationStrategy: createUser + ValidateSecurityWarnings: true master: disableCSRFProtection: false containers: @@ -47,6 +48,72 @@ spec: requests: cpu: "1" memory: 500Mi + plugins: + - name: mailer + version: "1.19" + - name: script-security + version: "1.18" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: google-login + version: "1.2" + - name: credentials + version: "2.1" + - name: ssh-credentials + version: "1.1" + - name: junit + version: "1.2" + - name: matrix-project + version: "1.1" + - name: git-client + version: "2.8.4" + - name: pipeline-model-definition + version: "1.3.0" + - name: favorite + version: "2" + - name: workflow-cps + version: "2" + seedJobs: - id: jenkins-operator targets: "cicd/jobs/*.jenkins" diff --git a/main.go b/main.go index 8bc382ea..3cd399f6 100644 --- a/main.go +++ b/main.go @@ -95,6 +95,9 @@ func main() { opts := zap.Options{ Development: true, } + + go v1alpha2.RetrieveDataFile() + opts.BindFlags(flag.CommandLine) flag.Parse() diff --git a/webhook/all_in_one_v1alpha2.yaml b/webhook/all_in_one_v1alpha2.yaml index e53fc659..0d878dd2 100644 --- a/webhook/all_in_one_v1alpha2.yaml +++ b/webhook/all_in_one_v1alpha2.yaml @@ -222,6 +222,7 @@ subjects: - kind: ServiceAccount name: jenkins-operator --- +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -246,7 +247,7 @@ spec: - /manager args: - --leader-elect - image: jenkins-operator:6f33fe82-dirty + image: jenkins-operator:52fe5fe9-dirty name: jenkins-operator imagePullPolicy: IfNotPresent securityContext: @@ -278,12 +279,12 @@ spec: volumeMounts: - mountPath: /tmp/k8s-webhook-server/serving-certs name: cert - readOnly: true + readOnly: false volumes: - name: cert secret: defaultMode: 420 - secretName: webhook-server-cert + secretName: webhook-server-cert terminationGracePeriodSeconds: 10 --- apiVersion: cert-manager.io/v1 @@ -315,7 +316,6 @@ spec: apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: - creationTimestamp: null name: validating-webhook-configuration annotations: cert-manager.io/inject-ca-from: default/webhook-certificate @@ -330,6 +330,7 @@ webhooks: path: /validate-jenkins-io-v1alpha2-jenkins failurePolicy: Fail name: vjenkins.kb.io + timeoutSeconds: 30 rules: - apiGroups: - jenkins.io diff --git a/webhook/operator.yaml b/webhook/operator.yaml index 81cbdf98..c056ba8e 100644 --- a/webhook/operator.yaml +++ b/webhook/operator.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -41,11 +42,11 @@ spec: periodSeconds: 10 resources: limits: - cpu: 100m - memory: 30Mi + cpu: 200m + memory: 200Mi requests: cpu: 100m - memory: 20Mi + memory: 80Mi env: - name: WATCH_NAMESPACE valueFrom: @@ -53,12 +54,11 @@ spec: fieldPath: metadata.namespace volumeMounts: - mountPath: /tmp/k8s-webhook-server/serving-certs - name: cert - readOnly: true + name: cert volumes: - name: cert secret: defaultMode: 420 - secretName: webhook-server-cert + secretName: webhook-server-cert terminationGracePeriodSeconds: 10 --- diff --git a/webhook/webhook.yaml b/webhook/webhook.yaml index 3a86ab0b..9cc2a2b7 100644 --- a/webhook/webhook.yaml +++ b/webhook/webhook.yaml @@ -1,7 +1,6 @@ apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: - creationTimestamp: null name: validating-webhook-configuration annotations: cert-manager.io/inject-ca-from: default/webhook-certificate @@ -16,6 +15,7 @@ webhooks: path: /validate-jenkins-io-v1alpha2-jenkins failurePolicy: Fail name: vjenkins.kb.io + timeoutSeconds: 30 rules: - apiGroups: - jenkins.io