Quellcode durchsuchen

Initial commit: Send-FilesToSftp.ps1 and README

Blance vor 1 Tag
Commit
b2fed6c373
2 geänderte Dateien mit 537 neuen und 0 gelöschten Zeilen
  1. 191 0
      README.md
  2. 346 0
      Send-FilesToSftp.ps1

+ 191 - 0
README.md

@@ -0,0 +1,191 @@
+# Send-FilesToSftp.ps1
+
+PowerShell script to transfer files to an SFTP server with regex filtering, secure credential handling, and logging.
+
+## Prerequisites
+
+**WinSCP .NET Assembly** (`WinSCPnet.dll`) is required. Install using one of these methods:
+
+| Method | Steps |
+|--------|-------|
+| **Drop-in** | Download the [.NET assembly package](https://winscp.net/eng/downloads.php), extract `WinSCPnet.dll` next to the script |
+| **NuGet** | `Install-Package WinSCP -Source nuget.org` |
+| **WinSCP Installer** | Install WinSCP and check the ".NET assembly" option during setup |
+
+The script auto-searches these locations in order:
+1. `-WinScpDllPath` parameter (if provided)
+2. Same directory as the script
+3. `lib\` subdirectory of the script folder
+4. `C:\Program Files (x86)\WinSCP\`
+5. `C:\Program Files\WinSCP\`
+6. NuGet package cache (`~\.nuget\packages\winscp`)
+
+## Quick Start
+
+```powershell
+# Basic usage — prompted for password interactively
+.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
+    -HostName "sftp.example.com" -UserName "uploader"
+```
+
+## Parameters
+
+| Parameter | Required | Default | Description |
+|-----------|----------|---------|-------------|
+| `-LocalPath` | Yes | — | Local source folder to scan |
+| `-RemotePath` | Yes | — | Remote SFTP destination folder |
+| `-HostName` | Yes | — | SFTP server hostname or IP |
+| `-UserName` | Yes | — | SFTP username |
+| `-FileFilter` | No | `.*` | Regex pattern to match filenames |
+| `-Port` | No | `22` | SFTP port |
+| `-Credential` | No | — | `PSCredential` object |
+| `-CredentialFile` | No | — | Path to saved credential XML (see below) |
+| `-KeyFilePath` | No | — | Path to SSH private key (`.ppk`) |
+| `-SshHostKeyFingerprint` | No | — | SSH host key fingerprint for verification |
+| `-Recurse` | No | `false` | Scan subdirectories |
+| `-DeleteAfterTransfer` | No | `false` | Delete local files after successful upload |
+| `-DryRun` | No | `false` | Preview transfers without uploading |
+| `-LogFile` | No | — | Path to log file (logs to console if omitted) |
+| `-WinScpDllPath` | No | — | Explicit path to `WinSCPnet.dll` |
+
+## Authentication
+
+The script supports three auth methods, checked in this order:
+
+### 1. Credential Object (interactive or pipeline)
+```powershell
+$cred = Get-Credential
+.\Send-FilesToSftp.ps1 -LocalPath "C:\data" -RemotePath "/uploads" `
+    -HostName "sftp.example.com" -UserName "brad" -Credential $cred
+```
+
+### 2. Saved Credential File (unattended / scheduled tasks)
+```powershell
+# One-time setup — run as the same user that will run the scheduled task
+Get-Credential | Export-Clixml -Path "C:\secure\sftp_cred.xml"
+
+# Use in script
+.\Send-FilesToSftp.ps1 -LocalPath "C:\data" -RemotePath "/uploads" `
+    -HostName "sftp.example.com" -UserName "svc_upload" `
+    -CredentialFile "C:\secure\sftp_cred.xml"
+```
+
+> **Note:** `Export-Clixml` encrypts credentials using Windows DPAPI, tied to the user account and machine that created the file. Only that same user on that same machine can decrypt it.
+
+### 3. SSH Private Key
+```powershell
+.\Send-FilesToSftp.ps1 -LocalPath "C:\data" -RemotePath "/uploads" `
+    -HostName "sftp.example.com" -UserName "svc_upload" `
+    -KeyFilePath "C:\keys\id_rsa.ppk"
+```
+
+### 4. Interactive Prompt
+If none of the above are provided and no key file is specified, you'll be prompted for a password at runtime.
+
+## File Filtering (Regex)
+
+The `-FileFilter` parameter uses PowerShell regex (case-insensitive by default).
+
+| Filter | Matches |
+|--------|---------|
+| `'\.csv$'` | All CSV files |
+| `'\.xlsx?$'` | `.xls` and `.xlsx` files |
+| `'^report_\d{8}'` | Files starting with `report_` + 8 digits (e.g. `report_20260415.csv`) |
+| `'(?-i)^Data'` | Files starting with `Data` (case-sensitive) |
+| `'badge.*\.xlsx$'` | Excel files with `badge` anywhere in the name |
+| `'\.(csv\|txt)$'` | CSV or TXT files |
+| `'.*'` | Everything (default) |
+
+## Usage Examples
+
+### Transfer all CSVs
+```powershell
+.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
+    -HostName "sftp.example.com" -UserName "uploader" -FileFilter '\.csv$'
+```
+
+### Move files (delete after upload) with logging
+```powershell
+.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
+    -HostName "sftp.example.com" -UserName "svc_upload" `
+    -CredentialFile "C:\secure\cred.xml" `
+    -DeleteAfterTransfer -LogFile "C:\logs\sftp_transfer.log"
+```
+
+### Dry run — preview without transferring
+```powershell
+.\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
+    -HostName "sftp.example.com" -UserName "uploader" -DryRun
+```
+
+### Recursive with subdirectories
+```powershell
+.\Send-FilesToSftp.ps1 -LocalPath "C:\data\projects" -RemotePath "/archive" `
+    -HostName "sftp.example.com" -UserName "uploader" `
+    -Recurse -FileFilter '\.pdf$'
+```
+
+## SSH Host Key Fingerprint
+
+For production use, always provide the host key fingerprint to prevent MITM attacks:
+
+```powershell
+# Get the fingerprint from the server
+ssh-keyscan sftp.example.com | ssh-keygen -lf -
+
+# Use it in the script
+.\Send-FilesToSftp.ps1 -LocalPath "C:\data" -RemotePath "/uploads" `
+    -HostName "sftp.example.com" -UserName "uploader" `
+    -SshHostKeyFingerprint "ssh-rsa 2048 aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99"
+```
+
+Pass `"*"` to accept any key (development/testing only — **not recommended for production**).
+
+## Scheduled Task Setup
+
+To run unattended via Windows Task Scheduler:
+
+1. **Save credentials** as the service account user:
+   ```powershell
+   Get-Credential | Export-Clixml -Path "C:\secure\sftp_cred.xml"
+   ```
+
+2. **Create the scheduled task:**
+   ```powershell
+   $action = New-ScheduledTaskAction `
+       -Execute "powershell.exe" `
+       -Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\scripts\Send-FilesToSftp.ps1" -LocalPath "C:\exports" -RemotePath "/incoming" -HostName "sftp.example.com" -UserName "svc_upload" -CredentialFile "C:\secure\sftp_cred.xml" -FileFilter "\.csv$" -DeleteAfterTransfer -LogFile "C:\logs\sftp.log"'
+
+   $trigger = New-ScheduledTaskTrigger -Daily -At "2:00AM"
+
+   Register-ScheduledTask -TaskName "SFTP Upload" `
+       -Action $action -Trigger $trigger `
+       -User "DOMAIN\svc_upload" -Password "****" `
+       -RunLevel Highest
+   ```
+
+## Exit Codes
+
+| Code | Meaning |
+|------|---------|
+| `0` | All files transferred (or no files matched filter) |
+| `1` | One or more failures occurred |
+
+## Log Output
+
+Logs include timestamps and severity levels. Sample output:
+
+```
+[2026-04-16 14:30:01] [INFO] ═══ SFTP Transfer Starting ═══
+[2026-04-16 14:30:01] [INFO] Local path  : C:\exports
+[2026-04-16 14:30:01] [INFO] Remote path : /incoming
+[2026-04-16 14:30:01] [INFO] File filter : \.csv$
+[2026-04-16 14:30:01] [INFO] Found 3 file(s) matching filter
+[2026-04-16 14:30:02] [SUCCESS] Connected to sftp.example.com
+[2026-04-16 14:30:03] [SUCCESS] Transferred: report_20260415.csv → /incoming/report_20260415.csv  (42.3 KB)
+[2026-04-16 14:30:03] [SUCCESS] Transferred: badges_export.csv → /incoming/badges_export.csv  (18.1 KB)
+[2026-04-16 14:30:04] [SUCCESS] Transferred: access_log.csv → /incoming/access_log.csv  (7.8 KB)
+[2026-04-16 14:30:04] [INFO] ═══ Transfer Complete ═══
+[2026-04-16 14:30:04] [INFO]   Succeeded : 3
+[2026-04-16 14:30:04] [INFO]   Mode      : COPY (source files retained)
+```

+ 346 - 0
Send-FilesToSftp.ps1

@@ -0,0 +1,346 @@
+<#
+.SYNOPSIS
+    Transfers files to an SFTP server with filtering and logging.
+
+.DESCRIPTION
+    Moves or copies files from a local folder to a remote SFTP destination.
+    Supports regex/wildcard file filtering, recursive scanning, and secure
+    credential storage. Uses the WinSCP .NET assembly for SFTP transport.
+
+.PARAMETER LocalPath
+    Local source folder to scan for files.
+
+.PARAMETER RemotePath
+    Remote SFTP destination folder (e.g. /uploads/incoming).
+
+.PARAMETER FileFilter
+    Regex pattern to match filenames (e.g. '\.csv$' or '^report_\d{8}').
+    Default: '.*' (all files).
+
+.PARAMETER HostName
+    SFTP server hostname or IP.
+
+.PARAMETER Port
+    SFTP port. Default: 22.
+
+.PARAMETER UserName
+    SFTP username.
+
+.PARAMETER SshHostKeyFingerprint
+    SSH host key fingerprint for server verification.
+    Use "ssh-rsa 2048 xx:xx:xx..." format, or pass "*" to accept any (NOT recommended for production).
+
+.PARAMETER Credential
+    PSCredential object. If omitted, you'll be prompted interactively.
+
+.PARAMETER CredentialFile
+    Path to a saved credential file (Export-Clixml). For scheduled/unattended runs.
+    Create one with: Get-Credential | Export-Clixml -Path "C:\secure\sftp_cred.xml"
+
+.PARAMETER KeyFilePath
+    Path to a private key file for key-based auth (optional).
+
+.PARAMETER Recurse
+    Scan subdirectories in LocalPath.
+
+.PARAMETER DeleteAfterTransfer
+    Delete local files after successful upload (move behavior).
+
+.PARAMETER DryRun
+    Show what would be transferred without actually uploading.
+
+.PARAMETER LogFile
+    Path to a log file. If omitted, logs to console only.
+
+.PARAMETER WinScpDllPath
+    Path to WinSCPnet.dll. Default: looks in script directory, then common install paths.
+
+.EXAMPLE
+    # Interactive - transfer all CSVs
+    .\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
+        -HostName "sftp.example.com" -UserName "uploader" -FileFilter '\.csv$'
+
+.EXAMPLE
+    # Unattended with saved credentials and move behavior
+    .\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
+        -HostName "sftp.example.com" -UserName "uploader" `
+        -CredentialFile "C:\secure\sftp_cred.xml" `
+        -FileFilter '^report_\d{8}' -DeleteAfterTransfer -LogFile "C:\logs\sftp.log"
+
+.EXAMPLE
+    # Dry run to preview what would transfer
+    .\Send-FilesToSftp.ps1 -LocalPath "C:\exports" -RemotePath "/incoming" `
+        -HostName "sftp.example.com" -UserName "uploader" -DryRun
+
+.EXAMPLE
+    # Key-based auth, recursive scan
+    .\Send-FilesToSftp.ps1 -LocalPath "C:\data" -RemotePath "/archive" `
+        -HostName "sftp.example.com" -UserName "svcaccount" `
+        -KeyFilePath "C:\keys\id_rsa.ppk" -Recurse
+#>
+
+[CmdletBinding(SupportsShouldProcess)]
+param(
+    [Parameter(Mandatory)]
+    [ValidateScript({ Test-Path $_ -PathType Container })]
+    [string]$LocalPath,
+
+    [Parameter(Mandatory)]
+    [string]$RemotePath,
+
+    [string]$FileFilter = '.*',
+
+    [Parameter(Mandatory)]
+    [string]$HostName,
+
+    [int]$Port = 22,
+
+    [Parameter(Mandatory)]
+    [string]$UserName,
+
+    [string]$SshHostKeyFingerprint = $null,
+
+    [PSCredential]$Credential,
+
+    [string]$CredentialFile,
+
+    [string]$KeyFilePath,
+
+    [switch]$Recurse,
+
+    [switch]$DeleteAfterTransfer,
+
+    [switch]$DryRun,
+
+    [string]$LogFile,
+
+    [string]$WinScpDllPath
+)
+
+# ── Logging ──────────────────────────────────────────────────────────────────
+
+function Write-Log {
+    param(
+        [string]$Message,
+        [ValidateSet('INFO','WARN','ERROR','SUCCESS')]
+        [string]$Level = 'INFO'
+    )
+
+    $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
+    $entry     = "[$timestamp] [$Level] $Message"
+
+    switch ($Level) {
+        'ERROR'   { Write-Host $entry -ForegroundColor Red }
+        'WARN'    { Write-Host $entry -ForegroundColor Yellow }
+        'SUCCESS' { Write-Host $entry -ForegroundColor Green }
+        default   { Write-Host $entry }
+    }
+
+    if ($LogFile) {
+        $entry | Out-File -FilePath $LogFile -Append -Encoding utf8
+    }
+}
+
+# ── Locate WinSCP .NET Assembly ──────────────────────────────────────────────
+
+function Find-WinScpDll {
+    $searchPaths = @(
+        $WinScpDllPath
+        (Join-Path $PSScriptRoot 'WinSCPnet.dll')
+        (Join-Path $PSScriptRoot 'lib\WinSCPnet.dll')
+        'C:\Program Files (x86)\WinSCP\WinSCPnet.dll'
+        'C:\Program Files\WinSCP\WinSCPnet.dll'
+    ) | Where-Object { $_ }
+
+    foreach ($path in $searchPaths) {
+        if (Test-Path $path) {
+            return $path
+        }
+    }
+
+    # Try NuGet package in common locations
+    $nugetPath = Get-ChildItem -Path "$env:USERPROFILE\.nuget\packages\winscp" -Filter 'WinSCPnet.dll' -Recurse -ErrorAction SilentlyContinue |
+        Sort-Object LastWriteTime -Descending | Select-Object -First 1
+    if ($nugetPath) { return $nugetPath.FullName }
+
+    return $null
+}
+
+# ── Main ─────────────────────────────────────────────────────────────────────
+
+try {
+    Write-Log "═══ SFTP Transfer Starting ═══"
+    Write-Log "Local path  : $LocalPath"
+    Write-Log "Remote path : $RemotePath"
+    Write-Log "File filter : $FileFilter"
+    Write-Log "Host        : ${HostName}:${Port}"
+    if ($DryRun) { Write-Log "*** DRY RUN MODE - No files will be transferred ***" -Level WARN }
+
+    # ── Find and load WinSCP ─────────────────────────────────────────────
+    $dllPath = Find-WinScpDll
+    if (-not $dllPath) {
+        Write-Log "WinSCPnet.dll not found. Install options:" -Level ERROR
+        Write-Log "  1) Install-Package WinSCP -Source nuget.org" -Level ERROR
+        Write-Log "  2) Download from https://winscp.net/eng/downloads.php (.NET assembly)" -Level ERROR
+        Write-Log "  3) Place WinSCPnet.dll in the same folder as this script" -Level ERROR
+        exit 1
+    }
+    Add-Type -Path $dllPath
+    Write-Log "Loaded WinSCP from: $dllPath"
+
+    # ── Resolve credentials ──────────────────────────────────────────────
+    $password = $null
+
+    if ($Credential) {
+        $password = $Credential.GetNetworkCredential().Password
+    }
+    elseif ($CredentialFile) {
+        if (-not (Test-Path $CredentialFile)) {
+            Write-Log "Credential file not found: $CredentialFile" -Level ERROR
+            exit 1
+        }
+        $savedCred = Import-Clixml -Path $CredentialFile
+        $password   = $savedCred.GetNetworkCredential().Password
+        Write-Log "Loaded credentials from file"
+    }
+    elseif (-not $KeyFilePath) {
+        $promptCred = Get-Credential -UserName $UserName -Message "Enter SFTP password for $HostName"
+        if (-not $promptCred) {
+            Write-Log "No credentials provided. Aborting." -Level ERROR
+            exit 1
+        }
+        $password = $promptCred.GetNetworkCredential().Password
+    }
+
+    # ── Collect files ────────────────────────────────────────────────────
+    $gciParams = @{ Path = $LocalPath; File = $true }
+    if ($Recurse) { $gciParams['Recurse'] = $true }
+
+    $allFiles = Get-ChildItem @gciParams | Where-Object { $_.Name -match $FileFilter }
+
+    if (-not $allFiles -or $allFiles.Count -eq 0) {
+        Write-Log "No files matched filter '$FileFilter' in $LocalPath" -Level WARN
+        exit 0
+    }
+
+    Write-Log "Found $($allFiles.Count) file(s) matching filter"
+
+    if ($DryRun) {
+        Write-Log "Files that would be transferred:" -Level INFO
+        foreach ($f in $allFiles) {
+            $relativePath = $f.FullName.Substring($LocalPath.TrimEnd('\').Length)
+            $remoteDest   = ($RemotePath.TrimEnd('/') + $relativePath) -replace '\\', '/'
+            $sizeKB       = [math]::Round($f.Length / 1KB, 1)
+            Write-Log "  $($f.FullName) → $remoteDest  (${sizeKB} KB)"
+        }
+        Write-Log "DRY RUN complete. $($allFiles.Count) file(s) would be transferred." -Level SUCCESS
+        exit 0
+    }
+
+    # ── Configure session ────────────────────────────────────────────────
+    $sessionOptions = New-Object WinSCP.SessionOptions -Property @{
+        Protocol = [WinSCP.Protocol]::Sftp
+        HostName = $HostName
+        PortNumber = $Port
+        UserName = $UserName
+    }
+
+    if ($password) {
+        $sessionOptions.Password = $password
+    }
+
+    if ($KeyFilePath) {
+        if (-not (Test-Path $KeyFilePath)) {
+            Write-Log "Private key file not found: $KeyFilePath" -Level ERROR
+            exit 1
+        }
+        $sessionOptions.SshPrivateKeyPath = $KeyFilePath
+        Write-Log "Using key-based auth: $KeyFilePath"
+    }
+
+    if ($SshHostKeyFingerprint) {
+        if ($SshHostKeyFingerprint -eq '*') {
+            Write-Log "Accepting any SSH host key — NOT SAFE FOR PRODUCTION" -Level WARN
+            $sessionOptions.GiveUpSecurityAndAcceptAnySshHostKey = $true
+        }
+        else {
+            $sessionOptions.SshHostKeyFingerprint = $SshHostKeyFingerprint
+        }
+    }
+    else {
+        Write-Log "No SSH host key fingerprint provided — accepting any key" -Level WARN
+        Write-Log "  Get your fingerprint with: ssh-keyscan $HostName | ssh-keygen -lf -" -Level WARN
+        $sessionOptions.GiveUpSecurityAndAcceptAnySshHostKey = $true
+    }
+
+    # ── Open session and transfer ────────────────────────────────────────
+    $session = New-Object WinSCP.Session
+    $successCount = 0
+    $failCount    = 0
+
+    try {
+        $session.Open($sessionOptions)
+        Write-Log "Connected to $HostName" -Level SUCCESS
+
+        # Ensure remote directory exists
+        if (-not $session.FileExists($RemotePath)) {
+            Write-Log "Creating remote directory: $RemotePath"
+            $session.CreateDirectory($RemotePath)
+        }
+
+        foreach ($file in $allFiles) {
+            $relativePath = ''
+            if ($Recurse) {
+                $relativePath = $file.DirectoryName.Substring($LocalPath.TrimEnd('\').Length) -replace '\\', '/'
+            }
+
+            $targetDir  = $RemotePath.TrimEnd('/') + $relativePath
+            $targetPath = "$targetDir/$($file.Name)"
+
+            try {
+                # Ensure subdirectory exists on remote when recursing
+                if ($Recurse -and $relativePath -and (-not $session.FileExists($targetDir))) {
+                    $session.CreateDirectory($targetDir)
+                    Write-Log "Created remote dir: $targetDir"
+                }
+
+                $transferOptions = New-Object WinSCP.TransferOptions
+                $transferOptions.TransferMode = [WinSCP.TransferMode]::Binary
+
+                $result = $session.PutFiles($file.FullName, $targetPath, $DeleteAfterTransfer, $transferOptions)
+                $result.Check()
+
+                $sizeKB = [math]::Round($file.Length / 1KB, 1)
+                Write-Log "Transferred: $($file.Name) → $targetPath  (${sizeKB} KB)" -Level SUCCESS
+                $successCount++
+            }
+            catch {
+                Write-Log "FAILED: $($file.Name) — $($_.Exception.Message)" -Level ERROR
+                $failCount++
+            }
+        }
+    }
+    finally {
+        if ($session) { $session.Dispose() }
+    }
+
+    # ── Summary ──────────────────────────────────────────────────────────
+    Write-Log "═══ Transfer Complete ═══"
+    Write-Log "  Succeeded : $successCount"
+    if ($failCount -gt 0) {
+        Write-Log "  Failed    : $failCount" -Level ERROR
+    }
+    if ($DeleteAfterTransfer) {
+        Write-Log "  Mode      : MOVE (source files deleted on success)"
+    }
+    else {
+        Write-Log "  Mode      : COPY (source files retained)"
+    }
+
+    if ($failCount -gt 0) { exit 1 }
+}
+catch {
+    Write-Log "Fatal error: $($_.Exception.Message)" -Level ERROR
+    Write-Log $_.ScriptStackTrace -Level ERROR
+    exit 1
+}