Updated Validation logic

- Defined a security manager struct to cache all the plugin data
- Added flag to make validating security warnings optional while deploying the operator
This commit is contained in:
sharmapulkit04 2021-08-06 19:01:27 +05:30
parent 37d0eac4e3
commit 1d2651d43f
5 changed files with 131 additions and 170 deletions

View File

@ -37,13 +37,8 @@ import (
var ( var (
jenkinslog = logf.Log.WithName("jenkins-resource") // log is for logging in this package. 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 PluginsDataManager PluginDataManager = *NewPluginsDataManager()
) _ webhook.Validator = &Jenkins{}
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 {
@ -57,8 +52,6 @@ func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error {
// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// +kubebuilder:webhook:path=/validate-jenkins-io-jenkins-io-v1alpha2-jenkins,mutating=false,failurePolicy=fail,sideEffects=None,groups=jenkins.io.jenkins.io,resources=jenkins,verbs=create;update,versions=v1alpha2,name=vjenkins.kb.io,admissionReviewVersions={v1,v1beta1} // +kubebuilder:webhook:path=/validate-jenkins-io-jenkins-io-v1alpha2-jenkins,mutating=false,failurePolicy=fail,sideEffects=None,groups=jenkins.io.jenkins.io,resources=jenkins,verbs=create;update,versions=v1alpha2,name=vjenkins.kb.io,admissionReviewVersions={v1,v1beta1}
var _ webhook.Validator = &Jenkins{}
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type // ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (in *Jenkins) ValidateCreate() error { func (in *Jenkins) ValidateCreate() error {
if in.Spec.ValidateSecurityWarnings { if in.Spec.ValidateSecurityWarnings {
@ -83,6 +76,15 @@ func (in *Jenkins) ValidateDelete() error {
return nil return nil
} }
type PluginDataManager struct {
pluginDataCache PluginsInfo
hosturl string
compressedFilePath string
pluginDataFile string
iscached bool
maxattempts int
}
type PluginsInfo struct { type PluginsInfo struct {
Plugins []PluginInfo `json:"plugins"` Plugins []PluginInfo `json:"plugins"`
} }
@ -112,36 +114,27 @@ type PluginData struct {
// Validates security warnings for both updating and creating a Jenkins CR // Validates security warnings for both updating and creating a Jenkins CR
func Validate(r Jenkins) error { func Validate(r Jenkins) error {
pluginset := make(map[string]PluginData) pluginset := make(map[string]PluginData)
var warningmsg string var faultybaseplugins string
var faultyuserplugins string
basePlugins := plugins.BasePlugins() basePlugins := plugins.BasePlugins()
temp, err := NewPluginsInfo()
AllPluginData := *temp
if err != nil {
return err
}
for _, plugin := range basePlugins { for _, plugin := range basePlugins {
// Only Update the map if the plugin is not present or a lower version is being used // 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 { 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"} pluginset[plugin.Name] = PluginData{Version: plugin.Version, Kind: "base"}
} }
} }
for _, plugin := range r.Spec.Master.Plugins { for _, plugin := range r.Spec.Master.Plugins {
if pluginData, ispresent := pluginset[plugin.Name]; !ispresent || semver.Compare(MakeSemanticVersion(plugin.Version), pluginData.Version) == 1 { 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"} pluginset[plugin.Name] = PluginData{Version: plugin.Version, Kind: "user-defined"}
} }
} }
jenkinslog.Info("Checking through all the warnings") for _, plugin := range PluginsDataManager.pluginDataCache.Plugins {
for _, plugin := range AllPluginData.Plugins {
if pluginData, ispresent := pluginset[plugin.Name]; ispresent { if pluginData, ispresent := pluginset[plugin.Name]; ispresent {
jenkinslog.Info("Checking for plugin", "name", plugin.Name) var hasvulnerabilities bool
for _, warning := range plugin.SecurityWarnings { for _, warning := range plugin.SecurityWarnings {
for _, version := range warning.Versions { for _, version := range warning.Versions {
firstVersion := version.FirstVersion firstVersion := version.FirstVersion
@ -154,113 +147,121 @@ func Validate(r Jenkins) error {
} }
if CompareVersions(firstVersion, lastVersion, pluginData.Version) { if CompareVersions(firstVersion, lastVersion, pluginData.Version) {
jenkinslog.Info("security Vulnerabilities detected", "message", warning.Message, "Check security Advisory", warning.URL) jenkinslog.Info("Security Vulnerability detected in "+pluginData.Kind+" "+plugin.Name+":"+pluginData.Version, "Warning message", warning.Message, "For more details,check security advisory", warning.URL)
warningmsg += "Security Vulnerabilities detected in " + pluginData.Kind + " plugin " + plugin.Name + "\n" hasvulnerabilities = true
} }
} }
} }
if hasvulnerabilities {
if pluginData.Kind == "base" {
faultybaseplugins += plugin.Name + ":" + pluginData.Version + "\n"
} else {
faultyuserplugins += plugin.Name + ":" + pluginData.Version + "\n"
} }
} }
}
if len(warningmsg) > 0 { }
return errors.New(warningmsg) if len(faultybaseplugins) > 0 || len(faultyuserplugins) > 0 {
var errormsg string
if len(faultybaseplugins) > 0 {
errormsg += "Security vulnerabilities detected in the following base plugins: \n" + faultybaseplugins
}
if len(faultyuserplugins) > 0 {
errormsg += "Security vulnerabilities detected in the following user-defined plugins: \n" + faultyuserplugins
}
return errors.New(errormsg)
} }
return nil return nil
} }
// Returns an object containing information of all the plugins present in the security center func NewPluginsDataManager() *PluginDataManager {
func NewPluginsInfo() (*PluginsInfo, error) { return &PluginDataManager{
var AllPluginData PluginsInfo hosturl: "https://ci.jenkins.io/job/Infra/job/plugin-site-api/job/generate-data/lastSuccessfulBuild/artifact/plugins.json.gzip",
for i := 0; i < 28; i++ { compressedFilePath: "/tmp/plugins.json.gzip",
if isRetrieved { pluginDataFile: "/tmp/plugins.json",
iscached: false,
maxattempts: 5,
}
}
// Downloads extracts and caches the JSON data in every 12 hours
func (in *PluginDataManager) CachePluginData(ch chan bool) {
for {
jenkinslog.Info("Initializing/Updating the plugin data cache")
var isdownloaded, isextracted, iscached bool
var err error
for i := 0; i < in.maxattempts; i++ {
err = in.Download()
if err == nil {
isdownloaded = true
break 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 isdownloaded {
if err != nil { for i := 0; i < in.maxattempts; i++ {
jenkinslog.Info("Failed to open the Plugins Data File") err = in.Extract()
return &AllPluginData, err if err == nil {
isextracted = true
break
} }
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) } else {
if err != nil { jenkinslog.Info("Cache Plugin Data", "failed to download file", err)
jenkinslog.Info("Failed to decode the Plugin JSON data file")
return &AllPluginData, err
} }
return &AllPluginData, nil if isextracted {
} for i := 0; i < in.maxattempts; i++ {
err = in.Cache()
// Downloads and extracts the JSON file in every 12 hours if err == nil {
func RetrieveDataFile() { iscached = true
for { break
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) if !iscached {
err = Extract() jenkinslog.Info("Cache Plugin Data", "failed to read plugin data file", err)
if err != nil {
jenkinslog.Info("Retreive File", "Error while extracting", err)
continue
} }
jenkinslog.Info("Retreive File", "Successfully extracted", pluginDataFile) } else {
isRetrieved = true jenkinslog.Info("Cache Plugin Data", "failed to extract file", err)
}
if !in.iscached {
ch <- iscached
}
in.iscached = in.iscached || iscached
time.Sleep(12 * time.Hour) time.Sleep(12 * time.Hour)
} }
} }
func Download() error { func (in *PluginDataManager) Download() error {
out, err := os.Create(in.compressedFilePath)
out, err := os.Create(compressedFile)
if err != nil { if err != nil {
return err return err
} }
defer out.Close() defer out.Close()
client := http.Client{ client := http.Client{
Timeout: 2000 * time.Second, Timeout: 1000 * time.Second,
} }
resp, err := client.Get(hosturl) resp, err := client.Get(in.hosturl)
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
jenkinslog.Info("Successfully Downloaded")
_, err = io.Copy(out, resp.Body) _, err = io.Copy(out, resp.Body)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func Extract() error { func (in *PluginDataManager) Extract() error {
reader, err := os.Open(compressedFile) reader, err := os.Open(in.compressedFilePath)
if err != nil { if err != nil {
return err return err
@ -272,7 +273,7 @@ func Extract() error {
} }
defer archive.Close() defer archive.Close()
writer, err := os.Create(pluginDataFile) writer, err := os.Create(in.pluginDataFile)
if err != nil { if err != nil {
return err return err
} }
@ -280,10 +281,28 @@ func Extract() error {
_, err = io.Copy(writer, archive) _, err = io.Copy(writer, archive)
return err return err
} }
// returns a semantic version that can be used for comparision // Loads the JSON data into memory and stores it
func (in *PluginDataManager) Cache() error {
jsonFile, err := os.Open(in.pluginDataFile)
if err != nil {
return err
}
defer jsonFile.Close()
byteValue, err := ioutil.ReadAll(jsonFile)
if err != nil {
return err
}
err = json.Unmarshal(byteValue, &in.pluginDataCache)
if err != nil {
return err
}
return nil
}
// returns a semantic version that can be used for comparison
func MakeSemanticVersion(version string) string { func MakeSemanticVersion(version string) string {
version = "v" + version version = "v" + version
return semver.Canonical(version) return semver.Canonical(version)

View File

@ -1,7 +1,8 @@
apiVersion: jenkins.io/v1alpha2 apiVersion: jenkins.io/v1alpha2
kind: Jenkins kind: Jenkins
metadata: metadata:
name: example-8 name: example
namespace: default namespace: default
spec: spec:
configurationAsCode: configurationAsCode:
@ -14,7 +15,6 @@ spec:
name: "" name: ""
jenkinsAPISettings: jenkinsAPISettings:
authorizationStrategy: createUser authorizationStrategy: createUser
ValidateSecurityWarnings: true
master: master:
disableCSRFProtection: false disableCSRFProtection: false
containers: containers:
@ -48,72 +48,6 @@ 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"

14
main.go
View File

@ -78,6 +78,7 @@ func main() {
var metricsAddr string var metricsAddr string
var enableLeaderElection bool var enableLeaderElection bool
var probeAddr string var probeAddr string
var ValidateSecurityWarnings bool
isRunningInCluster, err := resources.IsRunningInCluster() isRunningInCluster, err := resources.IsRunningInCluster()
if err != nil { if err != nil {
@ -88,6 +89,7 @@ func main() {
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", isRunningInCluster, "Enable leader election for controller manager. "+ flag.BoolVar(&enableLeaderElection, "leader-elect", isRunningInCluster, "Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.") "Enabling this will ensure there is only one active controller manager.")
flag.BoolVar(&ValidateSecurityWarnings, "validate-security-warnings", false, "Enable validation for potential security warnings in jenkins custom resource plugins")
hostname := flag.String("jenkins-api-hostname", "", "Hostname or IP of Jenkins API. It can be service name, node IP or localhost.") hostname := flag.String("jenkins-api-hostname", "", "Hostname or IP of Jenkins API. It can be service name, node IP or localhost.")
port := flag.Int("jenkins-api-port", 0, "The port on which Jenkins API is running. Note: If you want to use nodePort don't set this setting and --jenkins-api-use-nodeport must be true.") port := flag.Int("jenkins-api-port", 0, "The port on which Jenkins API is running. Note: If you want to use nodePort don't set this setting and --jenkins-api-use-nodeport must be true.")
useNodePort := flag.Bool("jenkins-api-use-nodeport", false, "Connect to Jenkins API using the service nodePort instead of service port. If you want to set this as true - don't set --jenkins-api-port.") useNodePort := flag.Bool("jenkins-api-use-nodeport", false, "Connect to Jenkins API using the service nodePort instead of service port. If you want to set this as true - don't set --jenkins-api-port.")
@ -95,9 +97,6 @@ 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()
@ -112,6 +111,15 @@ func main() {
} }
logger.Info(fmt.Sprintf("Watch namespace: %v", namespace)) logger.Info(fmt.Sprintf("Watch namespace: %v", namespace))
if ValidateSecurityWarnings {
ispluginsdatainitialized := make(chan bool)
go v1alpha2.PluginsDataManager.CachePluginData(ispluginsdatainitialized)
if !<-ispluginsdatainitialized {
fatal(errors.New("Unable to get the plugins data"), *debug)
}
}
// get a config to talk to the API server // get a config to talk to the API server
cfg, err := config.GetConfig() cfg, err := config.GetConfig()
if err != nil { if err != nil {

View File

@ -247,7 +247,7 @@ spec:
- /manager - /manager
args: args:
- --leader-elect - --leader-elect
image: jenkins-operator:52fe5fe9-dirty image: jenkins-operator:37d0eac4-dirty
name: jenkins-operator name: jenkins-operator
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext: securityContext:
@ -266,11 +266,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:
@ -279,7 +279,6 @@ spec:
volumeMounts: volumeMounts:
- mountPath: /tmp/k8s-webhook-server/serving-certs - mountPath: /tmp/k8s-webhook-server/serving-certs
name: cert name: cert
readOnly: false
volumes: volumes:
- name: cert - name: cert
secret: secret:

View File

@ -23,6 +23,7 @@ spec:
- /manager - /manager
args: args:
- --leader-elect - --leader-elect
- --validate-security-warnings
image: {DOCKER_REGISTRY}:{GITCOMMIT} image: {DOCKER_REGISTRY}:{GITCOMMIT}
name: jenkins-operator name: jenkins-operator
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent