Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions .github/workflows/windows-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Windows QA
on: [push, pull_request]
permissions:
contents: read
jobs:
regression:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: PowerShell 5.1 syntax and regression tests
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
Get-ChildItem -Recurse -Filter *.ps1 | ForEach-Object { $tokens=$null; $errors=$null; $null=[Management.Automation.Language.Parser]::ParseFile($_.FullName,[ref]$tokens,[ref]$errors); if ($errors.Count) { throw ($errors | Out-String) } }
Import-Module Pester -RequiredVersion 3.4.0
$result = Invoke-Pester -Script ./tests -PassThru
if ($result.FailedCount -gt 0) { throw 'Regression tests failed.' }
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ credential.dpapi
*.bak
*.zip
.DS_Store

region-state.json
run.lock
*password*.txt
*alerts*.txt
20 changes: 10 additions & 10 deletions Install-QbtGuard.ps1
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# qBtGuard 0.1.0-preview installer. Run as the intended scheduled-task Windows user.
# qBtGuard 0.2.0 candidate installer. Run as the intended scheduled-task Windows user.
[CmdletBinding()]
param(
[string]$Destination = (Join-Path $env:LOCALAPPDATA 'qBtGuard'),
[switch]$NoTask
[switch]$NoTask,
[string]$TaskName = 'qBtGuard Audit'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$taskName = 'qBtGuard Audit'
$taskName = $TaskName
$staging = $null
$installed = $false
$taskCreated = $false
Expand All @@ -26,7 +27,7 @@ try {
if (Test-Path -LiteralPath $destinationFull) { throw "Destination already exists: $destinationFull. No changes made." }
$parent = Split-Path -Parent $destinationFull
if (-not (Test-Path -LiteralPath $parent -PathType Container)) { throw "Destination parent does not exist: $parent" }
Write-Host 'qBtGuard preview: READ-ONLY. No torrent deletion or PIA changes.'
Write-Host 'qBtGuard candidate: audit mode by default; corrective actions require configuration and QA.'
$url = Read-Host 'qBittorrent Web UI URL [http://127.0.0.1:8080]'
if ([string]::IsNullOrWhiteSpace($url)) { $url = 'http://127.0.0.1:8080' }
$uri = [uri]$url
Expand All @@ -45,11 +46,9 @@ try {
$null = Invoke-RestMethod -Uri ($baseUrl + '/api/v2/app/version') -WebSession $session -Headers $headers -TimeoutSec 15 -ErrorAction Stop
$null = Invoke-RestMethod -Uri ($baseUrl + '/api/v2/torrents/info') -WebSession $session -Headers $headers -TimeoutSec 30 -ErrorAction Stop
Write-Host 'Authentication and read-only API access verified.'
$config = [ordered]@{
QbtUrl=$baseUrl; QbtUser=$user; BlockNoExtension=$true; VerifyMkvSignatures=$true
BlockedExtensions=@('.exe','.scr','.com','.bat','.cmd','.msi','.msp','.ps1','.psm1','.vbs','.vbe','.js','.jse','.wsf','.wsh','.hta','.cpl','.pif','.lnk','.jar','.zipx')
AuditOnly=$true; IntervalMinutes=10
}
$config = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'config.example.json') -Raw | ConvertFrom-Json
$config.QbtUrl = $baseUrl; $config.QbtUser = $user
$config.AuditOnly = $true
$staging = Join-Path $parent ('.qBtGuard-staging-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $staging -ErrorAction Stop | Out-Null
Copy-Item -LiteralPath $source -Destination (Join-Path $staging 'QbtGuard.ps1') -ErrorAction Stop
Expand All @@ -65,7 +64,7 @@ try {
$trigger = New-ScheduledTaskTrigger -Daily -At (Get-Date).AddMinutes(2)
$trigger.Repetition = New-CimInstance -Namespace 'Root/Microsoft/Windows/TaskScheduler' -ClassName 'MSFT_TaskRepetitionPattern' -ClientOnly -Property @{ Interval = 'PT10M'; Duration = 'P1D'; StopAtDurationEnd = $false }
$principal = New-ScheduledTaskPrincipal -UserId ([Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
$settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 9)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description 'qBtGuard read-only audit; runs while user is logged on' -ErrorAction Stop | Out-Null
$taskCreated = $true
}
Expand All @@ -84,3 +83,4 @@ try {
try { Invoke-WebRequest -Uri ($uri.AbsoluteUri.TrimEnd('/') + '/api/v2/auth/logout') -Method Post -WebSession $session -Headers $headers -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop | Out-Null } catch {}
}
}

20 changes: 20 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Consolidation and enforcement QA

This procedure is for anyone replacing older qBittorrent/PIA scripts. The pilot installation completed a manual cutover; the installer itself never modifies or disables other installations.

1. Inventory all tasks and startup launchers targeting qBittorrent or PIA. Record their accounts, paths, intervals, current configuration and capabilities. Back up task XML, scripts, credentials and persistent state privately.
2. Install this candidate into a new directory with `-NoTask`. Retain the old working installation. Configure the candidate for the actual machine and VPN adapter; keep audit mode enabled. Do not reuse another Windows user's DPAPI credential.
3. Run Windows syntax and mocked regression tests. Run a live audit and confirm API authentication, interface binding, forwarded-port discovery and MKV coverage. Compare results to the existing script. Concurrent old automation can remove findings before an audit sees them.
4. On an isolated test instance, exercise each enforced action independently: prohibited extension, extensionless file, invalid header, unreadable/missing MKV, healthy restart, blocked restart, port change, reconnecting VPN, explicit forwarding failure, recovery timeout and corrupt cooldown file. Use disposable torrent data. Verify no mutation in audit mode and no unintended deletion on failures.
5. Confirm the intended qBittorrent profile and adapter names agree before enabling restart. This candidate supports the 4.3.6 `Connection\Interface` profile key; other profile layouts fail closed and need an explicit implementation/test update.
6. For cutover, first disable the verified old tasks and wait for all their active runs to finish. Keep backups and do not delete the old installation. Enable the desired candidate action flags and enforcement mode, manually verify a cycle, then register exactly one task using the candidate launcher and intended Windows account. The stock installer refuses existing destinations/tasks; task replacement must be performed separately after review.
7. Verify the hidden launcher propagates a failure exit code, the task succeeds every ten minutes, and log summaries show actual coverage. Confirm kill-switch and qBittorrent binding behavior while PIA is disconnected.
8. For rollback, disable and stop the new task/run before re-enabling the old task. Restore old configuration/state if altered. Deleted torrent data is irreversible through this script; recover from independent backups if available.

Keep audit mode on and existing production protection in place until the candidate clears QA. The old deletion script may remain enforced during read-only comparison; never leave both enforcement engines scheduled after cutover.

## Retiring the old installation

After a successful observation period, export the current replacement task and preserve the old task XML, scripts, configuration, DPAPI files and state privately. Confirm the replacement task action points to the installation being retained. Remove only verified, disabled legacy task definitions; archive their script directories instead of deleting backups. Search task actions and startup launchers again for references to the archived paths. Never move the active installation without updating and verifying its task action. A directory with QA in its name can remain the production installation; renaming it is not required.

Keep rollback data tied to the original Windows account and machine. Do not publish credentials, personal configuration, logs or task exports to GitHub.
40 changes: 40 additions & 0 deletions QA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# qBtGuard 0.2.0 pilot validation

## Tested implementation

Engine commit: `f9fc69e96df5a8fb2137c1a1a2e75e1bb138f87f`.
Environment: Windows, Windows PowerShell 5.1, Pester 3.4.0, qBittorrent 4.3.6 and local PIA/WireGuard integration.
No addresses, account names, credentials, personal torrent names or raw logs are included in this report.

## Evidence

| Check | Observed result |
|---|---|
| Windows CI syntax and regression suite | 84 passed; 0 failed/skipped/pending |
| Pilot regression suite | 84 passed; 0 failed/skipped/pending |
| Audit action gate | Mocked regressions plus scoped live audits proposed actions without deletion |
| VPN profile/interface compatibility | Saved display name and distinct internal ID accepted; live API binding checked |
| qBittorrent restart | Missing process restarted in an isolated enforcement test after readiness and binding checks |
| PIA port synchronization | Deliberately mismatched listening port changed to the forwarded port and verified |
| VPN recovery | Revised bounded connect/retry implementation completed an unattended region recovery |
| Prohibited-extension fixture | Scoped audit flagged the file; enforcement removed torrent; both fixture data files verified absent |
| Extensionless fixture | Repeated using a verified 100% complete torrent; scoped enforcement removed torrent and both data files |
| Invalid MKV header fixture | Scoped audit inspected one completed MKV and reported invalid EBML signature; enforcement removed torrent and both data files |
| Initial whole-queue enforcement | 1,499 files, 358 MKVs inspected, 13 skipped; no findings/unreadable MKVs/removals |
| Silent scheduled execution | Replacement task returned 0 and continued on its ten-minute schedule |
| October 19 check | Latest task run 09:07 local returned 0; next run 09:17; both legacy tasks disabled |
| Quiet log observation | Successful heartbeats October 17 and 19; no errors or findings in the supplied log; latest heartbeat inspected 1,906 files and 503 MKVs, with 7 skipped |

The pilot cutover occurred October 15, 2026, after exporting old task definitions and copying installations into a private rollback directory. Production enabled PIA recovery, port synchronization, guarded restart and prohibited/extensionless deletion. Invalid-MKV deletion remained disabled after its isolated fixture test. The active installation retained its original QA directory name.

## Interpretation and limits

The observation covers October 15–19, not an independently verified history of every scheduled execution. Quiet logging omits uneventful enforcement summaries, and Task Scheduler's latest result establishes only the latest run. No organic prohibited-file event was recorded during the supplied observation; deletion evidence comes from controlled fixtures.

The first recovery test needed a manual reconnect and did not pass unattended QA. The implementation was revised and the subsequent recovery test completed without that assistance. The initial extensionless deletion attempt left files behind with unresolved location/completion issues; it was repeated from torrent creation with correct source data and verified completion. Do not count the initial attempt as a successful data-deletion test.

MKV inspection checks the EBML signature only. It does not establish complete container integrity, valid codecs, playability or absence of malware. Skipped and unreadable MKVs are not deletion triggers. A valid file in another container renamed as MKV can be flagged.

The original heartbeat rule writes only if the log is missing or older than 00:01 on the previous local calendar day; every-other-day heartbeats can therefore be expected. Findings, notices, actions and failures log immediately.

Clean-machine installation, different Windows/qBittorrent/PIA versions, remote API installations and alternate profile layouts are not established by this pilot. The task requires the installing user to be logged in and the computer awake. Following a reboot, user sign-in is required. No age-based disk cleanup is included.
Loading
Loading