302 lines
8.4 KiB
Go
302 lines
8.4 KiB
Go
/*
|
|
Copyright 2021.
|
|
|
|
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/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,
|
|
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 v1alpha2
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/jenkinsci/kubernetes-operator/pkg/plugins"
|
|
|
|
"golang.org/x/mod/semver"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
|
)
|
|
|
|
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).
|
|
For(in).
|
|
Complete()
|
|
}
|
|
|
|
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
|
|
|
// 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}
|
|
|
|
var _ webhook.Validator = &Jenkins{}
|
|
|
|
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
|
|
func (in *Jenkins) ValidateCreate() error {
|
|
if in.Spec.ValidateSecurityWarnings {
|
|
jenkinslog.Info("validate create", "name", in.Name)
|
|
return Validate(*in)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
|
|
func (in *Jenkins) ValidateUpdate(old runtime.Object) error {
|
|
if in.Spec.ValidateSecurityWarnings {
|
|
jenkinslog.Info("validate update", "name", in.Name)
|
|
return Validate(*in)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (in *Jenkins) ValidateDelete() error {
|
|
return nil
|
|
}
|
|
|
|
type PluginsInfo struct {
|
|
Plugins []PluginInfo `json:"plugins"`
|
|
}
|
|
|
|
type PluginInfo struct {
|
|
Name string `json:"name"`
|
|
SecurityWarnings []Warning `json:"securityWarnings"`
|
|
}
|
|
|
|
type Warning struct {
|
|
Versions []Version `json:"versions"`
|
|
ID string `json:"id"`
|
|
Message string `json:"message"`
|
|
URL string `json:"url"`
|
|
Active bool `json:"active"`
|
|
}
|
|
|
|
type Version struct {
|
|
FirstVersion string `json:"firstVersion"`
|
|
LastVersion string `json:"lastVersion"`
|
|
}
|
|
|
|
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)
|
|
pluginSemVer := MakeSemanticVersion(pluginVersion)
|
|
if semver.Compare(pluginSemVer, firstSemVer) == -1 || semver.Compare(pluginSemVer, lastSemVer) == 1 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|