Gorstaks Lite Antivirus
unknown
powershell
2 months ago
15 kB
3
No Index
# Antivirus.ps1
# Author: Gorstak
param(
[switch]$Uninstall,
[switch]$ChaosMode, # create & detect EICAR test file
[switch]$LearningMode, # log only — no quarantine, no kill
[switch]$SelfTest # basic config & path check, then exit
)
#Requires -Version 5.1
#Requires -RunAsAdministrator # we will enforce it anyway
$Script:Version = "2026-merged-light-0.9"
# ============================================================================
# 1. Early elevation check & restart if needed
# ============================================================================
function Test-IsAdmin {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
([Security.Principal.WindowsPrincipal]$id).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
if (-not (Test-IsAdmin)) {
Write-Host "Requesting elevation..." -ForegroundColor Yellow
try {
Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" $($PSBoundParameters.Keys -join ' ')" -Verb RunAs -Wait:$false
} catch {
Write-Host "Elevation failed: $($_.Exception.Message)" -ForegroundColor Red
}
exit 0
}
Write-Host "Running elevated ($PID)" -ForegroundColor Green
# ============================================================================
# 2. Configuration
# ============================================================================
$Base = "C:\ProgramData\Antivirus"
$Quarantine = "$Base\Quarantine"
$Backup = "$Base\Backup"
$LogDir = "$Base\Logs"
$LogFile = "$LogDir\antivirus_$(Get-Date -Format 'yyyy-MM').log"
$HashCache = "$Base\Data\hashcache.csv"
$PIDFile = "$Base\Data\pid.txt"
$MonitoredExtensions = @(
'.exe','.dll','.sys','.scr','.com','.cpl','.msi','.bat','.cmd','.ps1','.vbs',
'.js','.jse','.wsf','.hta','.jar','.lnk','.pif','.url','.exif','.winmd'
)
$RiskyPaths = @('\temp','\downloads','\appdata\local\temp','\public','\desktop')
$ProtectedProcesses = @(
'smss','csrss','wininit','winlogon','services','lsass','svchost',
'explorer','dwm','conhost','MsMpEng','NisSrv','SecurityHealthService'
)
$Config = @{
LearningMode = $LearningMode.IsPresent
AutoQuarantine = -not $LearningMode.IsPresent
AutoKill = -not $LearningMode.IsPresent
EnableToast = $true
CirclUrl = "https://hashlookup.circl.lu/lookup/sha256"
MaxHashCacheAgeDays= 45
EICARHash = "275A021BBFB6489E54D471899F7DB9D1663FC695EC2FE2A2C4538AABF651FD0F"
}
# Create structure
@($Base, $Quarantine, $Backup, $LogDir, "$Base\Data") | ForEach-Object {
if (-not (Test-Path $_)) { New-Item $_ -ItemType Directory -Force | Out-Null }
}
# ============================================================================
# 3. Logging
# ============================================================================
function Write-Log {
param(
[string]$Message,
[string]$Level = "INFO" # INFO, WARN, ERROR, THREAT, ALLOW
)
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$line = "$ts [$Level] $Message"
# Console
switch ($Level) {
"THREAT" { Write-Host $line -ForegroundColor Red }
"ERROR" { Write-Host $line -ForegroundColor Red }
"WARN" { Write-Host $line -ForegroundColor Yellow }
default { Write-Host $line -ForegroundColor Gray }
}
# File (rotate monthly by filename)
$line | Out-File -FilePath $LogFile -Append -Encoding utf8 -Force
# Very basic size rotation (optional improvement later)
if ((Get-Item $LogFile -ea SilentlyContinue).Length -gt 10MB) {
$old = $LogFile + ".old"
if (Test-Path $old) { Remove-Item $old -Force }
Rename-Item $LogFile $old -Force
}
}
Write-Log "Antivirus-Merged-Light v$Script:Version starting (PID:$PID) LearningMode:$($Config.LearningMode)" "INFO"
# ============================================================================
# 4. Hash reputation cache (most valuable addition)
# ============================================================================
$script:HashCache = @{}
function Load-HashCache {
if (-not (Test-Path $HashCache)) { return }
try {
Import-Csv $HashCache -Header Hash,IsSafe,Timestamp | ForEach-Object {
$dt = [datetime]::ParseExact($_.Timestamp,"yyyy-MM-dd HH:mm:ss",$null)
if ($dt -gt (Get-Date).AddDays(-$Config.MaxHashCacheAgeDays)) {
$script:HashCache[$_.Hash] = [bool]::Parse($_.IsSafe)
}
}
Write-Log "Loaded $($script:HashCache.Count) hash cache entries" "INFO"
} catch {
Write-Log "Hash cache load failed: $_" "WARN"
}
}
function Save-HashResult {
param([string]$Hash, [bool]$IsSafe)
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$Hash,$IsSafe,$ts" | Out-File $HashCache -Append -Encoding utf8
$script:HashCache[$Hash] = $IsSafe
}
function Test-IsKnownGood {
param([string]$Path)
if (-not (Test-Path $Path -PathType Leaf)) { return $false }
# Signature first (fast & strong)
try {
$sig = Get-AuthenticodeSignature $Path -ErrorAction Stop
if ($sig.Status -eq "Valid" -or $sig.Status -eq "TrustedPublisher") {
return $true
}
} catch {}
# Hash cache
try {
$h = (Get-FileHash $Path -Algorithm SHA256 -ErrorAction Stop).Hash.ToLower()
if ($script:HashCache.ContainsKey($h)) {
return $script:HashCache[$h]
}
} catch {}
# CIRCL (only one external lookup for now)
try {
$h = (Get-FileHash $Path -Algorithm SHA256).Hash.ToLower()
$r = Invoke-RestMethod "$($Config.CirclUrl)/$h" -TimeoutSec 6 -UseBasicParsing -ErrorAction Stop
if ($r) {
Save-HashResult $h $true
Write-Log "CIRCL known-good: $Path" "ALLOW"
return $true
}
} catch {
Write-Log "CIRCL lookup failed for $Path : $_" "WARN"
}
return $false
}
# ============================================================================
# 5. Quarantine logic
# ============================================================================
function Invoke-Quarantine {
param(
[string]$Path,
[string]$Reason
)
if ($Config.LearningMode -or -not $Config.AutoQuarantine) {
Write-Log "Would quarantine: $Path ($Reason)" "THREAT"
return
}
if (-not (Test-Path $Path)) { return }
$name = [IO.Path]::GetFileName($Path)
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$qPath = Join-Path $Quarantine "$name`_$stamp"
$bak = Join-Path $Backup "$name`_$stamp.bak"
# Try to release locks (non-protected processes only)
Get-Process | Where-Object {
$_.Modules.FileName -contains $Path -and $ProtectedProcesses -notcontains $_.Name
} | ForEach-Object {
try { Stop-Process $_.Id -Force -ea SilentlyContinue } catch {}
}
try {
Copy-Item $Path $bak -Force
Move-Item $Path $qPath -Force
Write-Log "QUARANTINED $Path → $qPath ($Reason) backup: $bak" "THREAT"
if ($Config.EnableToast) {
# Minimal toast (expand later with BurntToast module if wanted)
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.MessageBox]::Show("Threat quarantined: $name`nReason: $Reason","Antivirus Alert",[System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Warning)
}
} catch {
Write-Log "Quarantine failed: $Path → $($_.Exception.Message)" "ERROR"
}
}
# ============================================================================
# 6. Core decision logic
# ============================================================================
function Decide-And-Act {
param([string]$Path)
if (-not (Test-Path $Path -PathType Leaf)) { return }
$ext = [IO.Path]::GetExtension($Path).ToLower()
if ($ext -notin $MonitoredExtensions) { return }
# Fast allow: trusted signature / CIRCL / cache
if (Test-IsKnownGood $Path) {
Write-Log "Allowed (reputation) -> $Path" "ALLOW"
return
}
# Heuristic: small unsigned DLL in risky path
$isSuspDll = $false
if ($ext -in @('.dll','.winmd')) {
try {
$sig = Get-AuthenticodeSignature $Path
if ($sig.Status -ne "Valid") {
$size = (Get-Item $Path).Length
$lower = $Path.ToLower()
if ($RiskyPaths | Where-Object { $lower -like "*$_*" } -and $size -lt 4MB) {
$isSuspDll = $true
}
}
} catch {}
}
if ($isSuspDll) {
Invoke-Quarantine $Path "Suspicious small unsigned DLL in risky location"
return
}
# Default action
Write-Log "No strong reputation - monitoring only -> $Path" "INFO"
} # <--- this closing brace was probably missing
# ============================================================================
# 7. Chaos / EICAR test mode
# ============================================================================
if ($ChaosMode) {
Write-Host "`n=== CHAOS / EICAR TEST MODE ===" -ForegroundColor Cyan
$eicarPart1 = 'X5O!P%@AP[4\PZX54(P^)7CC)7}'
$eicarPart2 = '$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'
$eicarStr = $eicarPart1 + $eicarPart2
$testFile = Join-Path $env:TEMP "eicar-test-$(Get-Random).com"
Set-Content -Path $testFile -Value $eicarStr -Encoding Ascii -NoNewline
Write-Host "Created EICAR test file: $testFile" -ForegroundColor Green
Start-Sleep -Milliseconds 1200
Decide-And-Act $testFile
Remove-Item $testFile -Force -ErrorAction SilentlyContinue
Write-Host "Test finished. Check log: $LogFile`n" -ForegroundColor Cyan
exit 0
}
# ============================================================================
# 8. Initial scan (wider than original)
# ============================================================================
Write-Log "Initial scan started" "INFO"
$initialFolders = @(
"$env:USERPROFILE\Downloads",
"$env:USERPROFILE\Desktop",
"$env:TEMP",
"$env:APPDATA",
"$env:LOCALAPPDATA\Temp",
"C:\Temp",
"C:\Users\Public"
)
# Optional: shallow scan of fixed drives roots
Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' | ForEach-Object {
$root = $_.DeviceID + ":\"
if (Test-Path $root) { $initialFolders += $root }
}
foreach ($folder in $initialFolders) {
if (-not (Test-Path $folder)) { continue }
Get-ChildItem $folder -Recurse -Depth 2 -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -in $MonitoredExtensions } |
ForEach-Object { Decide-And-Act $_.FullName }
}
Write-Log "Initial scan finished" "INFO"
# ============================================================================
# 9. Real-time watchers
# ============================================================================
$WatchFolders = $initialFolders | Select-Object -Unique
$watcherList = @()
foreach ($folder in $WatchFolders) {
if (-not (Test-Path $folder)) { continue }
$w = New-Object IO.FileSystemWatcher $folder, "*.*" -Property @{
IncludeSubdirectories = $true
NotifyFilter = 'FileName,LastWrite'
EnableRaisingEvents = $true
}
Register-ObjectEvent $w Created -Action {
$p = $Event.SourceEventArgs.FullPath
$ext = [IO.Path]::GetExtension($p).ToLower()
if ($using:MonitoredExtensions -contains $ext) {
Start-Sleep -Milliseconds 900 # give file time to finish writing
Decide-And-Act $p
}
} | Out-Null
$watcherList += $w
Write-Log "Watcher started on: $folder" "INFO"
}
# ============================================================================
# 10. Reflective / manual mapping detector (kept from original — very nice feature)
# ============================================================================
Write-Log "Starting reflective/manual-map background scanner" "INFO"
Start-Job -Name "ReflectiveScanner" -ScriptBlock {
$protected = $using:ProtectedProcesses
$log = "$using:Base\reflective.log"
while ($true) {
Start-Sleep -Seconds 12
Get-Process | Where-Object { $_.WorkingSet64 -gt 35MB } | ForEach-Object {
$p = $_
$sus = [string]::IsNullOrWhiteSpace($p.Path) -or
($p.Modules | Where-Object { [string]::IsNullOrWhiteSpace($_.FileName) })
if ($sus -and $protected -notcontains $p.ProcessName) {
$msg = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | Possible reflective/manual-map → $($p.Name) ($($p.Id)) Path='$($p.Path)'"
$msg | Out-File $log -Append -Encoding utf8
try { Stop-Process $p.Id -Force -ea SilentlyContinue } catch {}
}
}
}
} | Out-Null
# ============================================================================
# 11. Simple WMI process creation hook
# ============================================================================
Register-WmiEvent -Query "SELECT * FROM Win32_ProcessStartTrace" -Action {
$path = $Event.SourceEventArgs.NewEvent.ProcessName
if ($path -and (Test-Path $path)) {
Decide-And-Act $path
}
} | Out-Null
Write-Log "WMI process creation hook registered" "INFO"
# ============================================================================
# 12. Main loop (periodic sweep)
# ============================================================================
try {
Load-HashCache
while ($true) {
Get-Process -ErrorAction SilentlyContinue | ForEach-Object {
try {
$path = $_.MainModule.FileName
if ($path -and (Test-Path $path)) {
Decide-And-Act $path
}
} catch {}
}
Start-Sleep -Seconds 45
}
}
finally {
Write-Log 'Script exiting / terminated' 'INFO'
foreach ($w in $watcherList) {
$w.EnableRaisingEvents = $false
$w.Dispose()
}
# Optional: remove PID file, cleanup mutex, etc.
}
Editor is loading...
Leave a Comment