15 Commits
Author SHA1 Message Date
mdaleo404 c404ea6ea5 Merge branch 'renovate/rich-15.x-lockfile' 2026-10-10 08:25:05 +01:00
mdaleo404 80e6fee2df Merge branch 'renovate/rich-15.x-lockfile' of ssh://git.sysmd.uk:222/guardutils/chguard into renovate/rich-15.x-lockfile
Lint & Security / lint-and-security (pull_request) Successful in 1m5s
Lint & Security / tests (pull_request) Successful in 22s
2026-10-10 08:20:36 +01:00
mdaleo404 cfbfcb6f4d Merge pull request 'Update dependency pre-commit to v4' (#18) from renovate/pre-commit-4.x into main
Reviewed-on: #18
2026-10-10 07:20:01 +00:00
mdaleo404 dc8ce892f8 Merge branch 'renovate/pre-commit-4.x' of ssh://git.sysmd.uk:222/guardutils/chguard into renovate/pre-commit-4.x
Lint & Security / lint-and-security (pull_request) Successful in 1m3s
Lint & Security / tests (pull_request) Successful in 23s
2026-10-10 08:18:03 +01:00
kamabot 7ef733b36e Update dependency pre-commit to v4 2026-10-10 08:17:52 +01:00
mdaleo404 58247bf06f Merge pull request 'Update non-major dependencies' (#17) from renovate/non-major-dependencies into main
Reviewed-on: #17
2026-10-10 07:16:59 +00:00
mdaleo404 b833fe0deb Merge branch 'renovate/non-major-dependencies' of ssh://git.sysmd.uk:222/guardutils/chguard into renovate/non-major-dependencies
Lint & Security / lint-and-security (pull_request) Successful in 1m3s
Lint & Security / tests (pull_request) Successful in 24s
2026-10-10 08:14:48 +01:00
kamabot 626b9e91d8 Update non-major dependencies 2026-10-10 08:14:27 +01:00
mdaleo404 345ed69dc1 Bump version to 0.6.1 2026-10-10 08:08:43 +01:00
mdaleo404 a317582b72 Uniquify automatic wrapper snapshot names 2026-10-10 08:07:19 +01:00
kamabot e01c21446f Update non-major dependencies
renovate/stability-days Updates have met minimum release age requirement
renovate/artifacts Artifact file update failure
Lint & Security / lint-and-security (pull_request) Successful in 1m15s
Lint & Security / tests (pull_request) Successful in 23s
2026-10-10 03:02:53 +00:00
kamabot 0bade103d0 Update dependency rich to v15
renovate/stability-days Updates have met minimum release age requirement
Lint & Security / lint-and-security (pull_request) Successful in 1m4s
Lint & Security / tests (pull_request) Successful in 24s
2026-10-09 17:07:16 +00:00
kamabot 1df5a31a7c Update dependency pre-commit to v4
renovate/stability-days Updates have met minimum release age requirement
Lint & Security / lint-and-security (pull_request) Successful in 1m9s
Lint & Security / tests (pull_request) Successful in 23s
2026-10-09 17:07:07 +00:00
mdaleo404 3d3e9dae13 Update workflows and tests 2026-10-09 18:01:04 +01:00
mdaleo404 f6a04e3166 Update package installation for sysmd repository 2026-10-06 18:44:42 +01:00
8 changed files with 164 additions and 41 deletions

No files matched your search

+25
View File
@@ -77,3 +77,28 @@ jobs:
and and
( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 ) ( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 )
' pip-audit.json ' pip-audit.json
tests:
name: tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install Poetry
run: pip install "poetry>=2,<3"
- name: Install project and dependencies
run: poetry install --with dev
- name: Run tests
run: poetry run pytest
+25
View File
@@ -77,3 +77,28 @@ jobs:
and and
( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 ) ( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 )
' pip-audit.json ' pip-audit.json
tests:
name: tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install Poetry
run: pip install "poetry>=2,<3"
- name: Install project and dependencies
run: poetry install --with dev
- name: Run tests
run: poetry run pytest
+3 -2
View File
@@ -316,7 +316,7 @@ The wrapper snapshot flow is:
wrapper_cmd wrapper_cmd
-> _extract_paths_from_command() -> _extract_paths_from_command()
-> _common_snapshot_root() -> _common_snapshot_root()
-> create auto-YYYYMMDD-HHMMSS state -> create a unique auto-YYYYMMDD-HHMMSS[-N] state
-> _iter_entries_for_target() for each path -> _iter_entries_for_target() for each path
-> insert each relative path once -> insert each relative path once
-> run subprocess.run(wrapper_cmd) -> run subprocess.run(wrapper_cmd)
@@ -798,7 +798,8 @@ The list of `PlannedChange` objects produced before any mutation occurs.
The `chguard -- chmod|chown|chgrp ...` mode that saves an automatic pre-command snapshot before running the command. The `chguard -- chmod|chown|chgrp ...` mode that saves an automatic pre-command snapshot before running the command.
**Auto snapshot** **Auto snapshot**
A wrapper-created state named like `auto-YYYYMMDD-HHMMSS`. A wrapper-created state named like `auto-YYYYMMDD-HHMMSS`, with a numeric
suffix when needed to keep same-second snapshots unique.
**Owner restore** **Owner restore**
Restoring numeric uid/gid with `os.chown(..., follow_symlinks=False)`. Restoring numeric uid/gid with `os.chown(..., follow_symlinks=False)`.
+11 -9
View File
@@ -57,6 +57,8 @@ auto-20251230-161301
``` ```
Auto-generated snapshots are visually distinguished in the output so they are easy to identify. Auto-generated snapshots are visually distinguished in the output so they are easy to identify.
Snapshots created during the same second use a numeric suffix to remain unique,
for example `auto-20251230-161301-1`.
### Scope control ### Scope control
Restore: Restore:
@@ -85,7 +87,7 @@ It only concerns itself with **ownership** and **permissions**.
## Installation ## Installation
### From GuardUtils package repo ### From the SysMD package repository
This is the preferred method of installation. This is the preferred method of installation.
@@ -95,7 +97,7 @@ This is the preferred method of installation.
```bash ```bash
sudo mkdir -p /usr/share/keyrings sudo mkdir -p /usr/share/keyrings
curl -fsSL https://repo.sysmd.uk/guardutils/guardutils.gpg | sudo gpg --dearmor -o /usr/share/keyrings/guardutils.gpg curl -fsSL https://repo.sysmd.uk/sysmd/sysmd.gpg | sudo gpg --dearmor -o /usr/share/keyrings/sysmd.gpg
``` ```
The GPG fingerprint is `0032C71FA6A11EF9567D4434C5C06BD4603C28B1`. The GPG fingerprint is `0032C71FA6A11EF9567D4434C5C06BD4603C28B1`.
@@ -103,7 +105,7 @@ The GPG fingerprint is `0032C71FA6A11EF9567D4434C5C06BD4603C28B1`.
#### 2) Add the APT source #### 2) Add the APT source
```bash ```bash
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/guardutils.gpg] https://repo.sysmd.uk/guardutils/debian stable main" | sudo tee /etc/apt/sources.list.d/guardutils.list echo "deb [arch=amd64 signed-by=/usr/share/keyrings/sysmd.gpg] https://repo.sysmd.uk/sysmd/debian stable main" | sudo tee /etc/apt/sources.list.d/sysmd.list
``` ```
#### 3) Update and install #### 3) Update and install
@@ -118,20 +120,20 @@ sudo apt install chguard
#### 1) Import the GPG key #### 1) Import the GPG key
``` ```
sudo rpm --import https://repo.sysmd.uk/guardutils/guardutils.gpg sudo rpm --import https://repo.sysmd.uk/sysmd/sysmd.gpg
``` ```
#### 2) Add the repository configuration #### 2) Add the repository configuration
``` ```
sudo tee /etc/yum.repos.d/guardutils.repo > /dev/null << 'EOF' sudo tee /etc/yum.repos.d/sysmd.repo > /dev/null << 'EOF'
[guardutils] [sysmd]
name=GuardUtils Repository name=SysMD Repository
baseurl=https://repo.sysmd.uk/guardutils/rpm/$basearch baseurl=https://repo.sysmd.uk/sysmd/rpm/$basearch
enabled=1 enabled=1
gpgcheck=1 gpgcheck=1
repo_gpgcheck=1 repo_gpgcheck=1
gpgkey=https://repo.sysmd.uk/guardutils/guardutils.gpg gpgkey=https://repo.sysmd.uk/sysmd/sysmd.gpg
EOF EOF
``` ```
+25 -3
View File
@@ -6,6 +6,7 @@ import grp
import importlib.metadata import importlib.metadata
import os import os
import pwd import pwd
import sqlite3
import stat import stat
import subprocess import subprocess
import sys import sys
@@ -166,6 +167,28 @@ def _common_snapshot_root(paths: list[Path]) -> Path:
return Path(os.path.commonpath([str(p.resolve()) for p in paths])) return Path(os.path.commonpath([str(p.resolve()) for p in paths]))
def _create_auto_state(
conn, root_path: Path, created_by_uid: int
) -> tuple[int, str]:
base_name = f"auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
suffix = 0
while True:
auto_name = base_name if suffix == 0 else f"{base_name}-{suffix}"
try:
state_id = create_state(
conn,
auto_name,
str(root_path),
created_by_uid,
commit=False,
)
except sqlite3.IntegrityError:
suffix += 1
else:
return state_id, auto_name
def _type_for_mode(mode: int) -> str | None: def _type_for_mode(mode: int) -> str | None:
if stat.S_ISDIR(mode): if stat.S_ISDIR(mode):
return "dir" return "dir"
@@ -451,12 +474,11 @@ def main() -> None:
paths = _extract_paths_from_command(wrapper_cmd) paths = _extract_paths_from_command(wrapper_cmd)
if paths: if paths:
auto_name = f"auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
root_path = _common_snapshot_root(paths) root_path = _common_snapshot_root(paths)
with conn: with conn:
state_id = create_state( state_id, auto_name = _create_auto_state(
conn, auto_name, str(root_path), os.getuid(), commit=False conn, root_path, os.getuid()
) )
seen_paths: set[str] = set() seen_paths: set[str] = set()
Generated
+20 -25
View File
@@ -1,15 +1,15 @@
# This file is automatically @generated by Poetry 2.5.0 and should not be changed by hand. # This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand.
[[package]] [[package]]
name = "argcomplete" name = "argcomplete"
version = "3.6.3" version = "3.7.2"
description = "Bash tab completion for argparse" description = "Bash tab completion for argparse"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.10"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce"}, {file = "argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19"},
{file = "argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c"}, {file = "argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9"},
] ]
[package.extras] [package.extras]
@@ -54,14 +54,14 @@ files = [
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "4.0.8" version = "4.1.0"
description = "A platform independent file lock." description = "A platform independent file lock."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main", "dev"] groups = ["main", "dev"]
files = [ files = [
{file = "filelock-4.0.8-py3-none-any.whl", hash = "sha256:325ff22f358c18443b1fcdfa0a7aa3faec4b500c2807c554719da4567b533d31"}, {file = "filelock-4.1.0-py3-none-any.whl", hash = "sha256:2ce9818e3e2d8f284c1a964414447ef148d42a5fd5e2a477a7118e574b293ec1"},
{file = "filelock-4.0.8.tar.gz", hash = "sha256:733d9b6b153fc63672f86104324186818b6bbe9dd7db84e9bb9887b6a04a2775"}, {file = "filelock-4.1.0.tar.gz", hash = "sha256:ad7f724afef953e731b1cc39bcd3a09166d72ed7fcdf29e6e88b1c3235c6715d"},
] ]
[[package]] [[package]]
@@ -153,21 +153,16 @@ files = [
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.5.1" version = "4.12.4"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main", "dev"] groups = ["main", "dev"]
files = [ files = [
{file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, {file = "platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7"},
{file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, {file = "platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250"},
] ]
[package.extras]
docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"]
type = ["mypy (>=1.18.2)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" version = "1.6.0"
@@ -186,14 +181,14 @@ testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "3.8.0" version = "4.6.2"
description = "A framework for managing and maintaining multi-language pre-commit hooks." description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.10"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, {file = "pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e"},
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, {file = "pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441"},
] ]
[package.dependencies] [package.dependencies]
@@ -340,14 +335,14 @@ files = [
[[package]] [[package]]
name = "rich" name = "rich"
version = "14.2.0" version = "15.0.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.9.0"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"}, {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"},
{file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"}, {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"},
] ]
[package.dependencies] [package.dependencies]
@@ -379,4 +374,4 @@ python-discovery = ">=1.6"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.11,<4.0" python-versions = ">=3.11,<4.0"
content-hash = "16fd1da8878d300a16fcff44591a79e0b90041b844eba6c976a53766a3fd5ec3" content-hash = "b7038c1e7741ec33308a455f42c3dc2e9430365483f42a33b2ef07cdcd819805"
+2 -2
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "chguard" name = "chguard"
version = "0.6.0" version = "0.6.1"
description = "Safety-first tool to snapshot and restore filesystem ownership and permissions." description = "Safety-first tool to snapshot and restore filesystem ownership and permissions."
authors = [{ name = "Marco D'Aleo", email = "marco@marcodaleo.com" }] authors = [{ name = "Marco D'Aleo", email = "marco@marcodaleo.com" }]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
@@ -21,7 +21,7 @@ Repository = "https://git.sysmd.uk/guardutils/chguard"
chguard = "chguard.cli:main" chguard = "chguard.cli:main"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pre-commit = "^3.8" pre-commit = "^4.0.0"
pytest = "^9.1.1" pytest = "^9.1.1"
[tool.black] [tool.black]
+53
View File
@@ -2,9 +2,13 @@ from __future__ import annotations
import os import os
import sys import sys
from datetime import datetime
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
import pytest
import chguard.cli as cli
from chguard.cli import _restore_preview_rows, main from chguard.cli import _restore_preview_rows, main
from chguard.db import connect, create_state, init_db from chguard.db import connect, create_state, init_db
from chguard.restore import PlannedChange from chguard.restore import PlannedChange
@@ -109,3 +113,52 @@ def test_restore_with_only_skipped_items_does_not_prompt(
output = capsys.readouterr().out output = capsys.readouterr().out
assert "missing path" in output assert "missing path" in output
assert "No applicable changes" in output assert "No applicable changes" in output
def test_wrapper_uses_unique_auto_names_within_same_second(
tmp_path: Path, monkeypatch
) -> None:
db_path = tmp_path / "states.db"
target = tmp_path / "target"
target.write_text("data", encoding="utf-8")
class FixedDatetime:
@classmethod
def now(cls) -> datetime:
return datetime(2026, 10, 10, 0, 10, 48)
monkeypatch.setattr(cli, "datetime", FixedDatetime)
monkeypatch.setattr(
cli.subprocess,
"run",
lambda command: SimpleNamespace(returncode=0),
)
for _ in range(2):
monkeypatch.setattr(
sys,
"argv",
[
"chguard",
"--db",
str(db_path),
"--",
"chmod",
"600",
str(target),
],
)
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
conn = connect(db_path)
rows = conn.execute("SELECT name FROM states ORDER BY id").fetchall()
conn.close()
assert rows == [
("auto-20261010-001048",),
("auto-20261010-001048-1",),
]