Reimplemented the validation logic with caching the security warnings

- Reimplemented the validator interface
- Updated manifests to allocate more resources
This commit is contained in:
sharmapulkit04 2021-08-04 16:06:14 +05:30
parent 52fe5fe95e
commit 37d0eac4e3
7 changed files with 286 additions and 105 deletions

View File

@ -19,6 +19,7 @@ COPY internal/ internal/
COPY pkg/ pkg/ COPY pkg/ pkg/
COPY version/ version/ COPY version/ version/
COPY main.go main.go COPY main.go main.go
RUN mkdir plugins/
# Build # Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -ldflags "-w $CTIMEVAR" -o manager main.go 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 / WORKDIR /
COPY --from=builder /workspace/manager . COPY --from=builder /workspace/manager .
USER 65532:65532 USER 65532:65532
ENTRYPOINT ["/manager"] ENTRYPOINT ["/manager"]

View File

@ -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 not use this file except in compliance with the License.
You may obtain a copy of the License at 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 Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
@ -17,10 +17,13 @@ limitations under the License.
package v1alpha2 package v1alpha2
import ( import (
"compress/gzip"
"encoding/json" "encoding/json"
"errors" "errors"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"time" "time"
"github.com/jenkinsci/kubernetes-operator/pkg/plugins" "github.com/jenkinsci/kubernetes-operator/pkg/plugins"
@ -32,8 +35,16 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook"
) )
// log is for logging in this package. var (
var jenkinslog = logf.Log.WithName("jenkins-resource") 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 { func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr). return ctrl.NewWebhookManagedBy(mgr).
@ -72,8 +83,13 @@ func (in *Jenkins) ValidateDelete() error {
return nil return nil
} }
type Warnings struct { type PluginsInfo struct {
Warnings []Warning `json:"securityWarnings"` Plugins []PluginInfo `json:"plugins"`
}
type PluginInfo struct {
Name string `json:"name"`
SecurityWarnings []Warning `json:"securityWarnings"`
} }
type Warning struct { type Warning struct {
@ -83,18 +99,197 @@ type Warning struct {
URL string `json:"url"` URL string `json:"url"`
Active bool `json:"active"` Active bool `json:"active"`
} }
type Version struct { type Version struct {
FirstVersion string `json:"firstVersion"` FirstVersion string `json:"firstVersion"`
LastVersion string `json:"lastVersion"` 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 { func MakeSemanticVersion(version string) string {
version = "v" + version version = "v" + version
return semver.Canonical(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 { func CompareVersions(firstVersion string, lastVersion string, pluginVersion string) bool {
firstSemVer := MakeSemanticVersion(firstVersion) firstSemVer := MakeSemanticVersion(firstVersion)
lastSemVer := MakeSemanticVersion(lastVersion) lastSemVer := MakeSemanticVersion(lastVersion)
@ -104,88 +299,3 @@ func CompareVersions(firstVersion string, lastVersion string, pluginVersion stri
} }
return true 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
}

View File

@ -1,8 +1,8 @@
apiVersion: jenkins.io/v1alpha2 apiVersion: jenkins.io/v1alpha2
kind: Jenkins kind: Jenkins
metadata: metadata:
name: example name: example-8
namespace: default namespace: default
spec: spec:
configurationAsCode: configurationAsCode:
configurations: [] configurations: []
@ -14,6 +14,7 @@ spec:
name: "" name: ""
jenkinsAPISettings: jenkinsAPISettings:
authorizationStrategy: createUser authorizationStrategy: createUser
ValidateSecurityWarnings: true
master: master:
disableCSRFProtection: false disableCSRFProtection: false
containers: containers:
@ -47,6 +48,72 @@ spec:
requests: requests:
cpu: "1" cpu: "1"
memory: 500Mi 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: seedJobs:
- id: jenkins-operator - id: jenkins-operator
targets: "cicd/jobs/*.jenkins" targets: "cicd/jobs/*.jenkins"

View File

@ -95,6 +95,9 @@ func main() {
opts := zap.Options{ opts := zap.Options{
Development: true, Development: true,
} }
go v1alpha2.RetrieveDataFile()
opts.BindFlags(flag.CommandLine) opts.BindFlags(flag.CommandLine)
flag.Parse() flag.Parse()

View File

@ -222,6 +222,7 @@ subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: jenkins-operator name: jenkins-operator
--- ---
---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@ -246,7 +247,7 @@ spec:
- /manager - /manager
args: args:
- --leader-elect - --leader-elect
image: jenkins-operator:6f33fe82-dirty image: jenkins-operator:52fe5fe9-dirty
name: jenkins-operator name: jenkins-operator
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext: securityContext:
@ -278,12 +279,12 @@ spec:
volumeMounts: volumeMounts:
- mountPath: /tmp/k8s-webhook-server/serving-certs - mountPath: /tmp/k8s-webhook-server/serving-certs
name: cert name: cert
readOnly: true readOnly: false
volumes: volumes:
- name: cert - name: cert
secret: secret:
defaultMode: 420 defaultMode: 420
secretName: webhook-server-cert secretName: webhook-server-cert
terminationGracePeriodSeconds: 10 terminationGracePeriodSeconds: 10
--- ---
apiVersion: cert-manager.io/v1 apiVersion: cert-manager.io/v1
@ -315,7 +316,6 @@ spec:
apiVersion: admissionregistration.k8s.io/v1 apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration kind: ValidatingWebhookConfiguration
metadata: metadata:
creationTimestamp: null
name: validating-webhook-configuration name: validating-webhook-configuration
annotations: annotations:
cert-manager.io/inject-ca-from: default/webhook-certificate cert-manager.io/inject-ca-from: default/webhook-certificate
@ -330,6 +330,7 @@ webhooks:
path: /validate-jenkins-io-v1alpha2-jenkins path: /validate-jenkins-io-v1alpha2-jenkins
failurePolicy: Fail failurePolicy: Fail
name: vjenkins.kb.io name: vjenkins.kb.io
timeoutSeconds: 30
rules: rules:
- apiGroups: - apiGroups:
- jenkins.io - jenkins.io

View File

@ -1,3 +1,4 @@
---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@ -41,11 +42,11 @@ spec:
periodSeconds: 10 periodSeconds: 10
resources: resources:
limits: limits:
cpu: 100m cpu: 200m
memory: 30Mi memory: 200Mi
requests: requests:
cpu: 100m cpu: 100m
memory: 20Mi memory: 80Mi
env: env:
- name: WATCH_NAMESPACE - name: WATCH_NAMESPACE
valueFrom: valueFrom:
@ -53,12 +54,11 @@ spec:
fieldPath: metadata.namespace fieldPath: metadata.namespace
volumeMounts: volumeMounts:
- mountPath: /tmp/k8s-webhook-server/serving-certs - mountPath: /tmp/k8s-webhook-server/serving-certs
name: cert name: cert
readOnly: true
volumes: volumes:
- name: cert - name: cert
secret: secret:
defaultMode: 420 defaultMode: 420
secretName: webhook-server-cert secretName: webhook-server-cert
terminationGracePeriodSeconds: 10 terminationGracePeriodSeconds: 10
--- ---

View File

@ -1,7 +1,6 @@
apiVersion: admissionregistration.k8s.io/v1 apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration kind: ValidatingWebhookConfiguration
metadata: metadata:
creationTimestamp: null
name: validating-webhook-configuration name: validating-webhook-configuration
annotations: annotations:
cert-manager.io/inject-ca-from: default/webhook-certificate cert-manager.io/inject-ca-from: default/webhook-certificate
@ -16,6 +15,7 @@ webhooks:
path: /validate-jenkins-io-v1alpha2-jenkins path: /validate-jenkins-io-v1alpha2-jenkins
failurePolicy: Fail failurePolicy: Fail
name: vjenkins.kb.io name: vjenkins.kb.io
timeoutSeconds: 30
rules: rules:
- apiGroups: - apiGroups:
- jenkins.io - jenkins.io