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 SecurityInterface 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'], 0775)
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.F_OK | os.R_OK):
120 f_down = open(self.old_download_package_checksum)
121 try:
122 self.previous_checksum = f_down.readline().strip().split()[0]
123 except (IndexError, OSError, IOError,):
124 pass
125 f_down.close()
126
128 """
129 Prepare GLSAs unpack directory and its permissions.
130 """
131 if os.path.isfile(self.unpackdir) or os.path.islink(self.unpackdir):
132 os.remove(self.unpackdir)
133
134 if os.path.isdir(self.unpackdir):
135 shutil.rmtree(self.unpackdir, True)
136 try:
137 os.rmdir(self.unpackdir)
138 except OSError:
139 pass
140
141 os.makedirs(self.unpackdir, 0775)
142 const_setup_perms(self.unpackdir, etpConst['entropygid'])
143
145 """
146 Download GLSA compressed package from a trusted source.
147 """
148 return self.__generic_download(self.security_url, self.download_package)
149
151 """
152 Download GLSA compressed package checksum (md5) from a trusted source.
153 """
154 return self.__generic_download(self.security_url_checksum,
155 self.download_package_checksum, show_speed = False)
156
158 """
159 Generic, secure, URL download method.
160
161 @param url: download URL
162 @type url: string
163 @param save_to: path to save file
164 @type save_to: string
165 @keyword show_speed: if True, download speed will be shown
166 @type show_speed: bool
167 @return: download status (True if download succeeded)
168 @rtype: bool
169 """
170 fetcher = self.Entropy.urlFetcher(url, save_to, resume = False,
171 show_speed = show_speed)
172 fetcher.progress = self.Entropy.progress
173 rc_fetch = fetcher.download()
174 del fetcher
175 if rc_fetch in ("-1", "-2", "-3", "-4"):
176 return False
177
178 self.Entropy.setup_default_file_perms(save_to)
179 return True
180
182 """
183 Verify downloaded GLSA checksum against downloaded GLSA package.
184 """
185
186 if not os.path.isfile(self.download_package_checksum) or \
187 not os.access(self.download_package_checksum, os.R_OK):
188 return 1
189
190 f_down = open(self.download_package_checksum)
191 read_err = False
192 try:
193 checksum = f_down.readline().strip().split()[0]
194 except (OSError, IOError, IndexError,):
195 read_err = True
196
197 f_down.close()
198 if read_err:
199 return 2
200
201 self.advisories_changed = True
202 if checksum == self.previous_checksum:
203 self.advisories_changed = False
204
205 md5res = self.entropyTools.compare_md5(self.download_package, checksum)
206 if not md5res:
207 return 3
208 return 0
209
211 """
212 Unpack downloaded GLSA package containing GLSA advisories.
213 """
214 rc_unpack = self.entropyTools.uncompress_tar_bz2(
215 self.download_package,
216 self.unpacked_package,
217 catchEmpty = True
218 )
219 const_setup_perms(self.unpacked_package, etpConst['entropygid'])
220 return rc_unpack
221
223 """
224 Remove previously installed GLSA advisories.
225 """
226 if os.listdir(etpConst['securitydir']):
227 shutil.rmtree(etpConst['securitydir'], True)
228 if not os.path.isdir(etpConst['securitydir']):
229 os.makedirs(etpConst['securitydir'], 0775)
230 const_setup_perms(self.unpackdir, etpConst['entropygid'])
231
233 """
234 Place unpacked advisories in place (into etpConst['securitydir']).
235 """
236 for advfile in os.listdir(self.unpacked_package):
237 from_file = os.path.join(self.unpacked_package, advfile)
238 to_file = os.path.join(etpConst['securitydir'], advfile)
239 try:
240 os.rename(from_file, to_file)
241 except OSError:
242 shutil.move(from_file, to_file)
243
245 """
246 Remove GLSA unpack directory.
247 """
248 shutil.rmtree(self.unpackdir, True)
249
250 - def clear(self, xcache = False):
251 """
252 Clear SecurityInterface cache (RAM and on-disk).
253
254 @keyword xcache: also remove Entropy on-disk cache if True
255 @type xcache: bool
256 """
257 self.adv_metadata = None
258 if xcache:
259 self.Entropy.clear_dump_cache(etpCache['advisories'])
260
262 """
263 Return cached advisories information metadata. It first tries to load
264 them from RAM and, in case of failure, it tries to gather the info
265 from disk, using EntropyCacher.
266 """
267 if self.adv_metadata != None:
268 return self.adv_metadata
269
270 if self.Entropy.xcache:
271 dir_checksum = self.entropyTools.md5sum_directory(
272 etpConst['securitydir'])
273 c_hash = "%s%s" % (
274 etpCache['advisories'], hash("%s|%s|%s" % (
275 hash(self.SystemSettings['repositories']['branch']),
276 hash(dir_checksum),
277 hash(etpConst['systemroot']),
278 )),
279 )
280 adv_metadata = self.__cacher.pop(c_hash)
281 if adv_metadata != None:
282 self.adv_metadata = adv_metadata.copy()
283 return self.adv_metadata
284
286 """
287 Set advisories information metadata cache.
288
289 @param adv_metadata: advisories metadata to store
290 @type adv_metadata: dict
291 """
292 if self.Entropy.xcache:
293 dir_checksum = self.entropyTools.md5sum_directory(
294 etpConst['securitydir'])
295 c_hash = "%s%s" % (
296 etpCache['advisories'], hash("%s|%s|%s" % (
297 hash(self.SystemSettings['repositories']['branch']),
298 hash(dir_checksum),
299 hash(etpConst['systemroot']),
300 )),
301 )
302 self.__cacher.push(c_hash, adv_metadata)
303
305 """
306 Return a list of advisory files. Internal method.
307 """
308 if not self.check_advisories_availability():
309 return []
310 xmls = os.listdir(etpConst['securitydir'])
311 xmls = sorted([x for x in xmls if x.endswith(".xml") and \
312 x.startswith("glsa-")])
313 return xmls
314
374
376 """
377 This function filters advisories metadata dict removing non-applicable
378 ones.
379
380 @param adv_metadata: security advisories metadata dict
381 @type adv_metadata: dict
382 @return: filtered security advisories metadata
383 @rtype: dict
384 """
385 keys = adv_metadata.keys()
386 for key in keys:
387 valid = True
388 if adv_metadata[key]['affected']:
389 affected = adv_metadata[key]['affected']
390 affected_keys = affected.keys()
391 valid = False
392 skipping_keys = set()
393 for a_key in affected_keys:
394 match = self.Entropy.atom_match(a_key)
395 if match[0] != -1:
396
397 valid = True
398 else:
399 skipping_keys.add(a_key)
400 if not valid:
401 del adv_metadata[key]
402 for a_key in skipping_keys:
403 try:
404 del adv_metadata[key]['affected'][a_key]
405 except KeyError:
406 continue
407 try:
408 if not adv_metadata[key]['affected']:
409 del adv_metadata[key]
410 except KeyError:
411 continue
412
413 return adv_metadata
414
416 """
417 Determine whether the system is affected by vulnerabilities listed
418 in the provided security advisory identifier.
419
420 @param adv_key: security advisories identifier
421 @type adv_key: string
422 @keyword adv_data: use the provided security advisories instead of
423 the stored one.
424 @type adv_data: dict
425 @return: True, if system is affected by vulnerabilities listed in the
426 provided security advisory.
427 @rtype: bool
428 """
429 if not adv_data:
430 adv_data = self.get_advisories_metadata()
431 if adv_key not in adv_data:
432 return False
433 mydata = adv_data[adv_key].copy()
434 del adv_data
435
436 if not mydata['affected']:
437 return False
438
439 for key in mydata['affected']:
440
441 vul_atoms = mydata['affected'][key][0]['vul_atoms']
442 unaff_atoms = mydata['affected'][key][0]['unaff_atoms']
443 unaffected_atoms = set()
444 if not vul_atoms:
445 return False
446 for atom in unaff_atoms:
447 matches = self.Entropy.clientDbconn.atomMatch(atom,
448 multiMatch = True)
449 for idpackage in matches[0]:
450 unaffected_atoms.add((idpackage, 0))
451
452 for atom in vul_atoms:
453 match = self.Entropy.clientDbconn.atomMatch(atom)
454 if (match[0] != -1) and (match not in unaffected_atoms):
455 self.affected_atoms.add(atom)
456 return True
457 return False
458
460 """
461 Return advisories metadata for installed packages containing
462 vulnerabilities.
463
464 @return: advisories metadata for vulnerable packages.
465 @rtype: dict
466 """
467 return self.__get_affection()
468
470 """
471 Return advisories metadata for installed packages not affected
472 by any vulnerability.
473
474 @return: advisories metadata for NON-vulnerable packages.
475 @rtype: dict
476 """
477 return self.__get_affection(affected = False)
478
480 """
481 If not affected: not affected packages will be returned.
482 If affected: affected packages will be returned.
483 """
484 adv_data = self.get_advisories_metadata()
485 adv_data_keys = adv_data.keys()
486 valid_keys = set()
487 for adv in adv_data_keys:
488 is_affected = self.is_affected(adv, adv_data)
489 if affected == is_affected:
490 valid_keys.add(adv)
491
492 for key in adv_data_keys:
493 if key not in valid_keys:
494 try:
495 del adv_data[key]
496 except KeyError:
497 pass
498
499 for adv in adv_data:
500 for key in adv_data[adv]['affected'].keys():
501 atoms = adv_data[adv]['affected'][key][0]['vul_atoms']
502 applicable = True
503 for atom in atoms:
504 if atom in self.affected_atoms:
505 applicable = False
506 break
507 if applicable == affected:
508 del adv_data[adv]['affected'][key]
509 return adv_data
510
512 """
513 Return a list of package atoms affected by vulnerabilities.
514
515 @return: list (set) of package atoms affected by vulnerabilities
516 @rtype: set
517 """
518 adv_data = self.get_advisories_metadata()
519 adv_data_keys = adv_data.keys()
520 del adv_data
521 self.affected_atoms.clear()
522 for key in adv_data_keys:
523 self.is_affected(key)
524 return self.affected_atoms
525
654
656 """
657 creates from the information in the I{versionNode} a
658 version string (format <op><version>).
659
660 @param vnode: a <vulnerable> or <unaffected> Node that
661 contains the version information for this atom
662 @type vnode: xml.dom.Node
663 @return: the version string
664 @rtype: string
665 """
666 return self.op_mappings[vnode.getAttribute("range")] + \
667 vnode.firstChild.data.strip()
668
670 """
671 creates from the given package name and information in the
672 I{versionNode} a (syntactical) valid portage atom.
673
674 @param pkgname: the name of the package for this atom
675 @type pkgname: string
676 @param vnode: a <vulnerable> or <unaffected> Node that
677 contains the version information for this atom
678 @type vnode: xml.dom.Node
679 @return: the portage atom
680 @rtype: string
681 """
682 return str(self.op_mappings[vnode.getAttribute("range")] + pkgname + \
683 "-" + vnode.firstChild.data.strip())
684
686 """
687 Return whether security advisories are available.
688
689 @return: availability
690 @rtype: bool
691 """
692 if not os.path.lexists(etpConst['securitydir']):
693 return False
694 if not os.path.isdir(etpConst['securitydir']):
695 return False
696 else:
697 return True
698 return False
699
701 """
702 This is the service method for remotely fetch advisories metadata.
703
704 @keyword do_cache: generates advisories cache
705 @type do_cache: bool
706 @return: execution status (0 means all file)
707 @rtype: int
708 """
709 mytxt = "%s: %s" % (
710 bold(_("Security Advisories")),
711 blue(_("testing service connection")),
712 )
713 self.Entropy.updateProgress(
714 mytxt,
715 importance = 2,
716 type = "info",
717 header = red(" @@ "),
718 footer = red(" ...")
719 )
720
721 mytxt = "%s: %s %s" % (
722 bold(_("Security Advisories")),
723 blue(_("getting latest GLSAs")),
724 red("..."),
725 )
726 self.Entropy.updateProgress(
727 mytxt,
728 importance = 2,
729 type = "info",
730 header = red(" @@ ")
731 )
732
733 gave_up = self.Entropy.lock_check(
734 self.Entropy.resources_check_lock)
735 if gave_up:
736 return 7
737
738 locked = self.Entropy.application_lock_check()
739 if locked:
740 return 4
741
742
743 acquired = self.Entropy.resources_create_lock()
744 if not acquired:
745 return 4
746 try:
747 rc_lock = self.__run_fetch()
748 except:
749 self.Entropy.resources_remove_lock()
750 raise
751 if rc_lock != 0:
752 return rc_lock
753
754 self.Entropy.resources_remove_lock()
755
756 if self.advisories_changed:
757 advtext = "%s: %s" % (
758 bold(_("Security Advisories")),
759 darkgreen(_("updated successfully")),
760 )
761 else:
762 advtext = "%s: %s" % (
763 bold(_("Security Advisories")),
764 darkgreen(_("already up to date")),
765 )
766
767 if do_cache and self.Entropy.xcache:
768 self.get_advisories_metadata()
769 self.Entropy.updateProgress(
770 advtext,
771 importance = 2,
772 type = "info",
773 header = red(" @@ ")
774 )
775
776 return 0
777
779
780 self.__prepare_unpack()
781
782
783 status = self.__download_glsa_package()
784 self.lastfetch = status
785 if not status:
786 mytxt = "%s: %s." % (
787 bold(_("Security Advisories")),
788 darkred(_("unable to download the package, sorry")),
789 )
790 self.Entropy.updateProgress(
791 mytxt,
792 importance = 2,
793 type = "error",
794 header = red(" ## ")
795 )
796 self.Entropy.resources_remove_lock()
797 return 1
798
799 mytxt = "%s: %s %s" % (
800 bold(_("Security Advisories")),
801 blue(_("Verifying checksum")),
802 red("..."),
803 )
804 self.Entropy.updateProgress(
805 mytxt,
806 importance = 1,
807 type = "info",
808 header = red(" # "),
809 back = True
810 )
811
812
813 status = self.__download_glsa_package_cksum()
814 if not status:
815 mytxt = "%s: %s." % (
816 bold(_("Security Advisories")),
817 darkred(_("cannot download the checksum, sorry")),
818 )
819 self.Entropy.updateProgress(
820 mytxt,
821 importance = 2,
822 type = "error",
823 header = red(" ## ")
824 )
825 self.Entropy.resources_remove_lock()
826 return 2
827
828
829 status = self.__verify_checksum()
830
831 if status == 1:
832 mytxt = "%s: %s." % (
833 bold(_("Security Advisories")),
834 darkred(_("cannot open packages, sorry")),
835 )
836 self.Entropy.updateProgress(
837 mytxt,
838 importance = 2,
839 type = "error",
840 header = red(" ## ")
841 )
842 self.Entropy.resources_remove_lock()
843 return 3
844 elif status == 2:
845 mytxt = "%s: %s." % (
846 bold(_("Security Advisories")),
847 darkred(_("cannot read the checksum, sorry")),
848 )
849 self.Entropy.updateProgress(
850 mytxt,
851 importance = 2,
852 type = "error",
853 header = red(" ## ")
854 )
855 self.Entropy.resources_remove_lock()
856 return 4
857 elif status == 3:
858 mytxt = "%s: %s." % (
859 bold(_("Security Advisories")),
860 darkred(_("digest verification failed, sorry")),
861 )
862 self.Entropy.updateProgress(
863 mytxt,
864 importance = 2,
865 type = "error",
866 header = red(" ## ")
867 )
868 self.Entropy.resources_remove_lock()
869 return 5
870 elif status == 0:
871 mytxt = "%s: %s." % (
872 bold(_("Security Advisories")),
873 darkgreen(_("verification Successful")),
874 )
875 self.Entropy.updateProgress(
876 mytxt,
877 importance = 1,
878 type = "info",
879 header = red(" # ")
880 )
881 else:
882 mytxt = _("Return status not valid")
883 raise InvalidData("InvalidData: %s." % (mytxt,))
884
885
886 if os.path.isfile(self.download_package_checksum) and \
887 os.path.isdir(etpConst['dumpstoragedir']):
888
889 if os.path.isfile(self.old_download_package_checksum):
890 os.remove(self.old_download_package_checksum)
891 shutil.copy2(self.download_package_checksum,
892 self.old_download_package_checksum)
893 self.Entropy.setup_default_file_perms(
894 self.old_download_package_checksum)
895
896
897 status = self.__unpack_advisories()
898 if status != 0:
899 mytxt = "%s: %s." % (
900 bold(_("Security Advisories")),
901 darkred(_("digest verification failed, try again later")),
902 )
903 self.Entropy.updateProgress(
904 mytxt,
905 importance = 2,
906 type = "error",
907 header = red(" ## ")
908 )
909 self.Entropy.resources_remove_lock()
910 return 6
911
912 mytxt = "%s: %s %s" % (
913 bold(_("Security Advisories")),
914 blue(_("installing")),
915 red("..."),
916 )
917 self.Entropy.updateProgress(
918 mytxt,
919 importance = 1,
920 type = "info",
921 header = red(" # ")
922 )
923
924
925 self.__clear_previous_advisories()
926
927 self.__put_advisories_in_place()
928
929 self.__cleanup_garbage()
930 return 0
931