mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-12-18 17:08:29 +00:00
453 lines
16 KiB
Go
453 lines
16 KiB
Go
// Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
|
|
// Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
|
|
// Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// 1. Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
//
|
|
// 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
// this list of conditions and the following disclaimer in the documentation
|
|
// and/or other materials provided with the distribution.
|
|
//
|
|
// 3. Neither the name of the copyright holder nor the names of its contributors
|
|
// may be used to endorse or promote products derived from this software without
|
|
// specific prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
package workflow
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"github.com/gobwas/glob"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/viper"
|
|
"go.temporal.io/sdk/client"
|
|
apollodb "peridot.resf.org/apollo/db"
|
|
apollopb "peridot.resf.org/apollo/pb"
|
|
"peridot.resf.org/apollo/rherrata"
|
|
"peridot.resf.org/apollo/rhsecurity"
|
|
"peridot.resf.org/apollo/rpmutils"
|
|
"peridot.resf.org/koji"
|
|
"peridot.resf.org/utils"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var forceKoji koji.API
|
|
|
|
type Controller struct {
|
|
log *logrus.Logger
|
|
temporal client.Client
|
|
db apollodb.Access
|
|
mainQueue string
|
|
|
|
errata rherrata.APIService
|
|
security rhsecurity.DefaultApi
|
|
|
|
vendor string
|
|
}
|
|
|
|
type Koji struct {
|
|
Endpoint string
|
|
Compose string
|
|
ModuleCompose string
|
|
}
|
|
|
|
type NewControllerInput struct {
|
|
Temporal client.Client
|
|
Database apollodb.Access
|
|
MainQueue string
|
|
}
|
|
|
|
type Option func(c *Controller)
|
|
|
|
func WithSecurityAPI(api rhsecurity.DefaultApi) Option {
|
|
return func(c *Controller) {
|
|
c.security = api
|
|
}
|
|
}
|
|
|
|
func WithErrataAPI(api rherrata.APIService) Option {
|
|
return func(c *Controller) {
|
|
c.errata = api
|
|
}
|
|
}
|
|
|
|
// NewController returns a new workflow controller. It is the entry point for the Temporal worker.
|
|
// Usually each project share a common controller with different workflows and activities enabled
|
|
// in the `cmd` package.
|
|
func NewController(input *NewControllerInput, opts ...Option) (*Controller, error) {
|
|
c := &Controller{
|
|
log: logrus.New(),
|
|
temporal: input.Temporal,
|
|
db: input.Database,
|
|
mainQueue: input.MainQueue,
|
|
vendor: viper.GetString("vendor"),
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(c)
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// productName simply appends major version to `Red Hat Enterprise Linux`
|
|
func productName(majorVersion int32) string {
|
|
return fmt.Sprintf("Red Hat Enterprise Linux %d", majorVersion)
|
|
}
|
|
|
|
// affectedProductNameForArchAndVersion creates appropriate upstream product names for arch and version
|
|
// This is then used to parse affected packages
|
|
func affectedProductNameForArchAndVersion(arch string, majorVersion int32) string {
|
|
var archString string
|
|
switch arch {
|
|
case "x86_64":
|
|
archString = "x86_64"
|
|
break
|
|
case "aarch64":
|
|
archString = "ARM 64"
|
|
break
|
|
case "ppc64le":
|
|
archString = "Power, little endian"
|
|
break
|
|
case "s390x":
|
|
archString = "IBM z Systems"
|
|
break
|
|
default:
|
|
archString = "UnknownBreakOnPurpose"
|
|
break
|
|
}
|
|
return fmt.Sprintf("Red Hat Enterprise Linux for %s 8", archString)
|
|
}
|
|
|
|
// productState returns appropriate proto type for string states
|
|
func productState(state string) apollopb.AffectedProduct_State {
|
|
switch state {
|
|
case "Under investigation":
|
|
return apollopb.AffectedProduct_STATE_UNDER_INVESTIGATION_UPSTREAM
|
|
case "Not affected":
|
|
return apollopb.AffectedProduct_STATE_UNKNOWN
|
|
case "Will not fix":
|
|
return apollopb.AffectedProduct_STATE_WILL_NOT_FIX_UPSTREAM
|
|
case "Out of support scope":
|
|
return apollopb.AffectedProduct_STATE_OUT_OF_SUPPORT_SCOPE
|
|
case "Affected":
|
|
return apollopb.AffectedProduct_STATE_AFFECTED_UPSTREAM
|
|
default:
|
|
return apollopb.AffectedProduct_STATE_UNDER_INVESTIGATION_UPSTREAM
|
|
}
|
|
}
|
|
|
|
// checkProduct is used to check and validate CVE package states and releases
|
|
func (c *Controller) checkProduct(tx apollodb.Access, cve *apollodb.CVE, shortCode *apollodb.ShortCode, product *apollodb.Product, productState apollopb.AffectedProduct_State, packageName string, advisory *string) bool {
|
|
// Re-create a valid product name using the short code prefix and major version.
|
|
// Example: Red Hat Enterprise Linux 8 translates to Rocky Linux 8 for the short code `RL`.
|
|
// Check `//apollo:seed.sql` for more info
|
|
mirrorProductName := fmt.Sprintf("%s %d", product.RedHatProductPrefix.String, product.RedHatMajorVersion.Int32)
|
|
|
|
// Get the affected product if exists
|
|
affectedProduct, err := tx.GetAffectedProductByCVEAndPackage(cve.ID, packageName)
|
|
if err != nil {
|
|
// The affected product does not exist, so we can mark this product as affected if this product exists
|
|
if err == sql.ErrNoRows {
|
|
// Check if the current package name matches an NVR and if we have a non-NVR variant
|
|
skipCreate := false
|
|
epochlessPackage := rpmutils.Epoch().ReplaceAllString(packageName, "")
|
|
if rpmutils.NVR().MatchString(epochlessPackage) {
|
|
nvr := rpmutils.NVR().FindStringSubmatch(epochlessPackage)
|
|
affectedProduct, err = tx.GetAffectedProductByCVEAndPackage(cve.ID, nvr[1])
|
|
if err == nil {
|
|
skipCreate = true
|
|
}
|
|
}
|
|
|
|
if !skipCreate {
|
|
// Get the mirrored product name product if exists (this should exist if supported)
|
|
// Example: Rocky Linux only supports 8 so we will only have `Rocky Linux 8` in our supported products
|
|
// In the future, when we support 8 and 9 at the same time, we only need to add `Rocky Linux 9` to start
|
|
// mirroring errata for el9 packages
|
|
product, err := tx.GetProductByNameAndShortCode(mirrorProductName, shortCode.Code)
|
|
if err != nil {
|
|
// Product isn't supported so skip
|
|
if err == sql.ErrNoRows {
|
|
logrus.Infof("Product %s not supported", mirrorProductName)
|
|
return true
|
|
} else {
|
|
logrus.Errorf("could not get product: %v", err)
|
|
return true
|
|
}
|
|
}
|
|
|
|
// If product state isn't set to unknown (usually when product isn't affected)
|
|
// create a new affected product entry for the CVE
|
|
if productState != apollopb.AffectedProduct_STATE_UNKNOWN {
|
|
affectedProduct, err = tx.CreateAffectedProduct(product.ID, cve.ID, int(productState), product.CurrentFullVersion, packageName, advisory)
|
|
if err != nil {
|
|
logrus.Errorf("could not create affected product: %v", err)
|
|
return true
|
|
}
|
|
logrus.Infof("Added product %s (%s) to %s with state %s", mirrorProductName, packageName, cve.ID, productState.String())
|
|
}
|
|
}
|
|
} else {
|
|
logrus.Errorf("could not get affected product: %v", err)
|
|
return true
|
|
}
|
|
}
|
|
|
|
// We don't use else because this may change if a non-NVR variant is found
|
|
if err == nil {
|
|
// If the state isn't set to unknown (it is then usually queued for deletion)
|
|
if productState != apollopb.AffectedProduct_STATE_UNKNOWN {
|
|
// If it's already in that state, skip
|
|
if int(productState) == affectedProduct.State {
|
|
return true
|
|
}
|
|
|
|
// If the affected product is set to FixedDownstream and we're trying to set it to FixedUpstream, skip
|
|
if affectedProduct.State == int(apollopb.AffectedProduct_STATE_FIXED_DOWNSTREAM) && productState == apollopb.AffectedProduct_STATE_FIXED_UPSTREAM {
|
|
return true
|
|
}
|
|
|
|
err := tx.UpdateAffectedProductStateAndPackageAndAdvisory(affectedProduct.ID, int(productState), packageName, advisory)
|
|
if err != nil {
|
|
logrus.Errorf("could not update affected product state: %v", err)
|
|
return true
|
|
}
|
|
logrus.Infof("Updated product %s (%s) on %s with state %s", mirrorProductName, packageName, cve.ID, productState.String())
|
|
} else {
|
|
// Delete affected product if state is set to Unknown
|
|
// That means that the product is set as NotAffected
|
|
err = tx.DeleteAffectedProduct(affectedProduct.ID)
|
|
if err != nil {
|
|
logrus.Errorf("could not delete unaffected product: %v", err)
|
|
return true
|
|
}
|
|
logrus.Infof("Product %s (%s) not affected by %s", mirrorProductName, packageName, cve.ID)
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *Controller) isNvrIdentical(build *koji.Build, nvr []string) bool {
|
|
// Join all release bits and remove the dist tag (because sometimes downstream forks do not match the upstream dist tag)
|
|
// Example: Rocky Linux 8.3 initial build did not tag updated RHEL packages as el8_3, but as el8
|
|
joinedRelease := rpmutils.Dist().ReplaceAllString(strings.TrimSuffix(strings.Join(nvr[2:], "."), "."), "")
|
|
// Remove all module release bits (to make it possible to actually match NVR)
|
|
joinedRelease = rpmutils.ModuleDist().ReplaceAllString(joinedRelease, "")
|
|
// Same operations for the build release
|
|
buildRelease := rpmutils.Dist().ReplaceAllString(build.Release, "")
|
|
buildRelease = rpmutils.ModuleDist().ReplaceAllString(buildRelease, "")
|
|
|
|
// Check if package name, version matches and that the release prefix matches
|
|
// The reason we're only checking for prefix in release is that downstream
|
|
// builds may append `.1` or something else
|
|
// Example: Rocky Linux appends `.rocky` to modified packages
|
|
if build.PackageName == nvr[0] && build.Version == nvr[1] && strings.HasPrefix(buildRelease, joinedRelease) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *Controller) checkForIgnoredPackage(ignoredPackages []string, packageName string) (bool, error) {
|
|
for _, ignoredPackage := range ignoredPackages {
|
|
g, err := glob.Compile(ignoredPackage)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if g.Match(packageName) {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (c *Controller) checkForRebootSuggestedPackage(pkgs []string, packageName string) (bool, error) {
|
|
for _, p := range pkgs {
|
|
g, err := glob.Compile(p)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if g.Match(packageName) {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (c *Controller) checkKojiForBuild(tx apollodb.Access, ignoredPackages []string, nvrOnly string, affectedProduct *apollodb.AffectedProduct, cve *apollodb.CVE) apollopb.BuildStatus {
|
|
product, err := tx.GetProductByID(affectedProduct.ProductID)
|
|
if err != nil {
|
|
c.log.Errorf("could not get product: %v", err)
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
if product.BuildSystem != "koji" {
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
|
|
var k koji.API
|
|
if forceKoji != nil {
|
|
k = forceKoji
|
|
} else {
|
|
k, err = koji.New(product.BuildSystemEndpoint)
|
|
if err != nil {
|
|
c.log.Errorf("could not create koji client: %v", err)
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
}
|
|
|
|
// Check if the submitted NVR is valid
|
|
nvr := rpmutils.NVR().FindStringSubmatch(nvrOnly)
|
|
if len(nvr) < 3 {
|
|
logrus.Errorf("Invalid NVR %s", nvrOnly)
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
nvr = nvr[1:]
|
|
|
|
match, err := c.checkForIgnoredPackage(ignoredPackages, nvr[0])
|
|
if err != nil {
|
|
logrus.Errorf("Invalid glob: %v", err)
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
if match {
|
|
return apollopb.BuildStatus_BUILD_STATUS_WILL_NOT_FIX
|
|
}
|
|
|
|
var tagged []*koji.Build
|
|
|
|
// If the package is part of a module, we have to check for valid builds
|
|
// rather than check in the compose tag
|
|
if strings.Contains(nvrOnly, ".module") {
|
|
// We need to find the package id
|
|
packageRes, err := k.GetPackage(&koji.GetPackageRequest{
|
|
PackageName: nvr[0],
|
|
})
|
|
if err != nil {
|
|
logrus.Errorf("Could not get package information from Koji: %v", err)
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
|
|
// Use package id to get builds
|
|
buildsRes, err := k.ListBuilds(&koji.ListBuildsRequest{
|
|
PackageID: packageRes.ID,
|
|
})
|
|
if err != nil {
|
|
logrus.Errorf("Could not get builds from Koji: %v", err)
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
|
|
tagged = buildsRes.Builds
|
|
} else {
|
|
// Non-module packages can be queried using the list tagged operation.
|
|
// We only check the compose tag
|
|
taggedRes, err := k.ListTagged(&koji.ListTaggedRequest{
|
|
Tag: product.KojiCompose.String,
|
|
Package: nvr[0],
|
|
})
|
|
if err != nil {
|
|
logrus.Errorf("Could not get tagged builds for package %s: %v", nvr[0], err)
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
|
|
tagged = taggedRes.Builds
|
|
}
|
|
|
|
// No valid builds found usually means that we don't ship that package
|
|
if len(tagged) <= 0 {
|
|
logrus.Errorf("No valid builds found for package %s", nvr[0])
|
|
return apollopb.BuildStatus_BUILD_STATUS_NOT_FIXED
|
|
}
|
|
|
|
// Use a top-level fixed state to track if the NVR exists (at least once for modules)
|
|
fixed := false
|
|
for _, build := range tagged {
|
|
latestBuild := build
|
|
// Skip module contents (this is content inserted by module-build-service)
|
|
if latestBuild.Extra != nil && latestBuild.Extra.Typeinfo != nil {
|
|
continue
|
|
}
|
|
|
|
// Re-construct a valid NVR
|
|
kojiNvr := fmt.Sprintf("%s-%s-%s", latestBuild.PackageName, latestBuild.Version, latestBuild.Release)
|
|
|
|
// If the NVR is identical, that means that the fix has been built
|
|
if c.isNvrIdentical(latestBuild, nvr) {
|
|
logrus.Infof("%s has been fixed downstream with build %d (%s)", cve.ID, latestBuild.BuildId, kojiNvr)
|
|
err := tx.UpdateAffectedProductStateAndPackageAndAdvisory(affectedProduct.ID, int(apollopb.AffectedProduct_STATE_FIXED_DOWNSTREAM), affectedProduct.Package, &affectedProduct.Advisory.String)
|
|
if err != nil {
|
|
logrus.Errorf("Could not update affected product %d: %v", affectedProduct.ID, err)
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
|
|
// Get all RPMs for build
|
|
rpms, err := k.ListRPMs(&koji.ListRPMsRequest{
|
|
BuildID: latestBuild.BuildId,
|
|
})
|
|
if err != nil {
|
|
logrus.Errorf("Could not get RPMs from Koji: %v", err)
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
|
|
var srcRpm string
|
|
for _, rpm := range rpms.RPMs {
|
|
if rpm.Arch == "src" {
|
|
epochInt := 0
|
|
if rpm.Epoch != nil {
|
|
epochInt = *rpm.Epoch
|
|
}
|
|
|
|
srcRpm = fmt.Sprintf("%s-%d:%s-%s.%s.rpm", rpm.Name, epochInt, rpm.Version, rpm.Release, rpm.Arch)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Add all RPMs as a build reference to the CVE
|
|
// This is the "Affected packages" section of an advisory
|
|
for _, rpm := range rpms.RPMs {
|
|
// Construct a valid rpm name (this is what the repos will contain)
|
|
rpmStr := fmt.Sprintf("%s-%d:%s-%s.%s.rpm", rpm.Name, utils.Default[int](rpm.Epoch), rpm.Version, rpm.Release, rpm.Arch)
|
|
_, err = tx.CreateBuildReference(affectedProduct.ID, rpmStr, srcRpm, cve.ID, "", utils.Pointer[string](strconv.Itoa(latestBuild.BuildId)), nil)
|
|
if err != nil {
|
|
logrus.Errorf("Could not create build reference: %v", err)
|
|
return apollopb.BuildStatus_BUILD_STATUS_SKIP
|
|
}
|
|
}
|
|
|
|
// We've seen at least one fix
|
|
fixed = true
|
|
// Since we've seen a fix, we don't have to keep looking
|
|
break
|
|
}
|
|
}
|
|
|
|
// No fix has been detected, will mark as FixedUpstream
|
|
if !fixed {
|
|
logrus.Errorf("%s has not been fixed for NVR %s", cve.ID, nvrOnly)
|
|
return apollopb.BuildStatus_BUILD_STATUS_NOT_FIXED
|
|
}
|
|
|
|
return apollopb.BuildStatus_BUILD_STATUS_FIXED
|
|
}
|