Coverage for pass_audit/__main__.py: 100%

75 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-16 11:35 +0000

1#!/usr/bin/env python3 

2# -*- coding: utf-8 -* 

3# pass audit - Password Store Extension (https://www.passwordstore.org/) 

4# Copyright (C) 2018-2022 Alexandre PUJOL <alexandre@pujol.io>. 

5# 

6# This program is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU General Public License as published by 

8# the Free Software Foundation, either version 3 of the License, or 

9# (at your option) any later version. 

10# 

11# This program is distributed in the hope that it will be useful, 

12# but WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

14# GNU General Public License for more details. 

15# 

16# You should have received a copy of the GNU General Public License 

17# along with this program. If not, see <http://www.gnu.org/licenses/>. 

18# 

19 

20import os 

21import sys 

22from argparse import ArgumentParser, RawDescriptionHelpFormatter 

23 

24from pass_audit import __version__ 

25from pass_audit.audit import PassAudit 

26from pass_audit.msg import Msg 

27from pass_audit.passwordstore import PasswordStore, PasswordStoreError 

28 

29 

30class ArgParser(ArgumentParser): 

31 """Manages argument parsing and adds some defaults.""" 

32 

33 def __init__(self): 

34 description = """ 

35 A pass extension for auditing your password repository. It supports safe 

36 breached password detection from haveibeenpwned.com using K-anonymity method, 

37 duplicated passwords, and password strength estimation using zxcvbn.""" 

38 epilog = "More information may be found in the pass-audit(1) man page." 

39 

40 super().__init__(prog='pass audit', 

41 description=description, 

42 formatter_class=RawDescriptionHelpFormatter, 

43 epilog=epilog) 

44 self.add_arguments() 

45 self.passwordstore = bool( 

46 os.environ.get('_PASSWORD_STORE_EXTENSION', '') == 'audit') 

47 

48 def add_arguments(self): 

49 """Set arguments.""" 

50 self.add_argument('paths', type=str, nargs='?', metavar='pass-names', 

51 default='', help="""Path(s) to audit in the password 

52 store, If empty audit the full store.""") 

53 

54 self.add_argument('-V', '--version', action='version', 

55 version='%(prog)s ' + __version__, 

56 help='Show the program version and exit.') 

57 self.add_argument('-n', '--name', type=str, default="*", 

58 help="""Check only passwords with this filename""") 

59 group = self.add_mutually_exclusive_group() 

60 group.add_argument('-v', '--verbose', action='count', default=0, 

61 help='Set verbosity level, ' 

62 'can be used more than once.') 

63 group.add_argument('-q', '--quiet', action='store_true', 

64 help='Be quiet.') 

65 

66 

67def setup(): 

68 """Read program arguments & sanity checks.""" 

69 parser = ArgParser() 

70 arg = parser.parse_args(sys.argv) 

71 msg = Msg(arg.verbose, arg.quiet) 

72 

73 if not parser.passwordstore: 

74 msg.die("not running inside password-store.") 

75 

76 if arg.paths == '': 

77 msg.message("Auditing whole store - this may take some time") 

78 

79 store = PasswordStore() 

80 if not store.exist(): 

81 msg.die("no password store to audit.") 

82 if not store.isvalid(): 

83 msg.die('invalid user ID, password access aborted.') 

84 

85 paths = store.list(arg.paths, arg.name) 

86 if not paths: 

87 msg.die(f"{arg.paths} is not in the password store.") 

88 

89 return msg, store, paths 

90 

91 

92def pass_read(msg, store, paths): 

93 """Read data from the password store.""" 

94 msg.verbose("Reading the password store") 

95 data = {} 

96 for path in paths: 

97 try: 

98 msg.verbose(f"Reading {path}") 

99 data[path] = store.show(path) 

100 except PasswordStoreError as error: 

101 msg.warning( 

102 f"Impossible to read {path} from the password store: {error}") 

103 return data 

104 

105 

106def zxcvbn_parse(details): 

107 """Nicely print the results from zxcvbn.""" 

108 sequence = '' 

109 for seq in details.get('sequence', []): 

110 sequence += f"{seq['token']}({seq['pattern']}) " 

111 res = f"Score {details['score']} ({details['guesses']} guesses). " 

112 return res + f"This estimate is based on the sequence {sequence}" 

113 

114 

115def main(): 

116 """pass-audit main function.""" 

117 msg, store, paths = setup() 

118 

119 data = pass_read(msg, store, paths) 

120 audit = PassAudit(data, msg.verb) 

121 

122 msg.verbose("Checking for breached passwords") 

123 breached = audit.password() 

124 for path, payload, count in breached: 

125 msg.warning(f"Password breached: {payload} from {path} has" 

126 f" been breached {count} time(s).") 

127 

128 msg.verbose("Checking for weak passwords") 

129 try: 

130 weak = audit.zxcvbn() 

131 except ImportError as error: 

132 weak = [] 

133 msg.warning(f"python3-{error.name} not present, skipping check") 

134 for path, payload, details in weak: 

135 msg.warning(f"Weak password detected: {payload} from {path}" 

136 f" might be weak. {zxcvbn_parse(details)}") 

137 

138 msg.verbose("Checking for duplicated passwords") 

139 duplicated = audit.duplicates() 

140 for paths in duplicated: 

141 msg.warning(f"Duplicated passwords detected in {', '.join(paths)}") 

142 

143 if not breached and not weak and not duplicated: 

144 msg.success(f"None of the {len(data)} passwords tested are " 

145 "breached, duplicated or weak.") 

146 else: 

147 msg.error(f"{len(data)} passwords tested and {len(breached)} breached," 

148 f" {len(weak)} weak passwords found," 

149 f" {len(duplicated)} duplicated passwords found.") 

150 msg.message("You should update them with 'pass update'.") 

151 

152 

153if __name__ == "__main__": 

154 sys.argv.pop(0) 

155 main()