Detecting Portable and Unauthorized Software with PowerShell and GPO

In most enterprise environments, software inventory is treated as a solved problem. Tools like Microsoft Endpoint Configuration Manager (MECM/SCCM), Intune, or even basic registry parsing are deployed, tuned, and assumed to provide complete visibility.

But inventory tools only report what is formally installed via Windows Installer, MSI, or recognized deployment mechanisms. They miss what lives outside those boundaries.

In our environment, we faced a compliance-driven cleanup of 1C:Enterprise instances. SCCM showed 100% compliance. Field audits and user reports showed otherwise. The gap? Legacy remnants, offline archives, and fully portable deployments running directly from network shares or user directories. These binaries never touched the registry, never registered in Appwiz.cpl, and remained invisible to standard CM inventories.

This article documents the exact architecture, implementation, and operational tuning used to detect, collect, and report on unauthorized or portable software across a Windows domain. The methodology is vendor-agnostic and can be adapted for any shadow-IT or compliance cleanup scenario.

Architecture & Design Rationale

Before diving into configuration, it’s critical to understand why we chose this stack:

ComponentWhy It Was Chosen
Group Policy (Computer Config)Centralized, auditable, runs in System context without user interaction.
Scheduled Task (At Startup)Executes before user login, ensures full drive access, and leverages SYSTEM privileges.
VBScript WrapperConsistently hides PowerShell console output in GPO contexts and avoids rare startup crashes that can occur when PowerShell is launched directly in System context on certain Windows builds.
PowerShell Detection ScriptNative, zero-dependency, recursive file/folder scanning with exclusion logic to protect I/O.
UNC Network ShareCentralized, agent-less collection. Each endpoint writes its own CSV, enabling easy scaling and audit trails.

Key Design Decisions:

  • Startup Trigger, not Logon: Ensures coverage for locked machines, shared terminals, and remote servers.
  • NT AUTHORITY\SYSTEM Context: Guarantees read access to all volumes, including those with restrictive ACLs.
  • Overwrite vs Append: Each boot generates a fresh report. No data accumulation, no stale state, simpler aggregation.

Implementation: Step-by-Step Deployment

1. Create & Link the GPO

Launch gpmc.msc to open the Group Policy Management Console. Navigate to the OU containing your target computer objects. Create a new GPO tailored for this detection scope.

Context menu in Group Policy Management Console highlighting the "Create a GPO in this domain, and Link it here..." option.

Name it descriptively: Det_PortableSoftware_1C_Inventory. Ensure it’s linked to an OU with computer objects, not users.

2. Configure Computer Configuration → Scheduled Tasks

Open the GPO editor. Navigate to:
Computer Configuration → Preferences → Control Panel Settings → Scheduled Tasks

Group Policy Management Editor tree view with Computer Configuration > Preferences > Control Panel Settings > Scheduled Tasks highlighted.

3. Create the Scheduled Task (Action & General Settings)

Right-click → New → Scheduled Task (At least Windows 7).

create scheduled task via GPO

General Tab:

  • Action: Update (ensures idempotent deployment across GPO refresh cycles)
  • Name: Det_1C_Portable_Scan
  • Run as: NT AUTHORITY\SYSTEM
  • Run whether user is logged on or not
  • Run with highest privileges
  • Hidden (prevents UI flicker during boot)
Scheduled Task General tab configuration showing Action set to Update, Run as NT AUTHORITY\System, and checkboxes for "Run whether user is logged on or not", "Run with highest privileges", and "Hidden" enabled.

Triggers Tab:

  • Begin the task: At startup
  • Delay task for: 1 minute (allows network stack and DFS/DFS-R to stabilize before share access)
Scheduled Task Triggers tab configuration showing trigger set to "At startup" with a 1-minute delay checkbox enabled.

Actions Tab:
Instead of launching PowerShell directly, we use a VBScript wrapper to handle console suppression reliably.

  • Program/script: C:\Windows\System32\wscript.exe
  • Add arguments: "\\yourdomain\SYSVOL\yourdomain.com\scripts\Det_1C_Wrapper.vbs"
Scheduled Task Actions tab configuration showing wscript.exe as the executable and a SYSVOL UNC path passed as an argument.

