1+ from datetime import datetime , timedelta , timezone
12from typing import Iterator
23
34from dissect .target .exceptions import UnsupportedPluginError
1314 ("string" , "hash" ),
1415 ("string" , "algorithm" ),
1516 ("string" , "crypt_param" ),
16- ("string " , "last_change" ),
17- ("varint " , "min_age" ),
18- ("varint " , "max_age" ),
17+ ("datetime " , "last_change" ),
18+ ("datetime " , "min_age" ),
19+ ("datetime " , "max_age" ),
1920 ("varint" , "warning_period" ),
20- ("string " , "inactivity_period" ),
21- ("string " , "expiration_date" ),
21+ ("varint " , "inactivity_period" ),
22+ ("datetime " , "expiration_date" ),
2223 ("string" , "unused_field" ),
2324 ],
2425)
@@ -39,6 +40,7 @@ def passwords(self) -> Iterator[UnixShadowRecord]:
3940
4041 Resources:
4142 - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
43+ - https://linux.die.net/man/5/shadow
4244 """
4345
4446 seen_hashes = set ()
@@ -64,19 +66,29 @@ def passwords(self) -> Iterator[UnixShadowRecord]:
6466
6567 seen_hashes .add (current_hash )
6668
69+ # improve readability
70+ last_change = shent .get (2 )
71+ min_age = shent .get (3 )
72+ max_age = shent .get (4 )
73+ expiration_date = shent .get (7 )
74+
6775 yield UnixShadowRecord (
6876 name = shent .get (0 ),
6977 crypt = shent .get (1 ),
7078 algorithm = crypt .get ("algo" ),
7179 crypt_param = crypt .get ("param" ),
7280 salt = crypt .get ("salt" ),
7381 hash = crypt .get ("hash" ),
74- last_change = shent .get (2 ),
75- min_age = shent .get (3 ),
76- max_age = shent .get (4 ),
77- warning_period = shent .get (5 ),
78- inactivity_period = shent .get (6 ),
79- expiration_date = shent .get (7 ),
82+ last_change = epoch_days_to_datetime (int (last_change )) if last_change else None ,
83+ min_age = epoch_days_to_datetime (int (last_change ) + int (min_age ))
84+ if last_change and isinstance (min_age , int ) and min_age > 0
85+ else None ,
86+ max_age = epoch_days_to_datetime (int (last_change ) + int (max_age ))
87+ if last_change and max_age
88+ else None ,
89+ warning_period = shent .get (5 ) if shent .get (5 ) else None ,
90+ inactivity_period = shent .get (6 ) if shent .get (6 ) else None ,
91+ expiration_date = epoch_days_to_datetime (int (expiration_date )) if expiration_date else None ,
8092 unused_field = shent .get (8 ),
8193 _target = self .target ,
8294 )
@@ -128,3 +140,11 @@ def extract_crypt_details(shent: dict) -> dict:
128140 crypt ["algo" ] = algos [crypt ["algo" ]]
129141
130142 return crypt
143+
144+
145+ def epoch_days_to_datetime (days : int ) -> datetime :
146+ """Convert a number representing the days since 1 January 1970 to a datetime object."""
147+ if not isinstance (days , int ):
148+ raise ValueError ("days argument should be an integer" )
149+
150+ return datetime (1970 , 1 , 1 , 0 , 0 , tzinfo = timezone .utc ) + timedelta (days )
0 commit comments