1+ from __future__ import annotations
2+
3+ from datetime import datetime , timedelta , timezone
14from typing import Iterator
25
36from dissect .target .exceptions import UnsupportedPluginError
1316 ("string" , "hash" ),
1417 ("string" , "algorithm" ),
1518 ("string" , "crypt_param" ),
16- ("string " , "last_change" ),
17- ("varint " , "min_age" ),
18- ("varint " , "max_age" ),
19+ ("datetime " , "last_change" ),
20+ ("datetime " , "min_age" ),
21+ ("datetime " , "max_age" ),
1922 ("varint" , "warning_period" ),
20- ("string " , "inactivity_period" ),
21- ("string " , "expiration_date" ),
23+ ("varint " , "inactivity_period" ),
24+ ("datetime " , "expiration_date" ),
2225 ("string" , "unused_field" ),
2326 ],
2427)
@@ -39,6 +42,7 @@ def passwords(self) -> Iterator[UnixShadowRecord]:
3942
4043 Resources:
4144 - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
45+ - https://linux.die.net/man/5/shadow
4246 """
4347
4448 seen_hashes = set ()
@@ -64,19 +68,53 @@ def passwords(self) -> Iterator[UnixShadowRecord]:
6468
6569 seen_hashes .add (current_hash )
6670
71+ # improve readability
72+ last_change = None
73+ min_age = None
74+ max_age = None
75+ expiration_date = None
76+
77+ try :
78+ last_change = int (shent .get (2 )) if shent .get (2 ) else None
79+ except ValueError as e :
80+ self .target .log .warning (
81+ "Unable to parse last_change shadow value in %s: %s ('%s')" , shadow_file , e , shent .get (2 )
82+ )
83+
84+ try :
85+ min_age = int (shent .get (3 )) if shent .get (3 ) else None
86+ except ValueError as e :
87+ self .target .log .warning (
88+ "Unable to parse last_change shadow value in %s: %s ('%s')" , shadow_file , e , shent .get (3 )
89+ )
90+
91+ try :
92+ max_age = int (shent .get (4 )) if shent .get (4 ) else None
93+ except ValueError as e :
94+ self .target .log .warning (
95+ "Unable to parse last_change shadow value in %s: %s ('%s')" , shadow_file , e , shent .get (4 )
96+ )
97+
98+ try :
99+ expiration_date = int (shent .get (7 )) if shent .get (7 ) else None
100+ except ValueError as e :
101+ self .target .log .warning (
102+ "Unable to parse last_change shadow value in %s: %s ('%s')" , shadow_file , e , shent .get (7 )
103+ )
104+
67105 yield UnixShadowRecord (
68106 name = shent .get (0 ),
69107 crypt = shent .get (1 ),
70108 algorithm = crypt .get ("algo" ),
71109 crypt_param = crypt .get ("param" ),
72110 salt = crypt .get ("salt" ),
73111 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 ) ,
112+ last_change = epoch_days_to_datetime ( last_change ) if last_change else None ,
113+ min_age = epoch_days_to_datetime ( last_change + min_age ) if last_change and min_age else None ,
114+ max_age = epoch_days_to_datetime ( last_change + max_age ) if last_change and max_age else None ,
115+ warning_period = shent .get (5 ) if shent . get ( 5 ) else None ,
116+ inactivity_period = shent .get (6 ) if shent . get ( 6 ) else None ,
117+ expiration_date = epoch_days_to_datetime ( expiration_date ) if expiration_date else None ,
80118 unused_field = shent .get (8 ),
81119 _target = self .target ,
82120 )
@@ -128,3 +166,11 @@ def extract_crypt_details(shent: dict) -> dict:
128166 crypt ["algo" ] = algos [crypt ["algo" ]]
129167
130168 return crypt
169+
170+
171+ def epoch_days_to_datetime (days : int ) -> datetime :
172+ """Convert a number representing the days since 1 January 1970 to a datetime object."""
173+ if not isinstance (days , int ):
174+ raise ValueError ("days argument should be an integer" )
175+
176+ return datetime (1970 , 1 , 1 , 0 , 0 , tzinfo = timezone .utc ) + timedelta (days )
0 commit comments