diff --git a/main.py b/main.py index 5d6699f..7004567 100755 --- a/main.py +++ b/main.py @@ -7,13 +7,23 @@ from pathlib import Path def main(): parser = argparse.ArgumentParser( - description="Convert a passwd file to a directory of systemd JSON user records.", + description="Convert a passwd/group/shadow file to a directory of systemd JSON user records.", ) parser.add_argument( "passwd_file", type=Path, help="Path to the passwd file.", ) + parser.add_argument( + "group_file", + type=Path, + help="Path to the group file.", + ) + parser.add_argument( + "shadow_file", + type=Path, + help="Path to the shadow file.", + ) parser.add_argument( "output_dir", type=Path, @@ -22,10 +32,18 @@ def main(): args = parser.parse_args() parsed_users = parse_passwd_file(args.passwd_file) + parsed_groups = parse_group_file(args.group_file) + parsed_shadow = parse_shadow_file(args.shadow_file) for user in parsed_users: print(json.dumps(user, indent=2)) + for group in parsed_groups: + print(json.dumps(group, indent=2)) + + for shadow_entry in parsed_shadow: + print(json.dumps(shadow_entry, indent=2)) + def parse_passwd_file(passwd_file: Path) -> list[dict]: users = [] @@ -41,7 +59,7 @@ def parse_passwd_file(passwd_file: Path) -> list[dict]: "name": parts[0], "uid": int(parts[2]), "gid": int(parts[3]), - "gecos": parse_gecos(parts[4]), + "gecos": parse_passwd_gecos(parts[4]), "home": parts[5], "shell": parts[6], } @@ -49,7 +67,7 @@ def parse_passwd_file(passwd_file: Path) -> list[dict]: return users -def parse_gecos(gecos: str) -> dict: +def parse_passwd_gecos(gecos: str) -> dict: parts = gecos.split(",") return { "full_name": parts[0] if len(parts) > 0 else "", @@ -60,5 +78,49 @@ def parse_gecos(gecos: str) -> dict: } +def parse_group_file(group_file: Path) -> list[dict]: + groups = [] + with group_file.open("r") as f: + for line in f: + if line.startswith("#") or not line.strip(): + continue + parts = line.strip().split(":") + if len(parts) < 4: + print(f"Warning: Skipping malformed line: {line.strip()}") + continue + group = { + "name": parts[0], + "gid": int(parts[2]), + "members": parts[3].split(",") if parts[3] else [], + } + groups.append(group) + return groups + + +def parse_shadow_file(shadow_file: Path) -> list[dict]: + shadow_entries = [] + with shadow_file.open("r") as f: + for line in f: + if line.startswith("#") or not line.strip(): + continue + parts = line.strip().split(":") + if len(parts) < 9: + print(f"Warning: Skipping malformed line: {line.strip()}") + continue + shadow_entry = { + "name": parts[0], + "password": parts[1], + "last_change": int(parts[2]) if parts[2].isdigit() else None, + "min_days": int(parts[3]) if parts[3].isdigit() else None, + "max_days": int(parts[4]) if parts[4].isdigit() else None, + "warn_days": int(parts[5]) if parts[5].isdigit() else None, + "inactive_days": int(parts[6]) if parts[6].isdigit() else None, + "expire_date": int(parts[7]) if parts[7].isdigit() else None, + "reserved": parts[8], + } + shadow_entries.append(shadow_entry) + return shadow_entries + + if __name__ == "__main__": main() diff --git a/test_assets/test.group b/test_assets/test.group new file mode 100644 index 0000000..c8b3962 --- /dev/null +++ b/test_assets/test.group @@ -0,0 +1,28 @@ +root:x:0: +daemon:x:1: +bin:x:2: +sys:x:3: +games:x:60: +man:x:12: +lp:x:7: +mail:x:8: +news:x:9: +uucp:x:10: +proxy:x:13: +www-data:x:33: +backup:x:34: +list:x:38: +irc:x:39: +gnats:x:41: +nobody:x:65534: +systemd-timesync:x:101: +systemd-network:x:103:user1 +systemd-resolve:x:104:user1,user2 +messagebus:x:105: +avahi:x:109: +saned:x:112: +xrdp:x:114: + +user1:x:1000: +user2:x:1001: +user3:x:1002: diff --git a/test.passwd b/test_assets/test.passwd similarity index 100% rename from test.passwd rename to test_assets/test.passwd diff --git a/test_assets/test.shadow b/test_assets/test.shadow new file mode 100644 index 0000000..31c9e36 --- /dev/null +++ b/test_assets/test.shadow @@ -0,0 +1,30 @@ +root:!*:1:::::: +daemon:!*:1:::::: +bin:!*:1:::::: +sys:!*:1:::::: +sync:!*:1:::::: +games:!*:1:::::: +man:!*:1:::::: +lp:!*:1:::::: +mail:!*:1:::::: +news:!*:1:::::: +uucp:!*:1:::::: +proxy:!*:1:::::: +www-data:!*:1:::::: +backup:!*:1:::::: +list:!*:1:::::: +irc:!*:1:::::: +gnats:!*:1:::::: +nobody:!*:1:::::: +_apt:!*:1:::::: +systemd-timesync:!*:1:::::: +systemd-network:!*:1:::::: +systemd-resolve:!*:1:::::: +messagebus:!*:1:::::: +avahi:!*:1:::::: +saned:!*:1:::::: +xrdp:!*:1:::::: + +user1:$y$j9T$gfNWp8nhIYxJtL7yy7myb0$LAsxtu.24imcyO5CH0xOL/G6Ii5LlLWnAJ.XL8CPYv3:20000:::::: +user2:$y$j9T$90l8J./nwd7sRU/HOyVtg.$f/M1K5evMFp0K2NYIjN257YHMUlg0wG/Sst1LXy2g3C:20001:::::: +user3:$y$j9T$lekKJIXaaL/24EvKehybL1$MSS0OHfiCTgcdExEDqaEsGFaYOphApGVV8fXrs.U1C5:20002::::::