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
« 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#
20import os
21import sys
22from argparse import ArgumentParser, RawDescriptionHelpFormatter
24from pass_audit import __version__
25from pass_audit.audit import PassAudit
26from pass_audit.msg import Msg
27from pass_audit.passwordstore import PasswordStore, PasswordStoreError
30class ArgParser(ArgumentParser):
31 """Manages argument parsing and adds some defaults."""
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."
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')
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.""")
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.')
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)
73 if not parser.passwordstore:
74 msg.die("not running inside password-store.")
76 if arg.paths == '':
77 msg.message("Auditing whole store - this may take some time")
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.')
85 paths = store.list(arg.paths, arg.name)
86 if not paths:
87 msg.die(f"{arg.paths} is not in the password store.")
89 return msg, store, paths
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
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}"
115def main():
116 """pass-audit main function."""
117 msg, store, paths = setup()
119 data = pass_read(msg, store, paths)
120 audit = PassAudit(data, msg.verb)
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).")
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)}")
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)}")
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'.")
153if __name__ == "__main__":
154 sys.argv.pop(0)
155 main()