From e0ec2ce60a7ebcb7d1c413a0b37089501d3453c2 Mon Sep 17 00:00:00 2001 From: Marco D'Aleo Date: Sun, 21 Dec 2025 09:29:21 +0000 Subject: [PATCH 1/4] Add tab completion, update README, version bump 0.2.0 --- README.md | 10 +++++ chguard/cli.py | 118 ++++++++++++++++++++++++++++++++++++++++--------- pyproject.toml | 2 +- 3 files changed, 108 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 1384f22..d6746a0 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,16 @@ Snapshots are stored in a local SQLite database containing: Usernames and permission strings are resolved only for display. +### TAB completion +Add this to your `.bashrc` +``` +eval "$(register-python-argcomplete chguard)" +``` +And then +``` +source ~/.bashrc +``` + ## pre-commit This project uses [**pre-commit**](https://pre-commit.com/) to run automatic formatting and security checks before each commit (Black, Bandit, and various safety checks). diff --git a/chguard/cli.py b/chguard/cli.py index 9fee496..fa4e1f3 100644 --- a/chguard/cli.py +++ b/chguard/cli.py @@ -1,6 +1,8 @@ from __future__ import annotations import argparse +import argcomplete +import importlib.metadata import os import sys import stat @@ -27,6 +29,13 @@ from chguard.restore import plan_restore, apply_restore from chguard.util import normalize_root +def get_version(): + try: + return importlib.metadata.version("chguard") + except importlib.metadata.PackageNotFoundError: + return "unknown" + + def _uid_to_name(uid: int) -> str: """Return username for uid, or uid as string if unknown.""" try: @@ -83,6 +92,19 @@ def _is_root() -> bool: return os.geteuid() == 0 +def complete_state_names(prefix, parsed_args, **kwargs): + try: + conn = connect( + Path(parsed_args.db).expanduser().resolve() + if parsed_args.db + else None + ) + rows = conn.execute("SELECT name FROM states").fetchall() + return [name for (name,) in rows if name.startswith(prefix)] + except Exception: + return [] + + def main() -> None: """ Entry point for the CLI. @@ -100,45 +122,99 @@ def main() -> None: ) actions = parser.add_mutually_exclusive_group(required=True) - actions.add_argument("--save", metavar="PATH", help="Save state for PATH") + + parser.add_argument( + "--version", + action="version", + version=f"chguard {get_version()}", + ) + actions.add_argument( - "--restore", action="store_true", help="Restore a saved state" - ) + "--save", + metavar="PATH", + help="Save state for PATH", + ).completer = argcomplete.FilesCompleter() + actions.add_argument( - "--list", action="store_true", help="List saved states" + "--restore", + action="store_true", + help="Restore a saved state", ) + actions.add_argument( - "--delete", metavar="STATE", help="Delete a saved state" + "--list", + action="store_true", + help="List saved states", + ) + + actions.add_argument( + "--delete", + metavar="STATE", + help="Delete a saved state", + ).completer = complete_state_names + + # positional STATE + parser.add_argument( + "state", + nargs="?", + help="State name (required with --restore)", + ).completer = complete_state_names + + parser.add_argument( + "--name", + help="State name (required with --save)", ) parser.add_argument( - "state", nargs="?", help="State name (required with --restore)" - ) - parser.add_argument("--name", help="State name (required with --save)") - parser.add_argument( - "--overwrite", action="store_true", help="Overwrite existing state" + "--overwrite", + action="store_true", + help="Overwrite existing state", ) parser.add_argument( - "--permissions", action="store_true", help="Restore MODE only" - ) - parser.add_argument( - "--owner", action="store_true", help="Restore OWNER only" + "--permissions", + action="store_true", + help="Restore MODE only", ) parser.add_argument( - "--dry-run", action="store_true", help="Preview only; do not apply" - ) - parser.add_argument( - "--yes", action="store_true", help="Apply without confirmation" + "--owner", + action="store_true", + help="Restore OWNER only", ) - parser.add_argument("--root", metavar="PATH", help="Override restore root") parser.add_argument( - "--exclude", action="append", default=[], help="Exclude path prefix" + "--dry-run", + action="store_true", + help="Preview only; do not apply", ) - parser.add_argument("--db", metavar="PATH", help="Override database path") + parser.add_argument( + "--yes", + action="store_true", + help="Apply without confirmation", + ) + + parser.add_argument( + "--root", + metavar="PATH", + help="Override restore root", + ).completer = argcomplete.FilesCompleter() + + parser.add_argument( + "--exclude", + action="append", + default=[], + help="Exclude path prefix", + ).completer = argcomplete.FilesCompleter() + + parser.add_argument( + "--db", + metavar="PATH", + help="Override database path", + ).completer = argcomplete.FilesCompleter() + + argcomplete.autocomplete(parser) args = parser.parse_args() console = Console() diff --git a/pyproject.toml b/pyproject.toml index 67919db..b2ab67a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chguard" -version = "0.1.0" +version = "0.2.0" description = "Safety-first tool to snapshot and restore filesystem ownership and permissions." authors = ["Marco D'Aleo "] license = "GPL-3.0-or-later" From e8f63386bbcf6e755702b47c704aba944a6c7efb Mon Sep 17 00:00:00 2001 From: Marco D'Aleo Date: Sun, 21 Dec 2025 09:40:56 +0000 Subject: [PATCH 2/4] Add filelock ^3.20.1 in order to get away from CVE-2025-68146 --- poetry.lock | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index c1bbb17..cca26fd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -289,4 +289,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "49f77d614e46109e49e997fa270cb7093d6f7e7d258e370c4eddd4354c20437f" +content-hash = "aa4ded468b14fc02b90fdb2a0b1bd446195d02affc359c045b6bbb93858aa747" diff --git a/pyproject.toml b/pyproject.toml index b2ab67a..f1b0688 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ python = ">=3.10,<4.0" rich = ">=12" argcomplete = ">=2" platformdirs = "^4.5.1" +filelock = "^3.20.1" [tool.poetry.scripts] chguard = "chguard.cli:main" From 5353310e15f87c7827a60647a160ffefe6dd1cae Mon Sep 17 00:00:00 2001 From: Marco D'Aleo Date: Sun, 21 Dec 2025 09:54:08 +0000 Subject: [PATCH 3/4] Edit workflow to run pip-audit against a poetry export file --- .gitea/workflows/lint-and-security.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/lint-and-security.yml b/.gitea/workflows/lint-and-security.yml index b74bef1..51377d9 100644 --- a/.gitea/workflows/lint-and-security.yml +++ b/.gitea/workflows/lint-and-security.yml @@ -22,8 +22,11 @@ jobs: - name: Run pre-commit hooks run: pre-commit run --all-files --color always - - name: Install pip-audit - run: pip install pip-audit + - name: Audit dependencies + run: | + pip install poetry pip-audit + poetry export -f requirements.txt --without-hashes \ + | pip-audit -r /dev/stdin - name: Run pip-audit run: pip-audit From 96970b6963f293386efc35b1cc158bad72d515bc Mon Sep 17 00:00:00 2001 From: Marco D'Aleo Date: Sun, 21 Dec 2025 10:01:05 +0000 Subject: [PATCH 4/4] Rework lint-and-security workflow to add Poetry and the export plugin to work with pip-audit --- .gitea/workflows/lint-and-security.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/lint-and-security.yml b/.gitea/workflows/lint-and-security.yml index 51377d9..fa95502 100644 --- a/.gitea/workflows/lint-and-security.yml +++ b/.gitea/workflows/lint-and-security.yml @@ -22,11 +22,15 @@ jobs: - name: Run pre-commit hooks run: pre-commit run --all-files --color always - - name: Audit dependencies + - name: Install Poetry + run: | + pip install poetry + poetry self add poetry-plugin-export + + - name: Install pip-audit + run: pip install pip-audit + + - name: Audit dependencies (Poetry lockfile) run: | - pip install poetry pip-audit poetry export -f requirements.txt --without-hashes \ | pip-audit -r /dev/stdin - - - name: Run pip-audit - run: pip-audit