1
2 """
3
4 @author: Fabio Erculiani <lxnay@sabayonlinux.org>
5 @contact: lxnay@sabayonlinux.org
6 @copyright: Fabio Erculiani
7 @license: GPL-2
8
9 B{Entropy Framework Security module}.
10
11 This module contains Entropy GLSA-based Security interfaces.
12
13
14 """
15 import os
16 import shutil
17 from entropy.exceptions import IncorrectParameter, InvalidData
18 from entropy.const import etpConst, etpCache, etpUi, const_setup_perms
19 from entropy.i18n import _
20 from entropy.output import blue, bold, red, darkgreen, darkred
21
23
24 """
25 ~~ GIVES YOU WINGS ~~
26 """
27
28 """
29 @note: thanks to Gentoo "gentoolkit" package, License below:
30 @note: This program is licensed under the GPL, version 2
31
32 @note: WARNING: this code is not intended to replace any Security mechanism,
33 @note: but it's just a way to handle Gentoo GLSAs.
34 @note: There are possible security holes and probably bugs in this code.
35
36 This class implements the Entropy packages Security framework.
37 It can be used to retrieve security advisories, get information
38 about unapplied advisories, etc.
39
40 """
41
42 import entropy.tools as entropyTools
43 - def __init__(self, entropy_client_instance):
44
45 """
46 Instance constructor.
47
48 @param entropy_client_instance: a valid entropy.client.interfaces.Client
49 instance
50 @type entropy_client_instance: entropy.client.interfaces.Client instance
51 """
52
53
54 from entropy.client.interfaces import Client
55 if not isinstance(entropy_client_instance, Client):
56 mytxt = _("A valid Client interface instance is needed")
57 raise IncorrectParameter("IncorrectParameter: %s" % (mytxt,))
58
59 self.Entropy = entropy_client_instance
60 from entropy.cache import EntropyCacher
61 self.__cacher = EntropyCacher()
62 from entropy.core.settings.base import SystemSettings
63 self.SystemSettings = SystemSettings()
64 self.lastfetch = None
65 self.previous_checksum = "0"
66 self.advisories_changed = None
67 self.adv_metadata = None
68 self.affected_atoms = set()
69
70 from xml.dom import minidom
71 self.minidom = minidom
72
73 self.op_mappings = {
74 "le": "<=",
75 "lt": "<",
76 "eq": "=",
77 "gt": ">",
78 "ge": ">=",
79 "rge": ">=",
80 "rle": "<=",
81 "rgt": ">",
82 "rlt": "<"
83 }
84
85 security_url = \
86 self.SystemSettings['repositories']['security_advisories_url']
87 security_file = os.path.basename(security_url)
88 md5_ext = etpConst['packagesmd5fileext']
89
90 self.unpackdir = os.path.join(etpConst['entropyunpackdir'],
91 "security-%s" % (self.entropyTools.get_random_number(),))
92 self.security_url = security_url
93 self.unpacked_package = os.path.join(self.unpackdir, "glsa_package")
94 self.security_url_checksum = security_url + md5_ext
95
96 self.download_package = os.path.join(self.unpackdir, security_file)
97 self.download_package_checksum = self.download_package + md5_ext
98 self.old_download_package_checksum = os.path.join(
99 etpConst['dumpstoragedir'], os.path.basename(security_url)
100 ) + md5_ext
101
102 self.security_package = os.path.join(etpConst['securitydir'],
103 os.path.basename(security_url))
104 self.security_package_checksum = self.security_package + md5_ext
105
106 try:
107
108 if os.path.isfile(etpConst['securitydir']) or \
109 os.path.islink(etpConst['securitydir']):
110 os.remove(etpConst['securitydir'])
111
112 if not os.path.isdir(etpConst['securitydir']):
113 os.makedirs(etpConst['securitydir'], 0o775)
114
115 except OSError:
116 pass
117 const_setup_perms(etpConst['securitydir'], etpConst['entropygid'])
118
119 if os.access(self.old_download_package_checksum, os.R_OK) and \
120 os.path.isfile(self.old_download_package_checksum):
121
122 f_down = open(self.old_download_package_checksum)
123 try:
124 self.previous_checksum = f_down.readline().strip().split()[0]
125 except (IndexError, OSError, IOError,):
126 pass
127 f_down.close()
128
130 """
131 Prepare GLSAs unpack directory and its permissions.
132 """
133 if os.path.isfile(self.unpackdir) or os.path.islink(self.unpackdir):
134 os.remove(self.unpackdir)
135
136 if os.path.isdir(self.unpackdir):
137 shutil.rmtree(self.unpackdir, True)
138 try:
139 os.rmdir(self.unpackdir)
140 except OSError:
141 pass
142
143 os.makedirs(self.unpackdir, 0o775)
144 const_setup_perms(self.unpackdir, etpConst['entropygid'])
145
147 """
148 Download GLSA compressed package from a trusted source.
149 """
150 return self.__generic_download(self.security_url, self.download_package)
151
153 """
154 Download GLSA compressed package checksum (md5) from a trusted source.
155 """
156 return self.__generic_download(self.security_url_checksum,
157 self.download_package_checksum, show_speed = False)
158
160 """
161 Generic, secure, URL download method.
162
163 @param url: download URL
164 @type url: string
165 @param save_to: path to save file
166 @type save_to: string
167 @keyword show_speed: if True, download speed will be shown
168 @type show_speed: bool
169 @return: download status (True if download succeeded)
170 @rtype: bool
171 """
172 fetcher = self.Entropy.urlFetcher(url, save_to, resume = False,
173 show_speed = show_speed)
174 fetcher.progress = self.Entropy.progress
175 rc_fetch = fetcher.download()
176 del fetcher
177 if rc_fetch in ("-1", "-2", "-3", "-4"):
178 return False
179
180 self.Entropy.setup_default_file_perms(save_to)
181 return True
182
184 """
185 Verify downloaded GLSA checksum against downloaded GLSA package.
186 """
187
188 if not os.path.isfile(self.download_package_checksum) or \
189 not os.access(self.download_package_checksum, os.R_OK):
190 return 1
191
192 f_down = open(self.download_package_checksum)
193 read_err = False
194 try:
195 checksum = f_down.readline().strip().split()[0]
196 except (OSError, IOError, IndexError,):
197 read_err = True
198
199 f_down.close()
200 if read_err:
201 return 2
202
203 self.advisories_changed = True
204 if checksum == self.previous_checksum:
205 self.advisories_changed = False
206
207 md5res = self.entropyTools.compare_md5(self.download_package, checksum)
208 if not md5res:
209 return 3
210 return 0
211
213 """
214 Unpack downloaded GLSA package containing GLSA advisories.
215 """
216 rc_unpack = self.entropyTools.uncompress_tar_bz2(
217 self.download_package,
218 self.unpacked_package,
219 catchEmpty = True
220 )
221 const_setup_perms(self.unpacked_package, etpConst['entropygid'])
222 return rc_unpack
223
233
235 """
236 Place unpacked advisories in place (into etpConst['securitydir']).
237 """
238 for advfile in os.listdir(self.unpacked_package):
239 from_file = os.path.join(self.unpacked_package, advfile)
240 to_file = os.path.join(etpConst['securitydir'], advfile)
241 try:
242 os.rename(from_file, to_file)
243 except OSError:
244 shutil.move(from_file, to_file)
245
247 """
248 Remove GLSA unpack directory.
249 """
250 shutil.rmtree(self.unpackdir, True)
251
252 - def clear(self, xcache = False):
253 """
254 Clear instance cache (RAM and on-disk).
255
256 @keyword xcache: also remove Entropy on-disk cache if True
257 @type xcache: bool
258 """
259 self.adv_metadata = None
260 if xcache:
261 self.Entropy.clear_dump_cache(etpCache['advisories'])
262
264 """
265 Return cached advisories information metadata. It first tries to load
266 them from RAM and, in case of failure, it tries to gather the info
267 from disk, using EntropyCacher.
268 """
269 if self.adv_metadata != None:
270 return self.adv_metadata
271
272 if self.Entropy.xcache:
273 dir_checksum = self.entropyTools.md5sum_directory(
274 etpConst['securitydir'])
275 c_hash = "%s%s" % (
276 etpCache['advisories'], hash("%s|%s|%s" % (
277 hash(self.SystemSettings['repositories']['branch']),
278 hash(dir_checksum),
279 hash(etpConst['systemroot']),
280 )),
281 )
282 adv_metadata = self.__cacher.pop(c_hash)
283 if adv_metadata != None:
284 self.adv_metadata = adv_metadata.copy()
285 return self.adv_metadata
286
288 """
289 Set advisories information metadata cache.
290
291 @param adv_metadata: advisories metadata to store
292 @type adv_metadata: dict
293 """
294 if self.Entropy.xcache:
295 dir_checksum = self.entropyTools.md5sum_directory(
296 etpConst['securitydir'])
297 c_hash = "%s%s" % (
298 etpCache['advisories'], hash("%s|%s|%s" % (
299 hash(self.SystemSettings['repositories']['branch']),
300 hash(dir_checksum),
301 hash(etpConst['systemroot']),
302 )),
303 )
304 self.__cacher.push(c_hash, adv_metadata)
305
307 """
308 Return a list of advisory files. Internal method.
309 """
310 if not self.check_advisories_availability():
311 return []
312 xmls = os.listdir(etpConst['securitydir'])
313 xmls = sorted([x for x in xmls if x.endswith(".xml") and \
314 x.startswith("glsa-")])
315 return xmls
316
376
378 """
379 This function filters advisories metadata dict removing non-applicable
380 ones.
381
382 @param adv_metadata: security advisories metadata dict
383 @type adv_metadata: dict
384 @return: filtered security advisories metadata
385 @rtype: dict
386 """
387 keys = list(adv_metadata.keys())
388 for key in keys:
389 valid = True
390 if adv_metadata[key]['affected']:
391 affected = adv_metadata[key]['affected']
392 affected_keys = list(affected.keys())
393 valid = False
394 skipping_keys = set()
395 for a_key in affected_keys:
396 match = self.Entropy.atom_match(a_key)
397 if match[0] != -1:
398
399 valid = True
400 else:
401 skipping_keys.add(a_key)
402 if not valid:
403 del adv_metadata[key]
404 for a_key in skipping_keys:
405 try:
406 del adv_metadata[key]['affected'][a_key]
407 except KeyError:
408 continue
409 try:
410 if not adv_metadata[key]['affected']:
411 del adv_metadata[key]
412 except KeyError:
413 continue
414
415 return adv_metadata
416
418 """
419 Determine whether the system is affected by vulnerabilities listed
420 in the provided security advisory identifier.
421
422 @param adv_key: security advisories identifier
423 @type adv_key: string
424 @keyword adv_data: use the provided security advisories instead of
425 the stored one.
426 @type adv_data: dict
427 @return: True, if system is affected by vulnerabilities listed in the
428 provided security advisory.
429 @rtype: bool
430 """
431 if not adv_data:
432 adv_data = self.get_advisories_metadata()
433 if adv_key not in adv_data:
434 return False
435 mydata = adv_data[adv_key].copy()
436 del adv_data
437
438 if not mydata['affected']:
439 return False
440
441 for key in mydata['affected']:
442
443 vul_atoms = mydata['affected'][key][0]['vul_atoms']
444 unaff_atoms = mydata['affected'][key][0]['unaff_atoms']
445 unaffected_atoms = set()
446 if not vul_atoms:
447 return False
448 for atom in unaff_atoms:
449 matches = self.Entropy.clientDbconn.atomMatch(atom,
450 multiMatch = True)
451 for idpackage in matches[0]:
452 unaffected_atoms.add((idpackage, 0))
453
454 for atom in vul_atoms:
455 match = self.Entropy.clientDbconn.atomMatch(atom)
456 if (match[0] != -1) and (match not in unaffected_atoms):
457 self.affected_atoms.add(atom)
458 return True
459 return False
460
462 """
463 Return advisories metadata for installed packages containing
464 vulnerabilities.
465
466 @return: advisories metadata for vulnerable packages.
467 @rtype: dict
468 """
469 return self.__get_affection()
470
472 """
473 Return advisories metadata for installed packages not affected
474 by any vulnerability.
475
476 @return: advisories metadata for NON-vulnerable packages.
477 @rtype: dict
478 """
479 return self.__get_affection(affected = False)
480
482 """
483 If not affected: not affected packages will be returned.
484 If affected: affected packages will be returned.
485 """
486 adv_data = self.get_advisories_metadata()
487 adv_data_keys = list(adv_data.keys())
488 valid_keys = set()
489 for adv in adv_data_keys:
490 is_affected = self.is_affected(adv, adv_data)
491 if affected == is_affected:
492 valid_keys.add(adv)
493
494 for key in adv_data_keys:
495 if key not in valid_keys:
496 try:
497 del adv_data[key]
498 except KeyError:
499 pass
500
501 for adv in adv_data:
502 for key in list(adv_data[adv]['affected'].keys()):
503 atoms = adv_data[adv]['affected'][key][0]['vul_atoms']
504 applicable = True
505 for atom in atoms:
506 if atom in self.affected_atoms:
507 applicable = False
508 break
509 if applicable == affected:
510 del adv_data[adv]['affected'][key]
511 return adv_data
512
514 """
515 Return a list of package atoms affected by vulnerabilities.
516
517 @return: list (set) of package atoms affected by vulnerabilities
518 @rtype: set
519 """
520 adv_data = self.get_advisories_metadata()
521 adv_data_keys = list(adv_data.keys())
522 del adv_data
523 self.affected_atoms.clear()
524 for key in adv_data_keys:
525 self.is_affected(key)
526 return self.affected_atoms
527
656
658 """
659 creates from the information in the I{versionNode} a
660 version string (format <op><version>).
661
662 @param vnode: a <vulnerable> or <unaffected> Node that
663 contains the version information for this atom
664 @type vnode: xml.dom.Node
665 @return: the version string
666 @rtype: string
667 """
668 return self.op_mappings[vnode.getAttribute("range")] + \
669 vnode.firstChild.data.strip()
670
672 """
673 creates from the given package name and information in the
674 I{versionNode} a (syntactical) valid portage atom.
675
676 @param pkgname: the name of the package for this atom
677 @type pkgname: string
678 @param vnode: a <vulnerable> or <unaffected> Node that
679 contains the version information for this atom
680 @type vnode: xml.dom.Node
681 @return: the portage atom
682 @rtype: string
683 """
684 return str(self.op_mappings[vnode.getAttribute("range")] + pkgname + \
685 "-" + vnode.firstChild.data.strip())
686
688 """
689 Return whether security advisories are available.
690
691 @return: availability
692 @rtype: bool
693 """
694 if not os.path.lexists(etpConst['securitydir']):
695 return False
696 if not os.path.isdir(etpConst['securitydir']):
697 return False
698 else:
699 return True
700 return False
701
703 """
704 This is the service method for remotely fetch advisories metadata.
705
706 @keyword do_cache: generates advisories cache
707 @type do_cache: bool
708 @return: execution status (0 means all file)
709 @rtype: int
710 """
711 mytxt = "%s: %s" % (
712 bold(_("Security Advisories")),
713 blue(_("testing service connection")),
714 )
715 self.Entropy.updateProgress(
716 mytxt,
717 importance = 2,
718 type = "info",
719 header = red(" @@ "),
720 footer = red(" ...")
721 )
722
723 mytxt = "%s: %s %s" % (
724 bold(_("Security Advisories")),
725 blue(_("getting latest GLSAs")),
726 red("..."),
727 )
728 self.Entropy.updateProgress(
729 mytxt,
730 importance = 2,
731 type = "info",
732 header = red(" @@ ")
733 )
734
735 gave_up = self.Entropy.lock_check(
736 self.Entropy.resources_check_lock)
737 if gave_up:
738 return 7
739
740 locked = self.Entropy.application_lock_check()
741 if locked:
742 return 4
743
744
745 acquired = self.Entropy.resources_create_lock()
746 if not acquired:
747 return 4
748 try:
749 rc_lock = self.__run_fetch()
750 except:
751 self.Entropy.resources_remove_lock()
752 raise
753 if rc_lock != 0:
754 return rc_lock
755
756 self.Entropy.resources_remove_lock()
757
758 if self.advisories_changed:
759 advtext = "%s: %s" % (
760 bold(_("Security Advisories")),
761 darkgreen(_("updated successfully")),
762 )
763 else:
764 advtext = "%s: %s" % (
765 bold(_("Security Advisories")),
766 darkgreen(_("already up to date")),
767 )
768
769 if do_cache and self.Entropy.xcache:
770 self.get_advisories_metadata()
771 self.Entropy.updateProgress(
772 advtext,
773 importance = 2,
774 type = "info",
775 header = red(" @@ ")
776 )
777
778 return 0
779
781
782 self.__prepare_unpack()
783
784
785 status = self.__download_glsa_package()
786 self.lastfetch = status
787 if not status:
788 mytxt = "%s: %s." % (
789 bold(_("Security Advisories")),
790 darkred(_("unable to download the package, sorry")),
791 )
792 self.Entropy.updateProgress(
793 mytxt,
794 importance = 2,
795 type = "error",
796 header = red(" ## ")
797 )
798 self.Entropy.resources_remove_lock()
799 return 1
800
801 mytxt = "%s: %s %s" % (
802 bold(_("Security Advisories")),
803 blue(_("Verifying checksum")),
804 red("..."),
805 )
806 self.Entropy.updateProgress(
807 mytxt,
808 importance = 1,
809 type = "info",
810 header = red(" # "),
811 back = True
812 )
813
814
815 status = self.__download_glsa_package_cksum()
816 if not status:
817 mytxt = "%s: %s." % (
818 bold(_("Security Advisories")),
819 darkred(_("cannot download the checksum, sorry")),
820 )
821 self.Entropy.updateProgress(
822 mytxt,
823 importance = 2,
824 type = "error",
825 header = red(" ## ")
826 )
827 self.Entropy.resources_remove_lock()
828 return 2
829
830
831 status = self.__verify_checksum()
832
833 if status == 1:
834 mytxt = "%s: %s." % (
835 bold(_("Security Advisories")),
836 darkred(_("cannot open packages, sorry")),
837 )
838 self.Entropy.updateProgress(
839 mytxt,
840 importance = 2,
841 type = "error",
842 header = red(" ## ")
843 )
844 self.Entropy.resources_remove_lock()
845 return 3
846 elif status == 2:
847 mytxt = "%s: %s." % (
848 bold(_("Security Advisories")),
849 darkred(_("cannot read the checksum, sorry")),
850 )
851 self.Entropy.updateProgress(
852 mytxt,
853 importance = 2,
854 type = "error",
855 header = red(" ## ")
856 )
857 self.Entropy.resources_remove_lock()
858 return 4
859 elif status == 3:
860 mytxt = "%s: %s." % (
861 bold(_("Security Advisories")),
862 darkred(_("digest verification failed, sorry")),
863 )
864 self.Entropy.updateProgress(
865 mytxt,
866 importance = 2,
867 type = "error",
868 header = red(" ## ")
869 )
870 self.Entropy.resources_remove_lock()
871 return 5
872 elif status == 0:
873 mytxt = "%s: %s." % (
874 bold(_("Security Advisories")),
875 darkgreen(_("verification Successful")),
876 )
877 self.Entropy.updateProgress(
878 mytxt,
879 importance = 1,
880 type = "info",
881 header = red(" # ")
882 )
883 else:
884 mytxt = _("Return status not valid")
885 raise InvalidData("InvalidData: %s." % (mytxt,))
886
887
888 if os.path.isfile(self.download_package_checksum) and \
889 os.path.isdir(etpConst['dumpstoragedir']):
890
891 if os.path.isfile(self.old_download_package_checksum):
892 os.remove(self.old_download_package_checksum)
893 shutil.copy2(self.download_package_checksum,
894 self.old_download_package_checksum)
895 self.Entropy.setup_default_file_perms(
896 self.old_download_package_checksum)
897
898
899 status = self.__unpack_advisories()
900 if status != 0:
901 mytxt = "%s: %s." % (
902 bold(_("Security Advisories")),
903 darkred(_("digest verification failed, try again later")),
904 )
905 self.Entropy.updateProgress(
906 mytxt,
907 importance = 2,
908 type = "error",
909 header = red(" ## ")
910 )
911 self.Entropy.resources_remove_lock()
912 return 6
913
914 mytxt = "%s: %s %s" % (
915 bold(_("Security Advisories")),
916 blue(_("installing")),
917 red("..."),
918 )
919 self.Entropy.updateProgress(
920 mytxt,
921 importance = 1,
922 type = "info",
923 header = red(" # ")
924 )
925
926
927 self.__clear_previous_advisories()
928
929 self.__put_advisories_in_place()
930
931 self.__cleanup_garbage()
932 return 0
933