2019-05-16 06:39:08 +02:00
#!/usr/bin/python3
2016-12-29 00:34:56 -06:00
import os
2018-08-13 22:47:01 -05:00
import re
2016-12-29 00:34:56 -06:00
import shutil
import subprocess
import sys
try :
from xml . etree import cElementTree as ET
except ImportError :
import cElementTree as ET
2017-10-11 22:19:24 -05:00
import osc . conf
2016-12-29 00:34:56 -06:00
import osc . core
2019-05-16 13:13:25 +02:00
from osc . util . helper import decode_list
2018-08-16 23:35:10 -05:00
from osclib . conf import Config
2018-01-17 18:08:40 -06:00
from osclib . core import devel_project_get
2018-01-17 20:36:03 -06:00
from osclib . core import devel_project_fallback
2018-11-06 16:15:20 -06:00
from osclib . core import group_members
2019-09-24 11:25:47 -05:00
from osclib . core import package_kind
2019-08-12 15:13:12 +02:00
from osclib . core import source_file_load
from osclib . core import target_archs
2019-05-16 06:39:08 +02:00
from urllib . error import HTTPError
2018-11-16 08:32:25 +01:00
2016-12-29 00:34:56 -06:00
import ReviewBot
2018-03-09 13:37:23 +01:00
from osclib . conf import str2bool
2016-12-29 00:34:56 -06:00
class CheckSource ( ReviewBot . ReviewBot ) :
SCRIPT_PATH = os . path . dirname ( os . path . realpath ( __file__ ) )
2019-08-12 15:13:12 +02:00
AUDIT_BUG_URL = " https://en.opensuse.org/openSUSE:Package_security_guidelines#audit_bugs "
AUDIT_BUG_MESSAGE = " The package is submitted to an official product and it has warnings that indicate that it need to go through a security review. Those warnings can only be ignored in devel projects. For more information please read: {} . " . format ( AUDIT_BUG_URL )
2016-12-29 00:34:56 -06:00
def __init__ ( self , * args , * * kwargs ) :
ReviewBot . ReviewBot . __init__ ( self , * args , * * kwargs )
2017-06-20 16:29:19 -05:00
# ReviewBot options.
2018-03-16 14:21:41 -05:00
self . request_default_return = True
2017-06-20 16:29:19 -05:00
2016-12-29 00:34:56 -06:00
self . skip_add_reviews = False
2017-10-11 22:19:24 -05:00
def target_project_config ( self , project ) :
# Load project config and allow for remote entries.
2018-08-16 23:35:10 -05:00
config = Config . get ( self . apiurl , project )
2017-10-11 22:19:24 -05:00
2018-06-28 12:13:26 -05:00
self . single_action_require = str2bool ( config . get ( ' check-source-single-action-require ' , ' False ' ) )
2018-03-09 13:37:23 +01:00
self . ignore_devel = not str2bool ( config . get ( ' devel-project-enforce ' , ' False ' ) )
2018-03-16 14:09:02 -05:00
self . in_air_rename_allow = str2bool ( config . get ( ' check-source-in-air-rename-allow ' , ' False ' ) )
2018-03-09 13:45:14 +01:00
self . add_review_team = str2bool ( config . get ( ' check-source-add-review-team ' , ' True ' ) )
2017-10-11 22:19:24 -05:00
self . review_team = config . get ( ' review-team ' )
2019-09-09 16:51:54 -05:00
self . mail_release_list = config . get ( ' mail-release-list ' )
2018-08-16 23:29:25 -05:00
self . staging_group = config . get ( ' staging-group ' )
2017-10-11 22:19:24 -05:00
self . repo_checker = config . get ( ' repo-checker ' )
self . devel_whitelist = config . get ( ' devel-whitelist ' , ' ' ) . split ( )
2018-09-12 11:48:23 +02:00
self . skip_add_reviews = False
2019-08-12 15:13:12 +02:00
self . security_review_team = config . get ( ' security-review-team ' , ' security-team ' )
self . bad_rpmlint_entries = config . get ( ' bad-rpmlint-entries ' , ' ' ) . split ( )
2017-10-11 22:19:24 -05:00
2018-06-28 12:11:24 -05:00
if self . action . type == ' maintenance_incident ' :
# The workflow effectively enforces the names to match and the
# parent code sets target_package from source_package so this check
# becomes useless and awkward to perform.
self . in_air_rename_allow = True
2018-06-28 12:13:26 -05:00
# The target project will be set to product and thus inherit
# settings, but override since real target is not product.
self . single_action_require = False
2018-06-28 12:25:24 -05:00
# It might make sense to supersede maintbot, but for now.
self . skip_add_reviews = True
2016-12-29 00:34:56 -06:00
def check_source_submission ( self , source_project , source_package , source_revision , target_project , target_package ) :
super ( CheckSource , self ) . check_source_submission ( source_project , source_package , source_revision , target_project , target_package )
2017-10-11 22:19:24 -05:00
self . target_project_config ( target_project )
2016-12-29 00:34:56 -06:00
2018-06-28 12:13:26 -05:00
if self . single_action_require and len ( self . request . actions ) != 1 :
self . review_messages [ ' declined ' ] = ' Only one action per request allowed '
return False
2019-09-24 11:25:47 -05:00
kind = package_kind ( self . apiurl , target_project , target_package )
if kind == ' meta ' :
self . review_messages [ ' accepted ' ] = ' Skipping all checks for meta packages '
2018-03-15 15:43:37 +01:00
return True
2019-09-24 11:26:52 -05:00
elif kind != ' source ' :
self . review_messages [ ' accepted ' ] = ' May not modify a non-source package of type {} ' . format ( kind )
return False
2018-03-15 15:43:37 +01:00
2018-06-28 12:10:19 -05:00
inair_renamed = target_package != source_package
2016-12-29 00:34:56 -06:00
if not self . ignore_devel :
2017-10-11 22:14:42 -05:00
self . logger . info ( ' checking if target package exists and has devel project ' )
2018-01-17 18:08:40 -06:00
devel_project , devel_package = devel_project_get ( self . apiurl , target_project , target_package )
2016-12-29 00:34:56 -06:00
if devel_project :
2017-02-10 11:19:25 -06:00
if ( source_project != devel_project or source_package != devel_package ) and \
not ( source_project == target_project and source_package == target_package ) :
# Not from proper devel project/package and not self-submission.
2016-12-29 00:34:56 -06:00
self . review_messages [ ' declined ' ] = ' Expected submission from devel package %s / %s ' % ( devel_project , devel_package )
return False
else :
# Check to see if other packages exist with the same source project
# which indicates that the project has already been used as devel.
if not self . is_devel_project ( source_project , target_project ) :
2018-01-02 13:27:10 +01:00
self . review_messages [ ' declined ' ] = (
' %s is not a devel project of %s , submit the package to a devel project first. '
' See https://en.opensuse.org/openSUSE:How_to_contribute_to_Factory#How_to_request_a_new_devel_project for details. '
) % ( source_project , target_project )
2016-12-29 00:34:56 -06:00
return False
2018-06-28 12:10:19 -05:00
else :
if source_project . endswith ( ' :Update ' ) :
# Allow for submission like:
# - source: openSUSE:Leap:15.0:Update/google-compute-engine.8258
# - target: openSUSE:Leap:15.1/google-compute-engine
# Note: home:jberry:Update would also be allowed via this condition,
# but that should be handled by leaper and human review.
2018-08-13 22:47:01 -05:00
# Ignore a dot in package name (ex. tpm2.0-abrmd) and instead
# only look for ending in dot number.
match = re . match ( r ' (.*) \ . \ d+$ ' , source_package )
if match :
inair_renamed = target_package != match . group ( 1 )
2018-06-28 12:10:19 -05:00
if not self . in_air_rename_allow and inair_renamed :
self . review_messages [ ' declined ' ] = ' Source and target package names must match '
return False
2016-12-29 00:34:56 -06:00
# Checkout and see if renaming package screws up version parsing.
dir = os . path . expanduser ( ' ~/co/ %s ' % self . request . reqid )
if os . path . exists ( dir ) :
2019-05-11 14:25:02 +02:00
self . logger . warning ( ' directory %s already exists ' % dir )
2016-12-29 00:34:56 -06:00
shutil . rmtree ( dir )
os . makedirs ( dir )
os . chdir ( dir )
old_info = { ' version ' : None }
try :
CheckSource . checkout_package ( self . apiurl , target_project , target_package , pathname = dir ,
server_service_files = True , expand_link = True )
shutil . rmtree ( os . path . join ( target_package , ' .osc ' ) )
os . rename ( target_package , ' _old ' )
2017-04-19 09:21:45 -05:00
old_info = self . package_source_parse ( target_project , target_package )
2019-08-27 14:49:17 -05:00
except HTTPError as e :
if e . code == 404 :
self . logger . info ( ' target package does not exist %s / %s ' % ( target_project , target_package ) )
else :
raise e
2016-12-29 00:34:56 -06:00
CheckSource . checkout_package ( self . apiurl , source_project , source_package , revision = source_revision ,
pathname = dir , server_service_files = True , expand_link = True )
os . rename ( source_package , target_package )
shutil . rmtree ( os . path . join ( target_package , ' .osc ' ) )
2017-04-19 09:21:45 -05:00
new_info = self . package_source_parse ( source_project , source_package , source_revision )
2019-09-20 14:37:46 +02:00
if not new_info [ ' filename ' ] . endswith ( ' .kiwi ' ) and new_info [ ' name ' ] != target_package :
2016-12-29 00:34:56 -06:00
shutil . rmtree ( dir )
self . review_messages [ ' declined ' ] = " A package submitted as %s has to build as ' Name: %s ' - found Name ' %s ' " % ( target_package , target_package , new_info [ ' name ' ] )
return False
2017-08-18 15:18:04 -05:00
# Run check_source.pl script and interpret output.
source_checker = os . path . join ( CheckSource . SCRIPT_PATH , ' check_source.pl ' )
2016-12-29 00:34:56 -06:00
civs = ' '
new_version = None
if old_info [ ' version ' ] and old_info [ ' version ' ] != new_info [ ' version ' ] :
new_version = new_info [ ' version ' ]
civs + = " NEW_VERSION= ' {} ' " . format ( new_version )
civs + = ' LC_ALL=C perl %s _old %s 2>&1 ' % ( source_checker , target_package )
p = subprocess . Popen ( civs , shell = True , stdout = subprocess . PIPE , close_fds = True )
ret = os . waitpid ( p . pid , 0 ) [ 1 ]
2019-05-16 13:13:25 +02:00
checked = decode_list ( p . stdout . readlines ( ) )
2016-12-29 00:34:56 -06:00
2019-05-16 13:13:25 +02:00
output = ' ' . join ( checked ) . replace ( ' \033 ' , ' ' )
2016-12-29 00:34:56 -06:00
os . chdir ( ' /tmp ' )
2017-11-22 17:29:14 +08:00
# ret = 0 : Good
# ret = 1 : Bad
# ret = 2 : Bad but can be non-fatal in some cases
if ret > 1 and target_project . startswith ( ' openSUSE:Leap: ' ) and ( source_project . startswith ( ' SUSE:SLE-15: ' ) or source_project . startswith ( ' openSUSE:Factory ' ) ) :
pass
elif ret != 0 :
2016-12-29 00:34:56 -06:00
shutil . rmtree ( dir )
self . review_messages [ ' declined ' ] = " Output of check script: \n " + output
return False
shutil . rmtree ( dir )
self . review_messages [ ' accepted ' ] = ' Check script succeeded '
if len ( checked ) :
self . review_messages [ ' accepted ' ] + = " \n \n Output of check script (non-fatal): \n " + output
if not self . skip_add_reviews :
2018-03-09 13:45:14 +01:00
if self . add_review_team and self . review_team is not None :
2017-08-18 15:15:26 -05:00
self . add_review ( self . request , by_group = self . review_team , msg = ' Please review sources ' )
2016-12-29 00:34:56 -06:00
2017-04-19 09:30:57 -05:00
if self . only_changes ( ) :
self . logger . debug ( ' only .changes modifications ' )
2018-11-06 16:15:20 -06:00
if self . staging_group and self . review_user in group_members ( self . apiurl , self . staging_group ) :
if not self . dryrun :
osc . core . change_review_state ( self . apiurl , str ( self . request . reqid ) , ' accepted ' ,
by_group = self . staging_group ,
message = ' skipping the staging process since only .changes modifications ' )
else :
self . logger . debug ( ' unable to skip staging review since not a member of staging group ' )
2017-04-19 09:30:57 -05:00
elif self . repo_checker is not None :
2016-12-29 00:34:56 -06:00
self . add_review ( self . request , by_user = self . repo_checker , msg = ' Please review build success ' )
2019-08-12 15:13:12 +02:00
if self . bad_rpmlint_entries :
if self . has_whitelist_warnings ( source_project , source_package , target_project , target_package ) :
# if there are any add a review for the security team
# maybe add the found warnings to the message for the review
self . add_review ( self . request , by_group = self . security_review_team , msg = CheckSource . AUDIT_BUG_MESSAGE )
if self . suppresses_whitelist_warnings ( source_project , source_package ) :
self . add_review ( self . request , by_group = self . security_review_team , msg = CheckSource . AUDIT_BUG_MESSAGE )
2016-12-29 00:34:56 -06:00
return True
2019-09-05 09:24:32 +02:00
def suppresses_whitelist_warnings ( self , source_project , source_package ) :
2019-08-12 15:13:12 +02:00
# checks if there's a rpmlintrc that suppresses warnings that we check
found_entries = set ( )
contents = source_file_load ( self . apiurl , source_project , source_package , source_package + ' -rpmlintrc ' )
if contents :
2019-09-05 09:24:32 +02:00
contents = re . sub ( r ' (?m)^ *#.* \ n? ' , ' ' , contents )
2019-08-12 15:13:12 +02:00
matches = re . findall ( r ' addFilter \ ([ " \' ]([^ " \' ]+)[ " \' ] \ ) ' , contents )
2019-09-05 09:24:32 +02:00
# this is a bit tricky. Since users can specify arbitrary regular expresions it's not easy
# to match bad_rpmlint_entries against what we found
2019-08-12 15:13:12 +02:00
for entry in self . bad_rpmlint_entries :
for match in matches :
2019-09-05 09:24:32 +02:00
# First we try to see if our entries appear verbatim in the rpmlint entries
if entry in match :
self . logger . info ( f ' found suppressed whitelist warning: { match } ' )
found_entries . add ( match )
# if that's not the case then we check if one of the entries in the rpmlint file would match one
# of our entries (e.g. addFilter(".*")
elif re . search ( match , entry ) and match not in found_entries :
self . logger . info ( f ' found rpmlint entry that suppresses an important warning: { match } ' )
found_entries . add ( match )
2019-08-12 15:13:12 +02:00
return found_entries
def has_whitelist_warnings ( self , source_project , source_package , target_project , target_package ) :
# this checks if this is a submit to an product project and it has warnings for non-whitelisted permissions/files
found_entries = set ( )
url = osc . core . makeurl ( self . apiurl , [ ' build ' , target_project ] )
xml = ET . parse ( osc . core . http_GET ( url ) ) . getroot ( )
for f in xml . findall ( ' entry ' ) :
# we check all repos in the source project for errors that exist in the target project
repo = f . attrib [ ' name ' ]
query = { ' last ' : 1 , }
for arch in target_archs ( self . apiurl , source_project , repo ) :
url = osc . core . makeurl ( self . apiurl , [ ' build ' , source_project , repo , arch , source_package , ' _log ' ] , query = query )
try :
result = osc . core . http_GET ( url )
contents = str ( result . read ( ) )
for entry in self . bad_rpmlint_entries :
if ( ' : W: ' + entry in contents ) and not ( entry in found_entries ) :
self . logger . info ( f ' found missing whitelist for warning: { entry } ' )
found_entries . add ( entry )
except HTTPError as e :
2019-09-12 11:51:38 +02:00
self . logger . info ( ' ERROR in URL %s [ %s ] ' % ( url , e ) )
2019-08-12 15:13:12 +02:00
return found_entries
2016-12-29 00:34:56 -06:00
def is_devel_project ( self , source_project , target_project ) :
2017-10-11 22:19:24 -05:00
if source_project in self . devel_whitelist :
2016-12-29 00:34:56 -06:00
return True
2017-07-18 17:08:02 -05:00
# Allow any projects already used as devel projects for other packages.
2016-12-29 00:34:56 -06:00
search = {
' package ' : " @project= ' %s ' and devel/@project= ' %s ' " % ( target_project , source_project ) ,
}
result = osc . core . search ( self . apiurl , * * search )
return result [ ' package ' ] . attrib [ ' matches ' ] != ' 0 '
@staticmethod
def checkout_package ( * args , * * kwargs ) :
_stdout = sys . stdout
2019-05-16 13:13:25 +02:00
sys . stdout = open ( os . devnull , ' w ' )
2016-12-29 00:34:56 -06:00
try :
result = osc . core . checkout_package ( * args , * * kwargs )
finally :
sys . stdout = _stdout
return result
2017-04-19 09:21:45 -05:00
def package_source_parse ( self , project , package , revision = None ) :
2016-12-29 00:34:56 -06:00
query = { ' view ' : ' info ' , ' parse ' : 1 }
if revision :
query [ ' rev ' ] = revision
url = osc . core . makeurl ( self . apiurl , [ ' source ' , project , package ] , query )
ret = { ' name ' : None , ' version ' : None }
try :
xml = ET . parse ( osc . core . http_GET ( url ) ) . getroot ( )
2018-11-16 08:32:25 +01:00
except HTTPError as e :
2016-12-29 00:34:56 -06:00
self . logger . error ( ' ERROR in URL %s [ %s ] ' % ( url , e ) )
return ret
# ET boolean check fails.
if xml . find ( ' name ' ) is not None :
ret [ ' name ' ] = xml . find ( ' name ' ) . text
if xml . find ( ' version ' ) is not None :
ret [ ' version ' ] = xml . find ( ' version ' ) . text
2019-09-20 14:37:46 +02:00
if xml . find ( ' filename ' ) is not None :
ret [ ' filename ' ] = xml . find ( ' filename ' ) . text
2016-12-29 00:34:56 -06:00
return ret
2017-04-19 09:30:57 -05:00
def only_changes ( self ) :
u = osc . core . makeurl ( self . apiurl , [ ' request ' , self . request . reqid ] ,
{ ' cmd ' : ' diff ' , ' view ' : ' xml ' } )
try :
diff = ET . parse ( osc . core . http_POST ( u ) ) . getroot ( )
for f in diff . findall ( ' action/sourcediff/files/file/*[@name] ' ) :
if not f . get ( ' name ' ) . endswith ( ' .changes ' ) :
return False
return True
except :
pass
return False
2016-12-29 00:34:56 -06:00
def check_action_add_role ( self , request , action ) :
# Decline add_role request (assumed the bot acting on requests to Factory or similar).
message = ' Roles to packages are granted in the devel project, not in %s . ' % action . tgt_project
if action . tgt_package is not None :
2018-01-17 20:36:03 -06:00
project , package = devel_project_fallback ( self . apiurl , action . tgt_project , action . tgt_package )
2018-01-17 18:08:40 -06:00
message + = ' Send this request to {} / {} . ' . format ( project , package )
2016-12-29 00:34:56 -06:00
self . review_messages [ ' declined ' ] = message
return False
2018-09-17 17:11:33 -05:00
def check_action_delete_package ( self , request , action ) :
2018-01-03 20:01:12 -06:00
self . target_project_config ( action . tgt_project )
2016-12-29 00:34:56 -06:00
try :
result = osc . core . show_project_sourceinfo ( self . apiurl , action . tgt_project , True , ( action . tgt_package ) )
root = ET . fromstring ( result )
2018-11-16 08:32:25 +01:00
except HTTPError :
2016-12-29 00:34:56 -06:00
return None
2017-10-12 19:11:01 +08:00
# Decline the delete request if there is another delete/submit request against the same package
query = " match=state/@name= ' new ' +and+(action/target/@project= ' {} ' +and+action/target/@package= ' {} ' ) " \
" +and+(action/@type= ' delete ' +or+action/@type= ' submit ' ) " . format ( action . tgt_project , action . tgt_package )
url = osc . core . makeurl ( self . apiurl , [ ' search ' , ' request ' ] , query )
matches = ET . parse ( osc . core . http_GET ( url ) ) . getroot ( )
if int ( matches . attrib [ ' matches ' ] ) > 1 :
ids = [ rq . attrib [ ' id ' ] for rq in matches . findall ( ' request ' ) ]
self . review_messages [ ' declined ' ] = " There is a pending request %s to %s / %s in process. " % ( ' , ' . join ( ids ) , action . tgt_project , action . tgt_package )
return False
2019-01-09 12:58:54 +01:00
# Decline delete requests against linked flavor package
linked = root . find ( ' sourceinfo/linked ' )
if not ( linked is None or self . check_linked_package ( action , linked ) ) :
2016-12-29 00:34:56 -06:00
return False
2019-01-09 12:58:54 +01:00
if not self . ignore_devel :
self . devel_project_review_ensure ( request , action . tgt_project , action . tgt_package )
if not self . skip_add_reviews and self . repo_checker is not None :
self . add_review ( self . request , by_user = self . repo_checker , msg = ' Is this delete request safe? ' )
return True
def check_linked_package ( self , action , linked ) :
if linked . get ( ' project ' , action . tgt_project ) != action . tgt_project :
return True
linked_package = linked . get ( ' package ' )
self . review_messages [ ' declined ' ] = " Delete the package %s instead " % ( linked_package )
return False
2018-09-17 17:06:18 -05:00
def check_action_delete_project ( self , request , action ) :
# Presumably if the request is valid the bot should be disabled or
# overridden, but seems like no valid case for allowing this (see #1696).
self . review_messages [ ' declined ' ] = ' Deleting the {} project is not allowed. ' . format ( action . tgt_project )
return False
2018-09-17 17:10:01 -05:00
def check_action_delete_repository ( self , request , action ) :
2019-09-09 16:51:54 -05:00
self . target_project_config ( action . tgt_project )
if self . mail_release_list :
2019-09-09 16:57:26 -05:00
self . review_messages [ ' declined ' ] = ' Deleting repositories is not allowed. ' \
' Contact {} to discuss further. ' . format ( self . mail_release_list )
2018-09-17 17:10:01 -05:00
return False
self . review_messages [ ' accepted ' ] = ' unhandled: removing repository '
return True
2016-12-29 00:34:56 -06:00
class CommandLineInterface ( ReviewBot . CommandLineInterface ) :
def __init__ ( self , * args , * * kwargs ) :
ReviewBot . CommandLineInterface . __init__ ( self , args , kwargs )
self . clazz = CheckSource
def get_optparser ( self ) :
parser = ReviewBot . CommandLineInterface . get_optparser ( self )
2017-10-04 15:13:32 -05:00
parser . add_option ( ' --skip-add-reviews ' , action = ' store_true ' , default = False , help = ' skip adding review after completing checks ' )
2016-12-29 00:34:56 -06:00
return parser
def setup_checker ( self ) :
bot = ReviewBot . CommandLineInterface . setup_checker ( self )
bot . skip_add_reviews = self . options . skip_add_reviews
return bot
if __name__ == " __main__ " :
app = CommandLineInterface ( )
sys . exit ( app . main ( ) )