19 Commits
Author SHA1 Message Date
mdaleo404 2c598887ff Merge pull request 'Update dependency filelock to v4.2.1 [SECURITY]' (#21) from renovate/pypi-filelock-vulnerability into main
Reviewed-on: #21
2026-10-28 10:14:52 +00:00
kamabot 1cdd09ecfd Update dependency filelock to v4.2.1 [SECURITY]
CI / lint-and-security (pull_request) Successful in 2m6s
CI / tests (3.11) (pull_request) Successful in 33s
CI / tests (3.13) (pull_request) Successful in 25s
2026-10-24 03:02:07 +00:00
mdaleo404 807130ffdb Standardise PR workflow on ci.yml and drop scheduled SBOM scan 2026-10-13 07:36:29 +01:00
mdaleo404 40840b5250 Use composite action for scans 2026-10-10 17:19:38 +01:00
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
9 changed files with 184 additions and 250 deletions

No files matched your search

@@ -1,4 +1,4 @@
name: Lint & Security name: CI
on: on:
pull_request: pull_request:
@@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
precommit-and-security: lint-and-security:
name: lint-and-security name: lint-and-security
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
@@ -70,10 +70,40 @@ jobs:
| "\($d.name)@\($d.version) -> \(.id) [fix: \(if (.fix_versions | length) > 0 then (.fix_versions | join(", ")) else "none" end)]" | "\($d.name)@\($d.version) -> \(.id) [fix: \(if (.fix_versions | length) > 0 then (.fix_versions | join(", ")) else "none" end)]"
' pip-audit.json ' pip-audit.json
# Fail only on fixable findings (mirrors the Grype policy in security-scan.yml). # Fail only on fixable findings. The type guard ensures a crashed or empty
# The type guard ensures a crashed/empty report fails rather than silently passing. # report fails rather than silently passing. Dependency security updates
# are handled by the central Renovate job.
jq -e ' jq -e '
(.dependencies | type) == "array" (.dependencies | type) == "array"
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
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.13"]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- 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
-188
View File
@@ -1,188 +0,0 @@
name: Security Scan
on:
schedule:
- cron: 27 8 * * *
workflow_dispatch:
jobs:
security-scan:
runs-on: running-man
env:
TARGET_DIR: .
COSIGN_VERSION: v3.0.5
SYFT_VERSION: v1.42.3
GRYPE_VERSION: v0.110.0
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Cosign (bootstrap)
run: |
set -euo pipefail
FILE="cosign-linux-amd64"
curl -fLO https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/${FILE}
chmod +x ${FILE}
mv ${FILE} /usr/local/bin/cosign
cosign version
- name: Install Syft (verified)
run: |
set -euo pipefail
VERSION_NO_V="${SYFT_VERSION#v}"
FILE="syft_${VERSION_NO_V}_linux_amd64.tar.gz"
BASE_URL="https://github.com/anchore/syft/releases/download/${SYFT_VERSION}"
curl -fLO ${BASE_URL}/${FILE}
curl -fLO ${BASE_URL}/syft_${VERSION_NO_V}_checksums.txt
curl -fLO ${BASE_URL}/syft_${VERSION_NO_V}_checksums.txt.sig
curl -fLO ${BASE_URL}/syft_${VERSION_NO_V}_checksums.txt.pem
cosign verify-blob \
--signature syft_${VERSION_NO_V}_checksums.txt.sig \
--certificate syft_${VERSION_NO_V}_checksums.txt.pem \
--certificate-identity-regexp "https://github.com/anchore/syft" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
syft_${VERSION_NO_V}_checksums.txt
CHECKSUM_LINE=$(grep " ${FILE}$" syft_${VERSION_NO_V}_checksums.txt)
if [ -z "$CHECKSUM_LINE" ]; then
echo "Missing checksum entry for ${FILE}"
exit 1
fi
echo "$CHECKSUM_LINE" | sha256sum -c -
tar -xzf ${FILE}
mv syft /usr/local/bin/
syft version
- name: Install Grype (verified)
run: |
set -euo pipefail
VERSION_NO_V="${GRYPE_VERSION#v}"
FILE="grype_${VERSION_NO_V}_linux_amd64.tar.gz"
BASE_URL="https://github.com/anchore/grype/releases/download/${GRYPE_VERSION}"
curl -fLO ${BASE_URL}/${FILE}
curl -fLO ${BASE_URL}/grype_${VERSION_NO_V}_checksums.txt
curl -fLO ${BASE_URL}/grype_${VERSION_NO_V}_checksums.txt.sig
curl -fLO ${BASE_URL}/grype_${VERSION_NO_V}_checksums.txt.pem
cosign verify-blob \
--signature grype_${VERSION_NO_V}_checksums.txt.sig \
--certificate grype_${VERSION_NO_V}_checksums.txt.pem \
--certificate-identity-regexp "https://github.com/anchore/grype" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
grype_${VERSION_NO_V}_checksums.txt
CHECKSUM_LINE=$(grep " ${FILE}$" grype_${VERSION_NO_V}_checksums.txt)
if [ -z "$CHECKSUM_LINE" ]; then
echo "Missing checksum entry for ${FILE}"
exit 1
fi
echo "$CHECKSUM_LINE" | sha256sum -c -
tar -xzf ${FILE}
mv grype /usr/local/bin/
grype version
- name: Generate SBOM
working-directory: ${{ env.TARGET_DIR }}
run: |
syft dir:. -o json > sbom.json
- name: Show SBOM contents
working-directory: ${{ env.TARGET_DIR }}
run: |
echo "Packages discovered by Syft:"
jq -r '.artifacts[] | "\(.name)@\(.version) [\(.type)]"' sbom.json | sort
- name: Run Grype scan (JSON)
id: audit
continue-on-error: true
working-directory: ${{ env.TARGET_DIR }}
run: |
grype sbom:sbom.json -o json > grype.json
echo "Vulnerabilities (fixable only):"
jq -r '
.matches[]
| select((.vulnerability.fix.versions | length) > 0)
| "\(.artifact.name)@\(.artifact.version) -> \(.vulnerability.id) [\(.vulnerability.severity)] | fixed: \(.vulnerability.fix.versions[0])"
' grype.json
# Fail only on fixable MEDIUM/HIGH/CRITICAL
jq -e '
[
.matches[]?
| select(
(
.vulnerability.severity == "Medium" or
.vulnerability.severity == "High" or
.vulnerability.severity == "Critical"
)
and
(
(.vulnerability.fix.versions | length) > 0
)
)
]
| length == 0
' grype.json
- name: Show full Grype table
working-directory: ${{ env.TARGET_DIR }}
run: |
echo "Full Grype report:"
grype sbom:sbom.json -o table
- name: Notify Node-RED on vulnerabilities
if: steps.audit.outcome == 'failure'
working-directory: ${{ env.TARGET_DIR }}
run: |
jq '
{
repo: "guardutils/chguard",
summary: (
"Total: " +
(
[
.matches[]
| select((.vulnerability.fix.versions | length) > 0)
] | length | tostring
)
),
vulnerabilities: [
.matches[]
| select((.vulnerability.fix.versions | length) > 0)
| {
library: .artifact.name,
cve: .vulnerability.id,
severity: .vulnerability.severity,
installed: .artifact.version,
fixed: (.vulnerability.fix.versions[0]),
title: .vulnerability.description,
url: .vulnerability.dataSource
}
]
}
' grype.json \
| curl -s -X POST https://nodered.sysmd.uk/vulns-alert \
-H "Content-Type: application/json" \
--data-binary @-
- name: Fail workflow if vulnerabilities found
if: steps.audit.outcome == 'failure'
run: exit 1
@@ -1,4 +1,4 @@
name: Lint & Security name: CI
on: on:
pull_request: pull_request:
@@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
precommit-and-security: lint-and-security:
name: lint-and-security name: lint-and-security
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
@@ -70,10 +70,40 @@ jobs:
| "\($d.name)@\($d.version) -> \(.id) [fix: \(if (.fix_versions | length) > 0 then (.fix_versions | join(", ")) else "none" end)]" | "\($d.name)@\($d.version) -> \(.id) [fix: \(if (.fix_versions | length) > 0 then (.fix_versions | join(", ")) else "none" end)]"
' pip-audit.json ' pip-audit.json
# Fail only on fixable findings (mirrors the Grype policy in security-scan.yml). # Fail only on fixable findings. The type guard ensures a crashed or empty
# The type guard ensures a crashed/empty report fails rather than silently passing. # report fails rather than silently passing. Dependency security updates
# are handled by the central Renovate job.
jq -e ' jq -e '
(.dependencies | type) == "array" (.dependencies | type) == "array"
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
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.13"]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- 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
+4 -14
View File
@@ -69,7 +69,7 @@ pyproject.toml Poetry package metadata and console script
poetry.lock locked dependency graph poetry.lock locked dependency graph
README.md user-facing documentation README.md user-facing documentation
.pre-commit-config.yaml Black and generic pre-commit hooks .pre-commit-config.yaml Black and generic pre-commit hooks
.gitea/workflows/ lint, dependency audit, SBOM and Grype workflows .gitea/workflows/ lint and dependency-audit workflows
dist/ built release artifacts, not source dist/ built release artifacts, not source
``` ```
@@ -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)
@@ -595,17 +595,6 @@ Gitea pull request workflow:
-> pip-audit dependency audit -> pip-audit dependency audit
``` ```
Scheduled/manual security workflow:
```text
.gitea/workflows/security-scan.yml
-> install verified Cosign, Syft, and Grype
-> generate SBOM
-> scan for vulnerabilities
-> notify Node-RED on fixable Medium/High/Critical vulnerabilities
-> fail workflow on those vulnerabilities
```
Pre-commit currently includes Bandit, Black, trailing whitespace, EOF, YAML, and TOML checks. Pre-commit currently includes Bandit, Black, trailing whitespace, EOF, YAML, and TOML checks.
--- ---
@@ -798,7 +787,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
+21 -26
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.2.4"
description = "A platform independent file lock." description = "A platform independent file lock."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.11"
groups = ["main", "dev"] groups = ["main", "dev"]
files = [ files = [
{file = "filelock-4.0.8-py3-none-any.whl", hash = "sha256:325ff22f358c18443b1fcdfa0a7aa3faec4b500c2807c554719da4567b533d31"}, {file = "filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e"},
{file = "filelock-4.0.8.tar.gz", hash = "sha256:733d9b6b153fc63672f86104324186818b6bbe9dd7db84e9bb9887b6a04a2775"}, {file = "filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d"},
] ]
[[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",),
]