4. VBScript Wrapper (Det_1C_Wrapper.vbs)

Store this in SYSVOL\...scripts\. It launches PowerShell silently.

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "powershell.exe -ExecutionPolicy Bypass -File ""\\yourdomain\SYSVOL\yourdomain.com\scripts\Det_1C_Detection.ps1""", 0, False

Why not direct PowerShell? GPO Scheduled Tasks sometimes inherit inconsistent execution policies or console handles in System context. wscript.exe guarantees a detached, silent execution path across Windows 10/11/Server 2016-2022.

The Detection Engine: PowerShell & Performance Tuning

Scanning every drive recursively at boot is a major I/O risk. The following script is optimized for production:

  • Dynamically discovers drives
  • Excludes known high-traffic/system directories
  • Overwrites (not appends) to prevent bloated logs
  • Includes execution timing for baseline monitoring
  • Handles CSV parsing safely for paths with commas
File Explorer view of a central network share containing multiple CSV files named after endpoints (e.g., PC1.csv, PC2.csv) generated by the detection task.
file generated by GPO
# Det_1C_Detection.ps1
# Production-ready, performance-tuned portable software scanner

$ComputerName = $env:COMPUTERNAME
$SharePath    = "\\fileserver\1c_inventory"
$ReportPath   = Join-Path $SharePath "$ComputerName.csv"

# Patterns to flag
$filePatterns = @("1cv8.exe", "1cv8s.exe", "1cestart.exe", "1cv8c.exe", "1cv8cons.exe")
$folderPatterns = @("1c*", "*1cv8*", "*1centerprise*")

# Critical exclusions to prevent boot I/O spikes
$excludeDirs = @(
    "$env:ProgramData",
    "C:\Windows",
    "C:\Program Files",
    "C:\Program Files (x86)",
    "$env:SystemRoot\System32"
)

# Dynamically discover all local & mapped drives
$drives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Root -ne "\\" }

# Initialize report (overwrite, not append)
"ComputerName,Type,FullPath,FileSize,LastWriteTime" | Out-File -FilePath $ReportPath -Encoding UTF8 -Force

