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 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 self.Entropy.resources_remove_lock()
741 return 4
742
743
744 self.Entropy.resources_create_lock()
745 try:
746 rc_lock = self.__run_fetch()
747 except:
748 self.Entropy.resources_remove_lock()
749 raise
750 if rc_lock != 0:
751 return rc_lock
752
753 self.Entropy.resources_remove_lock()
754
755 if self.advisories_changed:
756 advtext = "%s: %s" % (
757 bold(_("Security Advisories")),
758 darkgreen(_("updated successfully")),
759 )
760 else:
761 advtext = "%s: %s" % (
762 bold(_("Security Advisories")),
763 darkgreen(_("already up to date")),
764 )
765
766 if do_cache and self.Entropy.xcache:
767 self.get_advisories_metadata()
768 self.Entropy.updateProgress(
769 advtext,
770 importance = 2,
771 type = "info",
772 header = red(" @@ ")
773 )
774
775 return 0
776
778
779 self.__prepare_unpack()
780
781
782 status = self.__download_glsa_package()
783 self.lastfetch = status
784 if not status:
785 mytxt = "%s: %s." % (
786 bold(_("Security Advisories")),
787 darkred(_("unable to download the package, sorry")),
788 )
789 self.Entropy.updateProgress(
790 mytxt,
791 importance = 2,
792 type = "error",
793 header = red(" ## ")
794 )
795 self.Entropy.resources_remove_lock()
796 return 1
797
798 mytxt = "%s: %s %s" % (
799 bold(_("Security Advisories")),
800 blue(_("Verifying checksum")),
801 red("..."),
802 )
803 self.Entropy.updateProgress(
804 mytxt,
805 importance = 1,
806 type = "info",
807 header = red(" # "),
808 back = True
809 )
810
811
812 status = self.__download_glsa_package_cksum()
813 if not status:
814 mytxt = "%s: %s." % (
815 bold(_("Security Advisories")),
816 darkred(_("cannot download the checksum, sorry")),
817 )
818 self.Entropy.updateProgress(
819 mytxt,
820 importance = 2,
821 type = "error",
822 header = red(" ## ")
823 )
824 self.Entropy.resources_remove_lock()
825 return 2
826
827
828 status = self.__verify_checksum()
829
830 if status == 1:
831 mytxt = "%s: %s." % (
832 bold(_("Security Advisories")),
833 darkred(_("cannot open packages, sorry")),
834 )
835 self.Entropy.updateProgress(
836 mytxt,
837 importance = 2,
838 type = "error",
839 header = red(" ## ")
840 )
841 self.Entropy.resources_remove_lock()
842 return 3
843 elif status == 2:
844 mytxt = "%s: %s." % (
845 bold(_("Security Advisories")),
846 darkred(_("cannot read the checksum, sorry")),
847 )
848 self.Entropy.updateProgress(
849 mytxt,
850 importance = 2,
851 type = "error",
852 header = red(" ## ")
853 )
854 self.Entropy.resources_remove_lock()
855 return 4
856 elif status == 3:
857 mytxt = "%s: %s." % (
858 bold(_("Security Advisories")),
859 darkred(_("digest verification failed, sorry")),
860 )
861 self.Entropy.updateProgress(
862 mytxt,
863 importance = 2,
864 type = "error",
865 header = red(" ## ")
866 )
867 self.Entropy.resources_remove_lock()
868 return 5
869 elif status == 0:
870 mytxt = "%s: %s." % (
871 bold(_("Security Advisories")),
872 darkgreen(_("verification Successful")),
873 )
874 self.Entropy.updateProgress(
875 mytxt,
876 importance = 1,
877 type = "info",
878 header = red(" # ")
879 )
880 else:
881 mytxt = _("Return status not valid")
882 raise InvalidData("InvalidData: %s." % (mytxt,))
883
884
885 if os.path.isfile(self.download_package_checksum) and \
886 os.path.isdir(etpConst['dumpstoragedir']):
887
888 if os.path.isfile(self.old_download_package_checksum):
889 os.remove(self.old_download_package_checksum)
890 shutil.copy2(self.download_package_checksum,
891 self.old_download_package_checksum)
892 self.Entropy.setup_default_file_perms(
893 self.old_download_package_checksum)
894
895
896 status = self.__unpack_advisories()
897 if status != 0:
898 mytxt = "%s: %s." % (
899 bold(_("Security Advisories")),
900 darkred(_("digest verification failed, try again later")),
901 )
902 self.Entropy.updateProgress(
903 mytxt,
904 importance = 2,
905 type = "error",
906 header = red(" ## ")
907 )
908 self.Entropy.resources_remove_lock()
909 return 6
910
911 mytxt = "%s: %s %s" % (
912 bold(_("Security Advisories")),
913 blue(_("installing")),
914 red("..."),
915 )
916 self.Entropy.updateProgress(
917 mytxt,
918 importance = 1,
919 type = "info",
920 header = red(" # ")
921 )
922
923
924 self.__clear_previous_advisories()
925
926 self.__put_advisories_in_place()
927
928 self.__cleanup_garbage()
929 return 0
930