ai-config-shield 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/bin/lock.ps1 +107 -0
- package/bin/unlock.ps1 +79 -0
- package/package.json +34 -0
- package/tests/check-status.ps1 +38 -0
- package/tests/e2e-security-audit.ps1 +220 -0
- package/tests/live-uac-demo.ps1 +120 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dennis Menze
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# AIConfigShield 🛡️
|
|
2
|
+
|
|
3
|
+
### Stop AI agents from taking shortcuts in your configuration files.
|
|
4
|
+
|
|
5
|
+
## The Background
|
|
6
|
+
Large Language Models (LLMs) and AI coding agents are powerful, but they have a "laziness" property learned from the vast amount of human data they've been trained on. When tasked with fixing thousands of linting errors or complex architectural issues, they often try to take the path of least resistance.
|
|
7
|
+
|
|
8
|
+
Instead of fixing 1000 errors, an AI might "secretly" modify your configuration files (like `.flake8`, `eslint.config.js`, or `.pre-commit-config.yaml`) to ignore entire categories of rules. You return to your PC after half an hour, the AI proudly reports "All tasks completed!", only for you to find out it actually fixed 10 errors and muted the other 990.
|
|
9
|
+
|
|
10
|
+
Even worse, a simple Windows "Read-Only" attribute is often not enough. Modern AI agents are clever enough to recognize and remove that attribute to continue their shortcut. **AIConfigShield** provides hardened, NTFS-level protection that makes unauthorized modification **impossible** without administrative privileges.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
- **Unbypassable Locking**: Uses NTFS permissions (`takeown` and `icacls`) to lock files at a system level.
|
|
14
|
+
- **Admin-Only Access**: Prevents any modification by the standard user session. Even "force" writes fail.
|
|
15
|
+
- **Auto-Elevation**: Automatically requests Administrator privileges to apply these deep locks.
|
|
16
|
+
- **Universal**: Use it in any project (Node, Python, Go, etc.) as long as you're on Windows.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g ai-config-shield
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Locking your "Fortress"
|
|
27
|
+
Lock your linting configs and hooks to ensure the AI actually *fixes* the code instead of hiding the problems:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx shield-lock eslint.config.js .flake8 .pre-commit-config.yaml .git/hooks/pre-commit
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Unlocking for Manual Maintenance
|
|
34
|
+
When *you* (the human) want to change the configurations:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx shield-unlock <path-to-file-or-dir>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
The tool performs a "Take Ownership" operation and then modifies the Access Control List (ACL) to grant the current user only **Read & Execute** rights, while preserving **Full Control** for the SYSTEM and Administrators (who must consciously elevate to change these rules).
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
MIT
|
|
45
|
+
|
|
46
|
+
|
package/bin/lock.ps1
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# FILE-LOCK-TOOL: LOCK
|
|
2
|
+
param(
|
|
3
|
+
[Parameter(Mandatory=$true, Position=0)]
|
|
4
|
+
[string[]]$Targets,
|
|
5
|
+
[switch] $SkipProfile
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
$global:LockLogFile = Join-Path (Get-Location).Path "lock_output.log"
|
|
9
|
+
|
|
10
|
+
function Log-Lock {
|
|
11
|
+
param([string]$Msg)
|
|
12
|
+
$timestamp = Get-Date -Format "HH:mm:ss.fff"
|
|
13
|
+
"[$timestamp] $Msg" | Out-File $global:LockLogFile -Append -Encoding utf8
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Log-Lock "=== LOCK START ==="
|
|
17
|
+
Log-Lock "Args: $($PSBoundParameters | Out-String)"
|
|
18
|
+
|
|
19
|
+
# --- 0. ADMIN-CHECK mit Auto-Elevation ---
|
|
20
|
+
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
|
|
21
|
+
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
22
|
+
$scriptPath = $MyInvocation.MyCommand.Path
|
|
23
|
+
$workDir = (Get-Location).Path
|
|
24
|
+
|
|
25
|
+
$targetArgs = ($Targets | ForEach-Object { "`"$_`"" }) -join " "
|
|
26
|
+
$skipArg = if ($SkipProfile) { " -SkipProfile" } else { "" }
|
|
27
|
+
$cmd = "Set-Location '$workDir'; & '$scriptPath' $targetArgs$skipArg; exit `$LASTEXITCODE"
|
|
28
|
+
|
|
29
|
+
$proc = Start-Process pwsh -ArgumentList "-ExecutionPolicy Bypass", "-Command", $cmd -Verb RunAs -Wait -PassThru
|
|
30
|
+
exit $proc.ExitCode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# --- 1. PRE-RESET: Entsperren für Enumeration ---
|
|
34
|
+
Log-Lock "PRE-RESET: Unlocking targets for enumeration..."
|
|
35
|
+
foreach ($targetRaw in $Targets) {
|
|
36
|
+
if (-not (Test-Path $targetRaw)) {
|
|
37
|
+
Log-Lock "WARN: Target not found: $targetRaw"
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
$item = Get-Item $targetRaw -Force -ErrorAction SilentlyContinue
|
|
41
|
+
if ($null -eq $item) { continue }
|
|
42
|
+
|
|
43
|
+
Log-Lock "PRE-RESET: $($item.FullName)"
|
|
44
|
+
if ($item.PSIsContainer) {
|
|
45
|
+
& icacls "$($item.FullName)" /reset /T /C 2>&1 | Out-Null
|
|
46
|
+
} else {
|
|
47
|
+
& icacls "$($item.FullName)" /reset /C 2>&1 | Out-Null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
Log-Lock "PRE-RESET: Done"
|
|
51
|
+
|
|
52
|
+
# --- 2. SCHUTZ ANWENDEN ---
|
|
53
|
+
$allFiles = @()
|
|
54
|
+
foreach ($targetRaw in $Targets) {
|
|
55
|
+
if (-not (Test-Path $targetRaw)) { continue }
|
|
56
|
+
$item = Get-Item $targetRaw -Force -ErrorAction SilentlyContinue
|
|
57
|
+
if ($null -eq $item) { continue }
|
|
58
|
+
|
|
59
|
+
if ($item -is [System.IO.DirectoryInfo]) {
|
|
60
|
+
$allFiles += $item.FullName
|
|
61
|
+
try {
|
|
62
|
+
$children = Get-ChildItem $item.FullName -Recurse -Force -ErrorAction Stop
|
|
63
|
+
$allFiles += ($children | ForEach-Object { $_.FullName })
|
|
64
|
+
} catch {
|
|
65
|
+
Log-Lock "WARN: Cannot enumerate $($item.FullName): $_"
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
$allFiles += $item.FullName
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Log-Lock "Expanded targets to $($allFiles.Count) files"
|
|
73
|
+
|
|
74
|
+
foreach ($fullPath in $allFiles) {
|
|
75
|
+
if (-not (Test-Path $fullPath)) { continue }
|
|
76
|
+
$item = Get-Item $fullPath -Force
|
|
77
|
+
$isDir = ($item -is [System.IO.DirectoryInfo])
|
|
78
|
+
|
|
79
|
+
Log-Lock "--- Processing: $fullPath (Dir=$isDir) ---"
|
|
80
|
+
Write-Host "🛡️ Sperre $(Split-Path $fullPath -Leaf)..." -ForegroundColor Cyan
|
|
81
|
+
|
|
82
|
+
# A. Besitz erzwingen (takeown)
|
|
83
|
+
Log-Lock "STEP: takeown"
|
|
84
|
+
$out = & takeown /F "$fullPath" /A 2>&1 | Out-String
|
|
85
|
+
Log-Lock "OUTPUT: $out"
|
|
86
|
+
|
|
87
|
+
# B. Vererbung kappen und aufräumen
|
|
88
|
+
Log-Lock "STEP: icacls reset"
|
|
89
|
+
$out = & icacls "$fullPath" /reset /c 2>&1 | Out-String
|
|
90
|
+
Log-Lock "OUTPUT: $out"
|
|
91
|
+
|
|
92
|
+
Log-Lock "STEP: icacls inheritance:r"
|
|
93
|
+
$out = & icacls "$fullPath" /inheritance:r /c 2>&1 | Out-String
|
|
94
|
+
Log-Lock "OUTPUT: $out"
|
|
95
|
+
|
|
96
|
+
# C. Lesezugriff erlauben
|
|
97
|
+
Log-Lock "STEP: icacls grant RX"
|
|
98
|
+
$out = & icacls "$fullPath" /grant "${env:USERNAME}:(RX)" /c 2>&1 | Out-String
|
|
99
|
+
Log-Lock "OUTPUT: $out"
|
|
100
|
+
|
|
101
|
+
Log-Lock "STEP: icacls grant Admin+System"
|
|
102
|
+
$out = & icacls "$fullPath" /grant "*S-1-5-32-544:(F)" /grant "SYSTEM:(F)" /c 2>&1 | Out-String
|
|
103
|
+
Log-Lock "OUTPUT: $out"
|
|
104
|
+
|
|
105
|
+
Log-Lock "--- DONE: $fullPath ---"
|
|
106
|
+
Write-Host "✅ GESPERRT: $(Split-Path $fullPath -Leaf)" -ForegroundColor Green
|
|
107
|
+
}
|
package/bin/unlock.ps1
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# FILE-LOCK-TOOL: UNLOCK
|
|
2
|
+
param(
|
|
3
|
+
[Parameter(Mandatory=$true, Position=0)]
|
|
4
|
+
[string[]]$Targets,
|
|
5
|
+
[switch] $SkipProfile
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
$global:UnlockLogFile = Join-Path (Get-Location).Path "unlock_output.log"
|
|
9
|
+
|
|
10
|
+
function Log-Unlock {
|
|
11
|
+
param([string]$Msg)
|
|
12
|
+
$timestamp = Get-Date -Format "HH:mm:ss.fff"
|
|
13
|
+
"[$timestamp] $Msg" | Out-File $global:UnlockLogFile -Append -Encoding utf8
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Log-Unlock "=== UNLOCK START ==="
|
|
17
|
+
|
|
18
|
+
# --- 0. ADMIN-CHECK mit Auto-Elevation ---
|
|
19
|
+
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
|
|
20
|
+
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
21
|
+
$scriptPath = $MyInvocation.MyCommand.Path
|
|
22
|
+
$workDir = (Get-Location).Path
|
|
23
|
+
|
|
24
|
+
$targetArgs = ($Targets | ForEach-Object { "`"$_`"" }) -join " "
|
|
25
|
+
$skipArg = if ($SkipProfile) { " -SkipProfile" } else { "" }
|
|
26
|
+
$cmd = "Set-Location '$workDir'; & '$scriptPath' $targetArgs$skipArg; exit `$LASTEXITCODE"
|
|
27
|
+
|
|
28
|
+
$proc = Start-Process pwsh -ArgumentList "-ExecutionPolicy Bypass", "-Command", $cmd -Verb RunAs -Wait -PassThru
|
|
29
|
+
exit $proc.ExitCode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# --- 1. ENTSPERRUNG ---
|
|
33
|
+
foreach ($targetRaw in $Targets) {
|
|
34
|
+
if (-not (Test-Path $targetRaw)) {
|
|
35
|
+
Log-Unlock "WARN: Target not found: $targetRaw"
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
$item = Get-Item $targetRaw -Force
|
|
39
|
+
$fullPath = $item.FullName
|
|
40
|
+
$isDir = $item.PSIsContainer
|
|
41
|
+
$fileName = Split-Path $fullPath -Leaf
|
|
42
|
+
|
|
43
|
+
Log-Unlock "--- Processing: $fullPath (Dir=$isDir) ---"
|
|
44
|
+
Write-Host "🔓 Entsperre $fileName (Dir=$isDir)..." -ForegroundColor Cyan
|
|
45
|
+
|
|
46
|
+
$takeownArgs = @("/F", "$fullPath", "/A")
|
|
47
|
+
if ($isDir) { $takeownArgs += "/R"; $takeownArgs += "/D"; $takeownArgs += "Y" }
|
|
48
|
+
|
|
49
|
+
# A. Besitz erzwingen (Admin)
|
|
50
|
+
Log-Unlock "STEP: takeown"
|
|
51
|
+
$out = & takeown $takeownArgs 2>&1 | Out-String
|
|
52
|
+
Log-Unlock "OUTPUT: $out"
|
|
53
|
+
|
|
54
|
+
# B. Integrität zurück auf Medium setzen
|
|
55
|
+
$intArgs = @("$fullPath", "/setintegritylevel", "M", "/c")
|
|
56
|
+
if ($isDir) { $intArgs += "/T" }
|
|
57
|
+
Log-Unlock "STEP: setintegritylevel M"
|
|
58
|
+
$out = & icacls $intArgs 2>&1 | Out-String
|
|
59
|
+
Log-Unlock "OUTPUT: $out"
|
|
60
|
+
|
|
61
|
+
# C. ACLs komplett zurücksetzen
|
|
62
|
+
$resetArgs = @("$fullPath", "/reset", "/c")
|
|
63
|
+
if ($isDir) { $resetArgs += "/T" }
|
|
64
|
+
Log-Unlock "STEP: reset"
|
|
65
|
+
$out = & icacls $resetArgs 2>&1 | Out-String
|
|
66
|
+
Log-Unlock "OUTPUT: $out"
|
|
67
|
+
|
|
68
|
+
# D. Besitzer zurück an User
|
|
69
|
+
$ownArgs = @("$fullPath", "/setowner", "${env:USERNAME}", "/c")
|
|
70
|
+
if ($isDir) { $ownArgs += "/T" }
|
|
71
|
+
Log-Unlock "STEP: setowner ${env:USERNAME}"
|
|
72
|
+
$out = & icacls $ownArgs 2>&1 | Out-String
|
|
73
|
+
Log-Unlock "OUTPUT: $out"
|
|
74
|
+
|
|
75
|
+
Log-Unlock "--- DONE: $fullPath ---"
|
|
76
|
+
Write-Host "✅ $fileName (und Inhalte) entsperrt." -ForegroundColor Green
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Log-Unlock "=== UNLOCK END ==="
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-config-shield",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Protect your configuration files from being silently bypassed by AI agents.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"shield-lock": "./bin/lock.ps1",
|
|
7
|
+
"shield-unlock": "./bin/unlock.ps1"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"lock": "pwsh -ExecutionPolicy Bypass -File ./bin/lock.ps1",
|
|
11
|
+
"unlock": "pwsh -ExecutionPolicy Bypass -File ./bin/unlock.ps1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ai",
|
|
15
|
+
"shield",
|
|
16
|
+
"lock",
|
|
17
|
+
"unlock",
|
|
18
|
+
"permissions",
|
|
19
|
+
"security",
|
|
20
|
+
"windows",
|
|
21
|
+
"powershell",
|
|
22
|
+
"lint",
|
|
23
|
+
"config"
|
|
24
|
+
],
|
|
25
|
+
"author": "Denksystem",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/denksystem/AIConfigShield.git"
|
|
30
|
+
},
|
|
31
|
+
"os": [
|
|
32
|
+
"win32"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string[]] $Files = @(
|
|
3
|
+
"package.json",
|
|
4
|
+
"pyproject.toml",
|
|
5
|
+
".husky\pre-commit",
|
|
6
|
+
".husky\pre-push"
|
|
7
|
+
)
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
$allLocked = $true
|
|
11
|
+
$allOpen = $true
|
|
12
|
+
|
|
13
|
+
foreach ($f in $Files) {
|
|
14
|
+
if (Test-Path $f) {
|
|
15
|
+
try {
|
|
16
|
+
# Versuch, die Datei zu öffnen (Lesen+Schreiben)
|
|
17
|
+
$stream = [System.IO.File]::Open(
|
|
18
|
+
$f,
|
|
19
|
+
[System.IO.FileMode]::Open,
|
|
20
|
+
[System.IO.FileAccess]::ReadWrite
|
|
21
|
+
)
|
|
22
|
+
$stream.Close()
|
|
23
|
+
Write-Host "OPEN: $f" -ForegroundColor Green
|
|
24
|
+
$allLocked = $false
|
|
25
|
+
} catch {
|
|
26
|
+
Write-Host "LOCKED: $f" -ForegroundColor Red
|
|
27
|
+
$allOpen = $false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if ($allLocked) {
|
|
33
|
+
exit 10
|
|
34
|
+
} # Code 10 = ALL LOCKED
|
|
35
|
+
if ($allOpen) {
|
|
36
|
+
exit 20
|
|
37
|
+
} # Code 20 = ALL OPEN
|
|
38
|
+
exit 30 # Code 30 = MIXED
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# MASTER E2E SECURITY & ANTI-GHOST AUDIT (Fail-Fast)
|
|
2
|
+
param(
|
|
3
|
+
[switch] $NoElevate,
|
|
4
|
+
[switch] $VerifyOnly,
|
|
5
|
+
[string] $Expect = "All" # All, Locked, Open
|
|
6
|
+
)
|
|
7
|
+
Write-Host "DEBUG: E2E SCRIPT STARTED (Param Expect=$Expect, VerifyOnly=$VerifyOnly, NoElevate=$NoElevate)"
|
|
8
|
+
|
|
9
|
+
# --- 0. ADMIN-CHECK ---
|
|
10
|
+
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
|
|
11
|
+
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -and -not $NoElevate) {
|
|
12
|
+
Write-Host "CRITICAL: E2E-Test erfordert Administrator-Rechte!" -ForegroundColor Red
|
|
13
|
+
Start-Process pwsh -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs
|
|
14
|
+
exit
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
$baseFiles = @("package.json", "pyproject.toml", ".husky")
|
|
18
|
+
|
|
19
|
+
# Log File Setup
|
|
20
|
+
$global:AuditLogFile = "$PSScriptRoot\audit_debug.log"
|
|
21
|
+
"--- NEW AUDIT RUN $(Get-Date) ---" | Out-File $global:AuditLogFile -Append -Encoding utf8
|
|
22
|
+
|
|
23
|
+
function Log-Audit {
|
|
24
|
+
param([string]$Msg)
|
|
25
|
+
$timestamp = Get-Date -Format "HH:mm:ss.fff"
|
|
26
|
+
"[$timestamp] $Msg" | Out-File $global:AuditLogFile -Append -Encoding utf8
|
|
27
|
+
# Auch in Konsole für Live-Log
|
|
28
|
+
Microsoft.PowerShell.Utility\Write-Host "[$timestamp] $Msg" -ForegroundColor Gray
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# 1. Alle Dateien auflösen (rekursiv bei Ordnern wie .husky)
|
|
32
|
+
$files = @()
|
|
33
|
+
foreach ($f in $baseFiles) {
|
|
34
|
+
if (Test-Path $f) {
|
|
35
|
+
$item = Get-Item $f
|
|
36
|
+
if ($item -is [System.IO.DirectoryInfo]) {
|
|
37
|
+
$files += Get-ChildItem $f -File -Recurse | ForEach-Object { $_.FullName }
|
|
38
|
+
} else {
|
|
39
|
+
$files += $item.FullName
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# --- 1. STEP RUNNER (STRICT) ---
|
|
45
|
+
function Run-Step {
|
|
46
|
+
param(
|
|
47
|
+
[string]$ActionName,
|
|
48
|
+
[scriptblock]$Action,
|
|
49
|
+
[switch]$ExpectedUnlocked,
|
|
50
|
+
[string]$TargetFile
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
Log-Audit "START: $ActionName on $TargetFile (Unlocked=$ExpectedUnlocked)"
|
|
54
|
+
|
|
55
|
+
# State Reset
|
|
56
|
+
$error.Clear()
|
|
57
|
+
$global:lastIdManualOverride = [int](Get-Random)
|
|
58
|
+
$global:lastSeenErrorCount = 0
|
|
59
|
+
|
|
60
|
+
$contentBefore = if ($TargetFile -and (Test-Path $TargetFile)) {
|
|
61
|
+
try { Get-Content $TargetFile -Raw -ErrorAction SilentlyContinue } catch { "" }
|
|
62
|
+
} else { "" }
|
|
63
|
+
# Reset State
|
|
64
|
+
$script:shadowWarningShown = $false
|
|
65
|
+
$global:AuditTargetFile = $TargetFile
|
|
66
|
+
$global:AuditWarningTriggered = $false
|
|
67
|
+
$global:SimulatedSecurityError = $null
|
|
68
|
+
$global:AuditLastExitCode = $null
|
|
69
|
+
$readErrorBefore = $false
|
|
70
|
+
$contentBefore = if ($TargetFile -and (Test-Path $TargetFile)) {
|
|
71
|
+
try { Get-Content $TargetFile -Raw -ErrorAction Stop } catch { $readErrorBefore = $true; Log-Audit "READ ERROR BEFORE: $_"; "" }
|
|
72
|
+
} else { "" }
|
|
73
|
+
|
|
74
|
+
$failed = $false
|
|
75
|
+
try {
|
|
76
|
+
$ErrorActionPreference = "Continue" # Continue damit Stderr captured werden kann
|
|
77
|
+
|
|
78
|
+
# Capture Output (Stdout + Stderr merged via 2>&1)
|
|
79
|
+
# Wir wollen es im Log UND (optional) sehen? Nein, primär Log.
|
|
80
|
+
$cmdOutput = & $Action 2>&1 | Out-String
|
|
81
|
+
|
|
82
|
+
# Logge den KOMPLETTEN Output
|
|
83
|
+
Log-Audit "OUTPUT from '$ActionName':"
|
|
84
|
+
if ($cmdOutput) {
|
|
85
|
+
$cmdOutput -split "`n" | ForEach-Object { Log-Audit " > $_" }
|
|
86
|
+
} else {
|
|
87
|
+
Log-Audit " (No Output)"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
$global:AuditLastExitCode = $LASTEXITCODE
|
|
91
|
+
Log-Audit "Action finished. ExitCode: $LASTEXITCODE"
|
|
92
|
+
|
|
93
|
+
if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne $null) {
|
|
94
|
+
$failed = $true
|
|
95
|
+
if (-not $global:SimulatedSecurityError) {
|
|
96
|
+
$global:SimulatedSecurityError = [PSCustomObject]@{
|
|
97
|
+
Exception = [PSCustomObject]@{ Message = "Native Command Failure for $TargetFile (ExitCode $LASTEXITCODE)" }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
$failed = $true
|
|
103
|
+
# Sicherstellen, dass der Fehler in $global:Error landet für den Hook
|
|
104
|
+
$global:SimulatedSecurityError = $_
|
|
105
|
+
Log-Audit "CATCH: $($_.Exception.Message)"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Fallback: Wenn Failed aber kein Fehlerobjekt (z.B. Native Git Failure)
|
|
109
|
+
# Und sicherstellen, dass wir es wirklich setzen!
|
|
110
|
+
if ($failed -and -not $global:SimulatedSecurityError) {
|
|
111
|
+
$global:SimulatedSecurityError = [PSCustomObject]@{
|
|
112
|
+
Exception = [PSCustomObject]@{ Message = "Generischer Fehler für $TargetFile (ExitCode $global:AuditLastExitCode)" }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Trigger Hook (explicitly invoke prompt with current state)
|
|
117
|
+
if (Get-Command prompt -ErrorAction SilentlyContinue) {
|
|
118
|
+
prompt | Out-Null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
$warningTriggered = $global:AuditWarningTriggered
|
|
122
|
+
$readErrorAfter = $false
|
|
123
|
+
$contentAfter = if ($TargetFile -and (Test-Path $TargetFile)) {
|
|
124
|
+
try { Get-Content $TargetFile -Raw -ErrorAction Stop } catch { $readErrorAfter = $true; Log-Audit "READ ERROR AFTER: $_"; "" }
|
|
125
|
+
} else { "" }
|
|
126
|
+
|
|
127
|
+
# Modified Logic: Only if NO Read Errors occurred
|
|
128
|
+
if ($readErrorBefore -or $readErrorAfter) {
|
|
129
|
+
$wasActuallyModified = $false
|
|
130
|
+
Log-Audit "SKIPPING Modified Check due to Read Errors (Before=$readErrorBefore, After=$readErrorAfter)"
|
|
131
|
+
} else {
|
|
132
|
+
$wasActuallyModified = ($contentBefore -ne $contentAfter)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
$errorDetail = if ($global:SimulatedSecurityError) { $global:SimulatedSecurityError.Exception.Message } else { "Keine Details (Failed=$failed)" }
|
|
136
|
+
|
|
137
|
+
if ($ExpectedUnlocked) {
|
|
138
|
+
if ($wasActuallyModified -or -not $failed) {
|
|
139
|
+
Log-Audit "SUCCESS: Allowed ($ActionName)"
|
|
140
|
+
return $true
|
|
141
|
+
} else {
|
|
142
|
+
Log-Audit "FAIL: Unexpected Block ($ActionName) - $errorDetail"
|
|
143
|
+
Microsoft.PowerShell.Utility\Write-Host " ❌ FEHLER: Unerwartet blockiert ($TargetFile)! Fehler: $errorDetail" -ForegroundColor Red
|
|
144
|
+
return $false
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
if ($wasActuallyModified) {
|
|
148
|
+
Log-Audit "CRITICAL: File Modified ($ActionName)"
|
|
149
|
+
Log-Audit " Before Len: $($contentBefore.Length)"
|
|
150
|
+
Log-Audit " After Len: $($contentAfter.Length)"
|
|
151
|
+
if ($contentBefore -eq $null) { Log-Audit " Before is NULL" }
|
|
152
|
+
if ($contentAfter -eq $null) { Log-Audit " After is NULL" }
|
|
153
|
+
|
|
154
|
+
Microsoft.PowerShell.Utility\Write-Host " ❌ SICHERHEITSLÜCKE: Datei $TargetFile manipuliert!" -ForegroundColor White -BackgroundColor Red
|
|
155
|
+
return $false
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (-not $warningTriggered) {
|
|
159
|
+
Log-Audit "FAIL-FAST: No Warning ($ActionName)"
|
|
160
|
+
Log-Audit " ErrDetails: $errorDetail"
|
|
161
|
+
Log-Audit " Pattern: $(Split-Path $TargetFile -Leaf)"
|
|
162
|
+
|
|
163
|
+
Microsoft.PowerShell.Utility\Write-Host " ❌ FAIL-FAST: Keine Warnung für $TargetFile" -ForegroundColor Red
|
|
164
|
+
return $false
|
|
165
|
+
}
|
|
166
|
+
Log-Audit "SUCCESS: Blocked + Warned ($ActionName)"
|
|
167
|
+
return $true
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# --- 2. AUDIT EXECUTION ---
|
|
172
|
+
try {
|
|
173
|
+
# 1. Dateiliste bereinigen (Duplikate entfernen)
|
|
174
|
+
$files = $files | Select-Object -Unique
|
|
175
|
+
|
|
176
|
+
if (Test-Path "./scripts/warning-hook.ps1") {
|
|
177
|
+
# Wichtig: Pattern muss auf LEAF-Namen basieren für Shell-Kompatibilität
|
|
178
|
+
$patternList = $files | ForEach-Object { Split-Path $_ -Leaf | ForEach-Object { [Regex]::Escape($_) } }
|
|
179
|
+
$global:ProtectedFilesPattern = ($patternList | Select-Object -Unique) -join '|'
|
|
180
|
+
. ./scripts/warning-hook.ps1
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (-not $VerifyOnly) {
|
|
184
|
+
Microsoft.PowerShell.Utility\Write-Host ">>> PHASE 1: LOCKING <<<" -ForegroundColor Yellow
|
|
185
|
+
. ./scripts/lock-files.ps1 -SkipProfile | Out-Null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if ($Expect -eq "All" -or $Expect -eq "Locked") {
|
|
189
|
+
foreach ($f in $files) {
|
|
190
|
+
if (Test-Path $f) {
|
|
191
|
+
# ACL Debug
|
|
192
|
+
try {
|
|
193
|
+
$acl = Get-Acl $f
|
|
194
|
+
Log-Audit "DEBUG ACL $f :: Owner=$($acl.Owner) Access=$($acl.AccessToString)"
|
|
195
|
+
} catch { Log-Audit "DEBUG ACL FAIL: $_" }
|
|
196
|
+
|
|
197
|
+
if (-not (Run-Step "Write Check" { "hack" | Out-File $f -Force } -TargetFile $f)) { exit 1 }
|
|
198
|
+
if (-not (Run-Step "Delete Check" { Remove-Item $f -Force } -TargetFile $f)) { exit 1 }
|
|
199
|
+
|
|
200
|
+
# Restore Check kann tricky sein bei untracked files, aber wir testen nur tracked files
|
|
201
|
+
if (-not (Run-Step "Restore Check" { git restore -s HEAD $f 2>&1 | Out-Null } -TargetFile $f)) { exit 1 }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if ($Expect -eq "All" -or $Expect -eq "Open") {
|
|
207
|
+
foreach ($f in $files) {
|
|
208
|
+
if (Test-Path $f) {
|
|
209
|
+
# Read Check: Versuche Inhalt zu lesen statt git add (weniger Side-Effects)
|
|
210
|
+
if (-not (Run-Step "Read Check" { Get-Content $f | Select-Object -First 1 } -ExpectedUnlocked -TargetFile $f)) { exit 1 }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
Microsoft.PowerShell.Utility\Write-Host "`nAUDIT BESTANDEN! ✅" -ForegroundColor Green
|
|
216
|
+
exit 0
|
|
217
|
+
} finally {
|
|
218
|
+
if (-not $VerifyOnly) { . ./scripts/unlock-files.ps1 -Targets $baseFiles | Out-Null }
|
|
219
|
+
}
|
|
220
|
+
exit 0
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# FINAL UAC DEMO (FULL CONSENT PROOF)
|
|
2
|
+
# Zeigt, dass KEINE Aenderung ohne explizite Zustimmung moeglich ist.
|
|
3
|
+
# Ablauf:
|
|
4
|
+
# 1. LOCK -> JA (Erfolg)
|
|
5
|
+
# 2. UNLOCK -> NEIN (Muss fehlschlagen, Datei bleibt gesperrt)
|
|
6
|
+
# 3. UNLOCK -> JA (Erfolg)
|
|
7
|
+
# 4. LOCK -> NEIN (Muss fehlschlagen, Datei bleibt offen)
|
|
8
|
+
|
|
9
|
+
$pwd = (Get-Item .).FullName
|
|
10
|
+
|
|
11
|
+
# Helper for detailed logging
|
|
12
|
+
function Log-Demo {
|
|
13
|
+
param([string]$Msg)
|
|
14
|
+
$timestamp = Get-Date -Format "HH:mm:ss.fff"
|
|
15
|
+
$logFile = Join-Path $pwd "scripts/audit_debug.log"
|
|
16
|
+
"[$timestamp] [DEMO-CONTROLLER] $Msg" | Out-File $logFile -Append -Encoding utf8
|
|
17
|
+
Write-Host $Msg -ForegroundColor DarkGray
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Clear all logs at start
|
|
21
|
+
$scriptsDir = Join-Path $pwd "scripts"
|
|
22
|
+
Remove-Item (Join-Path $scriptsDir "audit_debug.log") -ErrorAction SilentlyContinue
|
|
23
|
+
Remove-Item (Join-Path $scriptsDir "lock_output.log") -ErrorAction SilentlyContinue
|
|
24
|
+
Remove-Item (Join-Path $scriptsDir "unlock_output.log") -ErrorAction SilentlyContinue
|
|
25
|
+
Remove-Item (Join-Path $scriptsDir "lock_processing.log") -ErrorAction SilentlyContinue
|
|
26
|
+
|
|
27
|
+
Log-Demo "--- DEMO STARTED ---"
|
|
28
|
+
|
|
29
|
+
function Show-Header {
|
|
30
|
+
param($Text)
|
|
31
|
+
Log-Demo "PHASE START: $Text"
|
|
32
|
+
Write-Host "`n====================================================" -ForegroundColor Magenta
|
|
33
|
+
Write-Host " $Text" -ForegroundColor Magenta
|
|
34
|
+
Write-Host "====================================================" -ForegroundColor Magenta
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Run-Audit {
|
|
38
|
+
param($TargetExpect)
|
|
39
|
+
Log-Demo "Run-Audit Expect=$TargetExpect Start"
|
|
40
|
+
# Wir rufen das Audit-Skript auf und geben nur relevante Zeilen aus
|
|
41
|
+
# -NoElevate wird genutzt, da die Demo bereits UAC/Admin-Kontext steuert.
|
|
42
|
+
|
|
43
|
+
$auditPath = Join-Path $pwd "scripts/e2e-security-audit.ps1"
|
|
44
|
+
|
|
45
|
+
# Starte Audit als NEUEN Prozess, damit Windows frische ACLs liest (kein Caching vom Parent)
|
|
46
|
+
$proc = Start-Process pwsh -ArgumentList "-ExecutionPolicy Bypass", "-File `"$auditPath`"", "-Expect $TargetExpect", "-VerifyOnly", "-NoElevate" -Wait -PassThru -NoNewWindow
|
|
47
|
+
$auditExitCode = $proc.ExitCode
|
|
48
|
+
|
|
49
|
+
if ($auditExitCode -ne 0) {
|
|
50
|
+
Log-Demo "Run-Audit Failed with $auditExitCode"
|
|
51
|
+
Write-Host "🚨 DEMO ABBRUCH: Audit fehlgeschlagen mit Exit-Code: $auditExitCode" -ForegroundColor Red
|
|
52
|
+
if (Test-Path "$PSScriptRoot\audit_debug.log") {
|
|
53
|
+
Write-Host "`n--- AUDIT LOG (FEHLER DETAILS) ---" -ForegroundColor Yellow
|
|
54
|
+
Get-Content "$PSScriptRoot\audit_debug.log" -Tail 30
|
|
55
|
+
Write-Host "----------------------------------" -ForegroundColor Yellow
|
|
56
|
+
}
|
|
57
|
+
# NICHT automatisch entsperren - User soll manuell unlock-files.ps1 ausführen wenn nötig
|
|
58
|
+
exit 1
|
|
59
|
+
}
|
|
60
|
+
Log-Demo "Run-Audit Success"
|
|
61
|
+
Write-Host "✨ Audit ($TargetExpect) bestanden!" -ForegroundColor Green
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# --- PHASE 1: LOCK (JA) ---
|
|
65
|
+
Show-Header "PHASE 1: SPERRE AKTIVIEREN (KLICK: JA)"
|
|
66
|
+
Write-Host ">>> BITTE JETZT IM WINDOWS-FENSTER AUF 'JA' KLICKEN <<<" -ForegroundColor Red -BackgroundColor White
|
|
67
|
+
|
|
68
|
+
Log-Demo "Sending UAC Request for lock-files.ps1..."
|
|
69
|
+
Start-Process pwsh -ArgumentList "-NoProfile", "-File", "$pwd/scripts/lock-files.ps1", "-SkipProfile" -Verb RunAs -Wait
|
|
70
|
+
Log-Demo "UAC Request finished."
|
|
71
|
+
|
|
72
|
+
Write-Host "`nAUDIT 1: Prüfe ob Dateien GESPERRT sind (und Warnung kommt!)..." -ForegroundColor Yellow
|
|
73
|
+
Run-Audit "Locked"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --- PHASE 2: UNLOCK (NEIN) ---
|
|
77
|
+
Show-Header "PHASE 2: ENTSPERREN ABLEHNEN (KLICK: NEIN)"
|
|
78
|
+
Write-Host "Wir versuchen den Schutz aufzuheben. Du verbietest es." -ForegroundColor Gray
|
|
79
|
+
Write-Host ">>> BITTE JETZT IM WINDOWS-FENSTER AUF 'NEIN' / 'ABBRECHEN' KLICKEN <<<" -ForegroundColor Red -BackgroundColor White
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
Start-Process pwsh -ArgumentList "-NoProfile", "-File", "$pwd/scripts/unlock-files.ps1" -Verb RunAs -Wait -ErrorAction Stop
|
|
83
|
+
Write-Host "`nHinweis: Du hast 'JA' geklickt, wir wollten 'NEIN' testen." -ForegroundColor Gray
|
|
84
|
+
} catch {
|
|
85
|
+
Write-Host "`n✅ ERFOLG: Du hast das Entsperren verweigert!" -ForegroundColor Green
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Write-Host "`nAUDIT 2: Prüfe ob Dateien IMMER NOCH GESPERRT sind..." -ForegroundColor Yellow
|
|
89
|
+
Run-Audit "Locked"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# --- PHASE 3: UNLOCK (JA) ---
|
|
93
|
+
Show-Header "PHASE 3: ENTSPERREN ERLAUBEN (KLICK: JA)"
|
|
94
|
+
Write-Host "Jetzt erlauben wir es wirklich." -ForegroundColor Gray
|
|
95
|
+
Write-Host ">>> BITTE JETZT IM WINDOWS-FENSTER AUF 'JA' KLICKEN <<<" -ForegroundColor Red -BackgroundColor White
|
|
96
|
+
|
|
97
|
+
Start-Process pwsh -ArgumentList "-NoProfile", "-File", "$pwd/scripts/unlock-files.ps1" -Verb RunAs -Wait
|
|
98
|
+
|
|
99
|
+
Write-Host "`nAUDIT 3: Prüfe ob Dateien jetzt OFFEN sind..." -ForegroundColor Yellow
|
|
100
|
+
Run-Audit "Open"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --- PHASE 4: LOCK (NEIN) ---
|
|
104
|
+
Show-Header "PHASE 4: SPERRE ABLEHNEN (KLICK: NEIN)"
|
|
105
|
+
Write-Host "Versuch einer erneuten Sperre. Du verbietest es." -ForegroundColor Gray
|
|
106
|
+
Write-Host ">>> BITTE JETZT IM WINDOWS-FENSTER AUF 'NEIN' / 'ABBRECHEN' KLICKEN <<<" -ForegroundColor Red -BackgroundColor White
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
Start-Process pwsh -ArgumentList "-NoProfile", "-File", "$pwd/scripts/lock-files.ps1", "-SkipProfile" -Verb RunAs -Wait -ErrorAction Stop
|
|
110
|
+
Write-Host "`nHinweis: Du hast 'JA' geklickt, wir wollten 'NEIN' testen." -ForegroundColor Gray
|
|
111
|
+
} catch {
|
|
112
|
+
Write-Host "`n✅ ERFOLG: Du hast das Sperren verweigert!" -ForegroundColor Green
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Write-Host "`nAUDIT 4: Prüfe ob Dateien IMMER NOCH OFFEN sind..." -ForegroundColor Yellow
|
|
116
|
+
Run-Audit "Open"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
Show-Header "DEMO BEENDET: EXAKT 4 ADMIN-ENTSCHEIDUNGEN (JA, NEIN, JA, NEIN)"
|
|
120
|
+
Write-Host "Beweisführung abgeschlossen: Ohne dein JA passiert hier gar nichts." -ForegroundColor Green
|