function Test-IsExcluded {
    param([string]$Path)
    foreach ($ex in $excludeDirs) {
        if ($Path.StartsWith($ex, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $true
        }
    }
    return $false
}

$sw = [System.Diagnostics.Stopwatch]::StartNew()

foreach ($drive in $drives) {
    $root = $drive.Root
    if (-not (Test-Path $root)) { continue }

    # 1. File scan (usually faster & more targeted)
    try {
        Get-ChildItem -Path $root -Filter "*.exe" -Recurse -ErrorAction SilentlyContinue |
            Where-Object { $filePatterns -contains $_.Name -and -not (Test-IsExcluded $_.FullName) } |
            ForEach-Object {
                "$ComputerName,File,$($_.FullName),$($_.Length),$($_.LastWriteTime)" |
                    Out-File -FilePath $ReportPath -Append -Encoding UTF8
            }
    } catch {
        # Silently continue on permission/access errors
    }

    # 2. Folder scan (limited recursion depth to protect I/O)
    try {
        Get-ChildItem -Path $root -Directory -Depth 5 -ErrorAction SilentlyContinue |
            Where-Object { 
                $folderPatterns -contains $_.Name -or 
                $_.Name -like "1c*" -or 
                $_.Name -like "*1cv8*" 
            } |
            Where-Object { -not (Test-IsExcluded $_.FullName) } |
            ForEach-Object {
                "$ComputerName,Folder,$($_.FullName),$($_.Length),$($_.LastWriteTime)" |
                    Out-File -FilePath $ReportPath -Append -Encoding UTF8
            }
    } catch {}
}

$sw.Stop()
Write-Host "[$ComputerName] Scan completed in $($sw.Elapsed.TotalSeconds) seconds" -ForegroundColor Green

Key Optimizations:

  • Get-PSDrive dynamically maps volumes. No hardcoded D:\, E:\.
  • -Depth 5 on folder scans prevents infinite recursion on massive data drives.
  • Test-IsExcluded protects system & program directories.
  • Execution time is logged. If a machine consistently exceeds 30s, investigate high I/O or large data volumes.

Data Aggregation: From CSVs to Actionable Reports

Each endpoint generates a CSV at \\fileserver\1c_inventory\<COMPUTERNAME>.csv. Raw CSVs are useful for forensics, but leadership & compliance teams need a consolidated view.

Microsoft Excel workbook displaying aggregated scan results with "Computer" and "FoundItems" columns, formatted with bold headers and frozen top row.

Use ImportExcel (cross-platform compatible) to merge, clean, and export. Note the shift from Get-Content to Import-Csv for robust comma/quote handling.

# Aggregation Script (Run on admin workstation/CI server)
Import-Module ImportExcel -ErrorAction Stop

$SourceFolder = "D:\1c_inventory_reports"
$OutputExcel  = "D:\1c_inventory_reports\1C_Scan_Aggregated.xlsx"

$data = foreach ($csv in Get-ChildItem -Path $SourceFolder -Filter "*.csv") {
    $Computer = $csv.BaseName
    try {
        $rows = Import-Csv -Path $csv.FullName -ErrorAction Stop
        $found = $rows | Where-Object { $_.FullPath -ne $null } | Select-Object -ExpandProperty FullPath -Unique
        if ($found.Count -gt 0) {
            [PSCustomObject]@{
                Computer   = $Computer
                FoundItems = ($found -join "; ")
                ItemCount  = $found.Count
                LastScan   = (Get-Item $csv.FullName).LastWriteTime
            }
        }
    } catch {
        Write-Warning "Failed to process $csv : $($_.Exception.Message)"
    }
}

if ($data.Count -eq 0) {
    Write-Host "No findings detected." -ForegroundColor Yellow
    exit
}

$data | Export-Excel -Path $OutputExcel -WorksheetName "DetectedPortable" -BoldTopRow -FreezeTopRow -AutoSize -PassThru | Format-Table -AutoSize

Why Import-Csv? If a path contains commas (e.g., C:\Users\Name, Jr\1C), Get-Content will break your column alignment. Import-Csv handles RFC 4180 escaping natively.

Operational Pitfalls & Troubleshooting

Even well-architected scripts fail in production without addressing these real-world constraints:

IssueRoot CauseFix
Empty CSVs or “Access Denied”DOMAIN COMPUTERS lacks Write permissions on Share/NTFSGrant Change on Share, Modify on NTFS. Use icacls & net share to verify.
Boot stalls / High Disk QueueRecursive scan on 10TB data volumeAdd drive-specific exclusions, reduce -Depth, or schedule via at startup + 5min delay.
False PositivesBackup folders named 1C_Export or dev sandboxesAdd keyword exclusions: Where-Object { $_.Name -notlike "*Backup*" -and $_.Name -notlike "*Dev*" }
Task kills scan mid-wayDefault Scheduled Task timeout (3 days/1 hour)GPO Task → Settings tab → ✅ Do not stop the task if it runs longer than:
GPO not applyingComputer blocked, offline during refresh, or OU filteringRun gpupdate /force, check gpresult /h report.html, verify WMI/Security filtering.

Pro-Tip: GPO Processing Order & Security Filtering

By default, Authenticated Users (which includes DOMAIN COMPUTERS) have Read access. If you’re using Security Filtering, ensure the computer group is explicitly added, or revert to Authenticated Users for simplicity.

Conclusion

SCCM and Intune are excellent for tracking formally deployed software. But shadow IT, portable executables, and legacy remnants live outside those boundaries. This GPO + PowerShell architecture closes that visibility gap without installing agents, licensing third-party scanners, or compromising system performance.

By combining:

  • System-context startup execution
  • Performance-aware scanning logic
  • Centralized CSV collection
  • Robust CSV→Excel aggregation

…you get a lightweight, auditable, and compliant inventory overlay that actually reflects reality.

Next Steps in Production:

  1. Validate on a pilot OU first.
  2. Monitor execution times & I/O counters during initial boots.
  3. Integrate findings into your CM/Intune compliance dashboard via REST API or scheduled import.
  4. Automate remediation (e.g., Remove-Item or quarantine) once patterns are confirmed safe.

Have you encountered portable software blind spots in your environment? What detection layers are missing from your current stack? Drop a comment below.

2 thoughts on “Detecting Portable and Unauthorized Software with PowerShell and GPO”

Leave a Comment