Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.7] - 2026-05-10

### Security
- **APT `Clean()` now respects `DryRun`**. Previously, `apt autoclean` executed regardless of the `DryRun` option, silently defeating the safety mechanism users expect from dry-run mode. The YUM/Snap/Flatpak Clean implementations already honored DryRun; APT now matches that behavior. Regression tests added in `manager/apt/apt_clean_dryrun_test.go`.
- **Docker test runners no longer mask test failures**. The compose entrypoints used `bash -c` with `&&` chains and trailing `|| true` on fixture-generation steps; due to bash operator precedence, the `|| true` caught failures from earlier in the chain (including `go test`), letting failed tests pass CI silently. Switched to `bash -ec` with explicit `;` separators so test failures abort immediately while fixture-generation steps remain individually allowed to fail.
- Added `read_only: true` and `no-new-privileges:true` to the `test-all` aggregator service for defense-in-depth.

### Changed
- **`PackageInfo` JSON output now uses snake_case field names** (e.g. `package_manager`, `new_version`, `additional_data`) instead of Go's default PascalCase. Required fields (`name`, `status`, `package_manager`) are always emitted; optional fields use `omitempty`. Consumers parsing JSON output must update field names. (#40, thanks @aijanai)

### Fixed
- `go.mod` now declares `go 1.23.0` (full version) instead of `go 1.23`, resolving Go toolchain download failures in some environments. (#40)

## [0.1.6] - 2025-11-01

### Added
Expand Down Expand Up @@ -37,14 +50,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Technical debt cleanup and APT Upgrade method fix
- APT Upgrade method now correctly uses `apt install` for specific packages

## Recent Achievements ✅
## Recent Achievements ✅

### Architecture & Code Quality
- ✅ **CommandRunner Architecture**: Complete architectural consistency (Issue #20, PR #26)
- ✅ **APT & YUM executeCommand Pattern**: Centralized command execution, eliminated code duplication
- ✅ **Technical Debt Cleanup**: Fixed APT Upgrade method bug, removed misleading TODOs, verified no resource leaks

### Security Enhancements
### Security Enhancements
- ✅ **Security Enhancements**: Input validation for package names (Issue #23, PR #25)
- ✅ **Command Injection Prevention**: Comprehensive ValidatePackageName implementation across all package managers

Expand Down Expand Up @@ -77,7 +90,7 @@ Current development focus areas (see [GitHub Issues](https://github.com/bluet/sy
- **Security scanning with Snyk** - Add to CI/CD pipeline
- **CommandRunner migration** - Complete Snap and Flatpak integration (Issues #28, #29)

### Medium Priority Pending
### Medium Priority Pending
- **Test coverage improvements** - YUM gaps (Issue #32), Snap & Flatpak comprehensive suites
- **CLI improvements** - Upgrade display (Issue #3), macOS apt conflict (Issue #2)
- **Code quality** - Context support, custom error types, DRY principle improvements
Expand All @@ -90,7 +103,7 @@ Current development focus areas (see [GitHub Issues](https://github.com/bluet/sy

### Currently Supported ✅
- **APT** (Ubuntu/Debian) - Full feature support
- **YUM** (Rocky Linux/AlmaLinux/RHEL) - Full feature support
- **YUM** (Rocky Linux/AlmaLinux/RHEL) - Full feature support
- **Snap** (Universal packages) - Full feature support
- **Flatpak** (Universal packages) - Full feature support

Expand All @@ -101,4 +114,4 @@ Current development focus areas (see [GitHub Issues](https://github.com/bluet/sy
### Planned 📋
- **Homebrew** (macOS) - Planned for cross-platform expansion
- **Chocolatey/Scoop/winget** (Windows) - Planned for Windows support
- **Zypper** (openSUSE) - Lower priority
- **Zypper** (openSUSE) - Lower priority
13 changes: 10 additions & 3 deletions manager/apt/apt.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,16 +378,23 @@ func (a *PackageManager) UpgradeAll(opts *manager.Options) ([]manager.PackageInf

// Clean cleans the local package cache used by the apt package manager.
func (a *PackageManager) Clean(opts *manager.Options) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

if opts == nil {
opts = &manager.Options{
DryRun: false,
Interactive: false,
Verbose: false,
}
}
Comment thread
bluet marked this conversation as resolved.

// Handle dry run mode
if opts.DryRun {
log.Println("Dry run mode: would execute 'apt autoclean'")
return nil
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

args := []string{"autoclean"}
out, err := a.executeCommand(ctx, args, opts)
if err != nil {
Expand Down
65 changes: 65 additions & 0 deletions manager/apt/apt_clean_dryrun_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package apt_test

import (
"testing"

"github.com/bluet/syspkg/manager"
"github.com/bluet/syspkg/manager/apt"
)
Comment thread
bluet marked this conversation as resolved.

// TestCleanRespectsDryRun is the regression test for the security-relevant bug
// where Clean() executed `apt autoclean` even when opts.DryRun was true.
// Behavior contract: Clean(DryRun=true) MUST NOT execute any underlying command.
func TestCleanRespectsDryRun(t *testing.T) {
mockRunner := manager.NewMockCommandRunner()
pm := apt.NewPackageManagerWithCustomRunner(mockRunner)

if err := pm.Clean(&manager.Options{DryRun: true}); err != nil {
t.Fatalf("Clean(DryRun=true) returned error: %v", err)
}

if got := len(mockRunner.EnvCalls); got != 0 {
t.Errorf("Clean(DryRun=true) executed %d non-interactive command(s); expected 0. Calls: %v",
got, mockRunner.EnvCalls)
}
if got := len(mockRunner.InteractiveCalls); got != 0 {
t.Errorf("Clean(DryRun=true) executed %d interactive command(s); expected 0. Calls: %v",
got, mockRunner.InteractiveCalls)
}
}

// TestCleanRunsWithoutDryRun guards against the Clean(DryRun) fix being
// implemented as a blanket no-op. Without DryRun, Clean MUST invoke
// `apt autoclean`.
func TestCleanRunsWithoutDryRun(t *testing.T) {
mockRunner := manager.NewMockCommandRunner()
mockRunner.AddCommand("apt", []string{"autoclean"}, []byte("Reading package lists...\n"), nil)
pm := apt.NewPackageManagerWithCustomRunner(mockRunner)

if err := pm.Clean(&manager.Options{DryRun: false}); err != nil {
t.Fatalf("Clean(DryRun=false) returned error: %v", err)
}

if _, ok := mockRunner.EnvCalls["apt autoclean"]; !ok {
t.Errorf("Clean(DryRun=false) didn't invoke 'apt autoclean'. Recorded calls: %v",
mockRunner.EnvCalls)
}
}

// TestCleanRespectsDryRunWithNilOptsDefault verifies the nil-opts branch:
// when opts == nil, the code path defaults DryRun to false, so Clean
// SHOULD execute (proving the nil-opts default is preserved by the fix).
func TestCleanRunsWithNilOpts(t *testing.T) {
mockRunner := manager.NewMockCommandRunner()
mockRunner.AddCommand("apt", []string{"autoclean"}, []byte("Reading package lists...\n"), nil)
pm := apt.NewPackageManagerWithCustomRunner(mockRunner)

if err := pm.Clean(nil); err != nil {
t.Fatalf("Clean(nil) returned error: %v", err)
}

if _, ok := mockRunner.EnvCalls["apt autoclean"]; !ok {
t.Errorf("Clean(nil) didn't invoke 'apt autoclean'. Recorded calls: %v",
mockRunner.EnvCalls)
}
}
49 changes: 32 additions & 17 deletions testing/docker/docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ services:
volumes:
- ../..:/workspace
working_dir: /workspace
# NOTE: bash -ec ensures any failed required command (test, apt update)
# exits immediately. The `|| true` on fixture-generation lines is the
# explicit opt-out — those steps are allowed to fail without failing the
# build. Previously this used `&&` chaining which silently masked test
# failures because trailing `|| true` caught failures from earlier in the
# chain.
command: >
bash -c "
echo 'Running Ubuntu APT tests...' &&
go test -v -tags='unit integration apt' ./manager/apt ./osinfo &&
echo 'Generating APT fixtures...' &&
apt update &&
apt search vim > testing/fixtures/apt/search-vim-ubuntu22.txt 2>/dev/null || true &&
bash -ec "
echo 'Running Ubuntu APT tests...';
go test -v -tags='unit integration apt' ./manager/apt ./osinfo;
echo 'Generating APT fixtures...';
apt update;
apt search vim > testing/fixtures/apt/search-vim-ubuntu22.txt 2>/dev/null || true;
apt show vim > testing/fixtures/apt/show-vim-ubuntu22.txt 2>/dev/null || true
"

Expand All @@ -42,13 +48,14 @@ services:
volumes:
- ../..:/workspace
working_dir: /workspace
# See ubuntu-apt-test for rationale on bash -ec + ; separators.
command: >
bash -c "
echo 'Running Rocky Linux YUM tests...' &&
go test -v -tags='unit integration yum' ./manager/yum ./osinfo &&
echo 'Generating YUM fixtures...' &&
yum search vim > testing/fixtures/yum/search-vim-rocky8.txt 2>/dev/null || true &&
yum info vim-enhanced > testing/fixtures/yum/info-vim-rocky8.txt 2>/dev/null || true &&
bash -ec "
echo 'Running Rocky Linux YUM tests...';
go test -v -tags='unit integration yum' ./manager/yum ./osinfo;
echo 'Generating YUM fixtures...';
yum search vim > testing/fixtures/yum/search-vim-rocky8.txt 2>/dev/null || true;
yum info vim-enhanced > testing/fixtures/yum/info-vim-rocky8.txt 2>/dev/null || true;
yum list --installed > testing/fixtures/yum/list-installed-rocky8.txt 2>/dev/null || true
"

Expand All @@ -66,12 +73,13 @@ services:
volumes:
- ../..:/workspace
working_dir: /workspace
# See ubuntu-apt-test for rationale on bash -ec + ; separators.
command: >
bash -c "
echo 'Running AlmaLinux YUM tests...' &&
go test -v -tags='unit integration yum' ./manager/yum ./osinfo &&
echo 'Generating YUM fixtures...' &&
yum search vim > testing/fixtures/yum/search-vim-alma8.txt 2>/dev/null || true &&
bash -ec "
echo 'Running AlmaLinux YUM tests...';
go test -v -tags='unit integration yum' ./manager/yum ./osinfo;
echo 'Generating YUM fixtures...';
yum search vim > testing/fixtures/yum/search-vim-alma8.txt 2>/dev/null || true;
yum info vim-enhanced > testing/fixtures/yum/info-vim-alma8.txt 2>/dev/null || true
"

Expand Down Expand Up @@ -131,6 +139,13 @@ services:
- almalinux-yum-test
# - fedora-dnf-test # TODO: Enable when DNF support is implemented
# - alpine-apk-test # TODO: Enable when APK support is implemented
# Defense-in-depth: this aggregator service only runs an echo, so it can
# safely use a read-only root and no-new-privileges. Required services
# (ubuntu/rocky/alma) need write access for fixture generation, so they
# don't get these constraints.
Comment thread
bluet marked this conversation as resolved.
Outdated
read_only: true
security_opt:
- no-new-privileges:true
Comment thread
bluet marked this conversation as resolved.
Outdated
volumes:
- ../..:/workspace
working_dir: /workspace
Expand Down
Loading