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:
pull_request:
@@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true
jobs:
precommit-and-security:
lint-and-security:
name: lint-and-security
runs-on: ubuntu-latest
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)]"
' pip-audit.json
# Fail only on fixable findings (mirrors the Grype policy in security-scan.yml).
# The type guard ensures a crashed/empty report fails rather than silently passing.
# Fail only on fixable findings. The type guard ensures a crashed or empty
# report fails rather than silently passing. Dependency security updates
# are handled by the central Renovate job.
jq -e '
(.dependencies | type) == "array"
and
( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 )
' 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:
pull_request:
@@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true
jobs:
precommit-and-security:
lint-and-security:
name: lint-and-security
runs-on: ubuntu-latest
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)]"
' pip-audit.json
# Fail only on fixable findings (mirrors the Grype policy in security-scan.yml).
# The type guard ensures a crashed/empty report fails rather than silently passing.
# Fail only on fixable findings. The type guard ensures a crashed or empty
# report fails rather than silently passing. Dependency security updates
# are handled by the central Renovate job.
jq -e '
(.dependencies | type) == "array"
and
( [ .dependencies[] | (.vulns // [])[] | select((.fix_versions | length) > 0) ] | length == 0 )
' 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
README.md user-facing documentation
.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
```
@@ -316,7 +316,7 @@ The wrapper snapshot flow is:
wrapper_cmd
-> _extract_paths_from_command()
-> _common_snapshot_root()
-> create auto-YYYYMMDD-HHMMSS state
-> create a unique auto-YYYYMMDD-HHMMSS[-N] state
-> _iter_entries_for_target() for each path
-> insert each relative path once
-> run subprocess.run(wrapper_cmd)
@@ -595,17 +595,6 @@ Gitea pull request workflow:
-> 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.
---
@@ -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.
**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**
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.
Snapshots created during the same second use a numeric suffix to remain unique,
for example `auto-20251230-161301-1`.
### Scope control
Restore:
@@ -85,7 +87,7 @@ It only concerns itself with **ownership** and **permissions**.
## Installation
### From GuardUtils package repo
### From the SysMD package repository
This is the preferred method of installation.
@@ -95,7 +97,7 @@ This is the preferred method of installation.
```bash
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`.
@@ -103,7 +105,7 @@ The GPG fingerprint is `0032C71FA6A11EF9567D4434C5C06BD4603C28B1`.
#### 2) Add the APT source
```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
@@ -118,20 +120,20 @@ sudo apt install chguard
#### 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
```
sudo tee /etc/yum.repos.d/guardutils.repo > /dev/null << 'EOF'
[guardutils]
name=GuardUtils Repository
baseurl=https://repo.sysmd.uk/guardutils/rpm/$basearch
sudo tee /etc/yum.repos.d/sysmd.repo > /dev/null << 'EOF'
[sysmd]
name=SysMD Repository
baseurl=https://repo.sysmd.uk/sysmd/rpm/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://repo.sysmd.uk/guardutils/guardutils.gpg
gpgkey=https://repo.sysmd.uk/sysmd/sysmd.gpg
EOF
```
+25 -3
View File
@@ -6,6 +6,7 @@ import grp
import importlib.metadata
import os
import pwd
import sqlite3
import stat
import subprocess
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]))
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:
if stat.S_ISDIR(mode):
return "dir"
@@ -451,12 +474,11 @@ def main() -> None:
paths = _extract_paths_from_command(wrapper_cmd)
if paths:
auto_name = f"auto-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
root_path = _common_snapshot_root(paths)
with conn:
state_id = create_state(
conn, auto_name, str(root_path), os.getuid(), commit=False
state_id, auto_name = _create_auto_state(
conn, root_path, os.getuid()
)
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]]
name = "argcomplete"
version = "3.6.3"
version = "3.7.2"
description = "Bash tab completion for argparse"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce"},
{file = "argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c"},
{file = "argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19"},
{file = "argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9"},
]
[package.extras]
@@ -54,14 +54,14 @@ files = [
[[package]]
name = "filelock"
version = "4.0.8"
version = "4.2.4"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.10"
python-versions = ">=3.11"
groups = ["main", "dev"]
files = [
{file = "filelock-4.0.8-py3-none-any.whl", hash = "sha256:325ff22f358c18443b1fcdfa0a7aa3faec4b500c2807c554719da4567b533d31"},
{file = "filelock-4.0.8.tar.gz", hash = "sha256:733d9b6b153fc63672f86104324186818b6bbe9dd7db84e9bb9887b6a04a2775"},
{file = "filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e"},
{file = "filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d"},
]
[[package]]
@@ -153,21 +153,16 @@ files = [
[[package]]
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`."
optional = false
python-versions = ">=3.10"
groups = ["main", "dev"]
files = [
{file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"},
{file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"},
{file = "platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7"},
{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]]
name = "pluggy"
version = "1.6.0"
@@ -186,14 +181,14 @@ testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "3.8.0"
version = "4.6.2"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
{file = "pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e"},
{file = "pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441"},
]
[package.dependencies]
@@ -340,14 +335,14 @@ files = [
[[package]]
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"
optional = false
python-versions = ">=3.8.0"
python-versions = ">=3.9.0"
groups = ["main"]
files = [
{file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"},
{file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"},
{file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"},
{file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"},
]
[package.dependencies]
@@ -379,4 +374,4 @@ python-discovery = ">=1.6"
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<4.0"
content-hash = "16fd1da8878d300a16fcff44591a79e0b90041b844eba6c976a53766a3fd5ec3"
content-hash = "b7038c1e7741ec33308a455f42c3dc2e9430365483f42a33b2ef07cdcd819805"
+2 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "chguard"
version = "0.6.0"
version = "0.6.1"
description = "Safety-first tool to snapshot and restore filesystem ownership and permissions."
authors = [{ name = "Marco D'Aleo", email = "marco@marcodaleo.com" }]
license = "GPL-3.0-or-later"
@@ -21,7 +21,7 @@ Repository = "https://git.sysmd.uk/guardutils/chguard"
chguard = "chguard.cli:main"
[tool.poetry.group.dev.dependencies]
pre-commit = "^3.8"
pre-commit = "^4.0.0"
pytest = "^9.1.1"
[tool.black]
+53
View File
@@ -2,9 +2,13 @@ from __future__ import annotations
import os
import sys
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
import pytest
import chguard.cli as cli
from chguard.cli import _restore_preview_rows, main
from chguard.db import connect, create_state, init_db
from chguard.restore import PlannedChange
@@ -109,3 +113,52 @@ def test_restore_with_only_skipped_items_does_not_prompt(
output = capsys.readouterr().out
assert "missing path" 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",),
]