Skip to content

Commit 6c619b1

Browse files
committed
add tests and implement review feedback
1 parent 4260348 commit 6c619b1

File tree

2 files changed

+104
-12
lines changed

2 files changed

+104
-12
lines changed

dissect/target/plugins/os/unix/shadow.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from datetime import datetime, timedelta, timezone
24
from typing import Iterator
35

@@ -67,10 +69,38 @@ def passwords(self) -> Iterator[UnixShadowRecord]:
6769
seen_hashes.add(current_hash)
6870

6971
# 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)
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+
)
74104

75105
yield UnixShadowRecord(
76106
name=shent.get(0),
@@ -79,16 +109,12 @@ def passwords(self) -> Iterator[UnixShadowRecord]:
79109
crypt_param=crypt.get("param"),
80110
salt=crypt.get("salt"),
81111
hash=crypt.get("hash"),
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,
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,
89115
warning_period=shent.get(5) if shent.get(5) else None,
90116
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,
117+
expiration_date=epoch_days_to_datetime(expiration_date) if expiration_date else None,
92118
unused_field=shent.get(8),
93119
_target=self.target,
94120
)

tests/plugins/os/unix/test_shadow.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from datetime import datetime, timezone
22
from io import BytesIO
33
from pathlib import Path
4+
from textwrap import dedent
5+
6+
import pytest
47

58
from dissect.target.filesystem import VirtualFilesystem
69
from dissect.target.plugins.os.unix.shadow import ShadowPlugin
@@ -50,3 +53,66 @@ def test_unix_shadow_backup_file(target_unix_users: Target, fs_unix: VirtualFile
5053
assert results[0].name == "test"
5154
assert results[1].name == "other-user"
5255
assert results[0].hash == results[1].hash
56+
57+
58+
def test_unix_shadow_invalid_shent(
59+
caplog: pytest.LogCaptureFixture, target_unix_users: Target, fs_unix: VirtualFilesystem
60+
) -> None:
61+
"""test if we can parse invalid day values in shents."""
62+
63+
shadow_invalid = """
64+
no_last_change:$6$salt$hash1::0:99999:7::123456:
65+
no_max_age:$6$salt$hash2:18963:0::7:::
66+
only_last_change:$6$salt$hash3:18963::::::
67+
no_int_fields:$6$salt$hash4:string::::::
68+
daemon:*:18474:0:99999:7:::
69+
bin:*:18474:0:99999:7:::
70+
nobody:*:18474:0:99999:7:::
71+
regular:$6$salt$hash5:1337:0:99999:7::123456:
72+
"""
73+
fs_unix.map_file_fh("/etc/shadow", BytesIO(dedent(shadow_invalid).encode()))
74+
75+
results = list(target_unix_users.passwords())
76+
assert len(results) == 5
77+
78+
assert [r.name for r in results] == [
79+
"no_last_change",
80+
"no_max_age",
81+
"only_last_change",
82+
"no_int_fields",
83+
"regular",
84+
]
85+
86+
assert results[0].name == "no_last_change"
87+
assert results[0].last_change is None
88+
assert results[0].min_age is None
89+
assert results[0].max_age is None
90+
assert results[0].warning_period == 7
91+
assert results[0].inactivity_period is None
92+
assert results[0].expiration_date == datetime(2308, 1, 6, tzinfo=timezone.utc)
93+
94+
assert results[1].name == "no_max_age"
95+
assert results[1].last_change == datetime(2021, 12, 2, tzinfo=timezone.utc)
96+
assert results[1].max_age is None
97+
98+
assert results[2].name == "only_last_change"
99+
assert results[2].last_change == datetime(2021, 12, 2, tzinfo=timezone.utc)
100+
101+
assert results[3].name == "no_int_fields"
102+
assert results[3].last_change is None
103+
assert (
104+
"Unable to parse last_change shadow value in /etc/shadow: invalid literal for int() with base 10: 'string' ('string')"
105+
in caplog.text
106+
)
107+
108+
# make sure we parsed the last entry even though the other entries are 'broken'
109+
assert results[-1].name == "regular"
110+
assert results[-1].salt == "salt"
111+
assert results[-1].hash == "hash5"
112+
assert results[-1].algorithm == "sha512"
113+
assert results[-1].last_change == datetime(1973, 8, 30, tzinfo=timezone.utc)
114+
assert results[-1].min_age is None
115+
assert results[-1].max_age == datetime(2247, 6, 14, tzinfo=timezone.utc)
116+
assert results[-1].warning_period == 7
117+
assert results[-1].inactivity_period is None
118+
assert results[-1].expiration_date == datetime(2308, 1, 6, tzinfo=timezone.utc)

0 commit comments

Comments
 (0)