尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用WinSCP与PowerShell实现SFTP目录清单自动化审计方案

用WinSCP与PowerShell实现SFTP目录清单自动化审计方案 我最早意识到必须给 SFTP 目录做清单这件事是在一个周四的下午。客户打来电话说某个业务方的数据文件没按时出现在他们的抓取目录里两边运维互相指认最后发现文件根本没传上去但谁也没法证明没传上去这个事实发生在哪一端。从那之后我养成了一个习惯凡是经手的 SFTP 对接项目不管对方有没有要求我都会用 WinSCP 和 PowerShell 写一套自动化的目录清单生成脚本把远端有什么文件、文件多大、什么时间出现的这类信息按天落盘。这篇文章就把这套方案的完整思路和代码细节写出来给同样需要给 SFTP 操作留痕、做审计的同行参考。这套方案解决的实质问题有三个第一SFTP 目录内容可视化把看不见的远端目录变成可阅读、可检索的文本记录第二审计留痕每次执行的结果、登录的时间、操作的路径都有迹可循第三异常发现前置清单里文件数量、大小的突变本身就是一种监控信号。整个过程不需要额外安装大型平台一台 Windows 机器加上 WinSCP 命令行工具和 PowerShell 就够了适合中小团队和单机运维场景直接复用。1. 为什么我最终选择 WinSCP PowerShell 这套组合1.1 图形界面能做事但不能留痕如果没有审计需求那直接用 WinSCP 图形界面登录服务器浏览目录、拖拽文件效率其实很高。但图形界面的问题在于操作过程非常私人化。你看到了什么文件、什么时候看的、看了哪个目录这些信息不会自动落在文本里。真要追溯起来只能靠人回忆或者靠远端服务器的连接日志两个都不靠谱。我最初也试过用 WinSCP 自带的会话日志功能就是那个可以把操作过程记录到文件里的选项。但这个日志记录的是传输动作不是目录内容的结构化描述。换句话说它能告诉你某时间连接了服务器却没法直接告诉你服务器 /upload/archive 目录下所有文件的清单。1.2 为什么不用其他 SFTP 库市面上可以操作 SFTP 的方案不少Python 有 paramiko.NET 有 SSH.NETPowerShell 生态里也有 Posh-SSH 模块。我都用过说下最终选型的理由paramiko功能完整文档也算清晰但生产环境机器上不一定有 Python 环境为了一个目录清单任务去装 Python 解释器在客户机器上往往要过一层审批。Posh-SSHPowerShell 原生模块用起来挺顺手但它在内部封装了较多依赖而且在部分 Windows Server 上需要额外配置模块信任策略报错信息对新手不友好。WinSCP 的 .NET 程序集WinSCP 本身就是成熟的 SFTP/SCP 客户端它的 .NET 程序集把这个客户端的全部能力暴露给 PowerShell 调用。这意味着你不需要在目标机器上安装额外运行时只需要 WinSCP 工具本身和一个能被 Add-Type 加载的 DLL 文件。对我来说WinSCP PowerShell 这套组合最大的优势是操作系统自带的东西一个小工具依赖面窄可解释性强。客户那边机器上只要装了 WinSCP或者我把它放进脚本同目录PowerShell 是 Windows 自带的这就够了。从后续维护的角度看这套方案出问题时排查链路也短——脚本、DLL、远程服务器就这三个环节。当然如果你手上的服务器数量特别多动辄几十台上百台那还是应该上正规的堡垒机或者集中式跳板机平台目录清单只是辅助手段。但对几台到十几台服务器的规模来说这套轻量方案完全够用而且可控性非常高。2. 前置准备WinSCP .NET 程序集的获取与注册2.1 程序集从哪来、怎么加载WinSCP 在安装目录下会自带一个 .NET 程序集文件叫WinSCPnet.dll。以默认安装路径为例它位于C:\Program Files (x86)\WinSCP\WinSCPnet.dll这个 DLL 就是我们在 PowerShell 里调用的核心库。如果安装时用的是自定义目录可以去安装目录里找或者直接从 WinSCP 官网下载兼容包里面会包含 WinSCP.exe 和 WinSCPnet.dll。在 PowerShell 里加载它的方法很直接Add-Type -Path C:\Program Files (x86)\WinSCP\WinSCPnet.dll这里有个容易踩的坑如果你在 64 位 PowerShell 里加载 32 位程序集反之亦然有时候会报错或者行为异常。我建议统一使用x64 版本的 PowerShell并确认你安装的 WinSCP 也是 64 位版本。两边架构不一致时Add-Type虽然通常能成功但在调用某些方法时可能抛出奇怪的异常。2.2 凭据管理的安全边界脚本里要登录 SFTP就绕不开凭据。我的建议是不要把密码明文写在脚本里。尤其当脚本要通过任务计划程序自动运行时脚本会以普通文本形式躺在磁盘上任何有权限读取该文件的账号都能看到密码。推荐的做法是用 Windows 凭据管理器Credential Manager保存密码脚本运行时从中读取# 先从凭据管理器读取账号口令 $cred Get-StoredCredential -Target MySFTPServer # 如果机器上没装 CredentialManager 模块可以用 cmdkey 配合读取 # 更简单的临时方案把凭据放在脚本同目录的加密 XML 中 $cred Import-Clixml -Path .\cred.xmlExport-Clixml在 Windows 上有个特性导出的 XML 文件默认用当前用户的 DPAPI 密钥加密同一个用户在同一台机器上才能解密。虽然它不能防止更高权限的账号读取但至少避免了明文密码直接躺在脚本里这种最糟糕的情况。因为创建脚本并执行任务的账号通常是同一个专用服务账号所以这个方案在日常运维中够用且易于实施。2.3 配置文件分离我通常会建立一个固定的目录结构来组织这套脚本C:\Scripts\SFTPAudit\ ├── config.json # 服务器地址、路径、账号等配置 ├── cred.xml # 加密后的凭据 ├── sftp-audit.ps1 # 主脚本 ├── WinSCPnet.dll # 拷贝过来的程序集 └── logs\ # 执行日志把配置、凭据、脚本分离好处是换服务器时不用动代码改配置就行。config.json里存这样的内容{ HostName: sftp.example.com, Port: 22, UserName: audit_bot, RemoteBasePath: /upload/archive, LocalOutputDir: C:\\Scripts\\SFTPAudit\\reports }需要注意用户名也可以放配置里但密码绝对不要放。密码只放在加密的cred.xml里运行时导入。3. 核心脚本从建立会话到生成清单全流程3.1 连接会话的建立逻辑用 WinSCP .NET 程序集连接 SFTP核心是三个类SessionOptions、Session和TransferOptions。连接过程大致是这个模式$sessionOptions New-Object WinSCP.SessionOptions -Property { Protocol [WinSCP.Protocol]::Sftp HostName $config.HostName PortNumber $config.Port UserName $config.UserName Password $cred.Password SshHostKeyFingerprint $config.SshHostKeyFingerprint GiveUpSecurityAndAcceptAnySshHostKey $false }有个参数我想单独拿出来说SshHostKeyFingerprint。它默认是空的如果你不设置指纹第一次连接时会遇到主机密钥校验提示。更关键的是如果你图省事把GiveUpSecurityAndAcceptAnySshHostKey设为$true脚本虽然能跑通但等于放弃了中间人防护这在审计场景下是完全不可接受的——审计的前提就是连接过程本身是可信的。正确做法是在首次交互连接时获取服务器指纹$session New-Object WinSCP.Session $session.Open($sessionOptions) $session.SessionInfo.SshHostKeyFingerprint $session.Close()把指纹填到配置文件里以后再连接就不会再提示同时也固定了服务器的身份。建立会话时还有个细节SFTP 协议版本。WinSCP 默认会尝试协商一个双方都支持的最高版本一般不需要手动指定。但如果日志里看到路径解析相关的异常可以考虑把协议版本固定下来$sessionOptions.SftpVersion 3 # 有些老旧的 SFTP 服务器只支持 V33.2 目录遍历与文件元数据提取会话建立后获取目录清单的核心方法是ListDirectory$directory $session.ListDirectory($remotePath) foreach ($fileInfo in $directory.Files) { # $fileInfo 包含 Name, Length, LastWriteTime, IsDirectory 等属性 }默认的ListDirectory只列一层目录不会递归。如果你的目标目录结构是二层的比如按日期/按商家ID/文件我通常自己写递归函数处理。function Get-RemoteFileList { param( [WinSCP.Session]$session, [string]$path ) $result () $directory $session.ListDirectory($path) foreach ($entry in $directory.Files) { if ($entry.Name -eq . -or $entry.Name -eq ..) { continue } if ($entry.IsDirectory) { $result Get-RemoteFileList -session $session -path ($path / $entry.Name) } else { $result [PSCustomObject]{ FullPath $path / $entry.Name Name $entry.Name Length $entry.Length LastWriteTime $entry.LastWriteTime IsDirectory $false } } } return $result }这段递归有几个地方需要解释。一是Name为.和..的项必须跳过否则会无限递归二是路径拼接用/无论目标服务器是 Windows 还是 LinuxSFTP 协议层统一用/作为分隔符。对于大目录这个递归函数每次ListDirectory都会产生一次远程调用如果目录层级深、子目录多执行时间会比较长。我在一次实际运维中发现某个目录下有近两百个子目录完整遍历耗时约 3 分 20 秒。这个时间不算短但对于定时任务来说可以接受。3.3 可选计算文件校验和目录清单如果只有文件名、大小、时间审计价值已经能打七八十分。但如果能做到末次审计之后文件没有被篡改就需要引入哈希校验。WinSCP 的Session.CalculateFileChecksum方法支持这个需求$checksum $session.CalculateFileChecksum(sha-256, $remoteFilePath)需要提醒的是CalculateFileChecksum是通过 SFTP 扩展命令实现的不是所有 SFTP 服务器都支持。我在内网测试时OpenSSH 的 SFTP 子系统是支持的但有些商用 FTP 网关的 SFTP 实现不一定支持。所以这个能力要做成可选开关配置里加一个布尔值支持就开不支持就跳过不能让校验失败导致整个任务失败。为每个文件都单独计算 SHA-256 的代价是明显的假设 1GB 的文件服务端需要完整读取并计算。如果目录里文件特别多总耗时可能从几分钟膨胀到几十分钟。我的建议是根据业务需要才开如果目录清单本身只用来做文件追踪和数量核对那就关闭校验和如果对接的是金融、政务类项目再考虑开启并保留哈希记录。3.4 输出清单格式CSV 与时间戳命名清单文件我统一用 CSV 格式输出字段固定为FilePath,FileSize,LastModifiedUtc,Sha256,ScanStartUtcCSV 的好处是后续可以用 Excel、PowerShell、Python 任意处理。文件名带上执行时间的时间戳比如sftp-inventory_20250215_083000.csv这样每个周期的清单互不覆盖天然形成历史轨迹。$timestamp (Get-Date).ToString(yyyyMMdd_HHmmss) $reportPath Join-Path $config.LocalOutputDir sftp-inventory_$timestamp.csv $fileList | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8这里有个小细节Export-Csv在 Windows PowerShell 5.1 下默认编码是ASCII或UTF-8 without BOM如果文件名里包含非 ASCII 字符最好显式指定-Encoding UTF8否则 Excel 打开时中文可能乱码。PowerShell 7 下默认utf8NoBOM行为有所不同。4. 让清单可审计的四个关键设计有了一份 CSV 文件只是第一步。真正能让这份清单在审计场景下站得住脚还需要四个设计。4.1 审计日志单独记录连接与执行状态清单文件记录的是远端有什么但审计还需要记录脚本是否成功执行、有没有报警、连接过程是否正常。这部分信息我写到独立的日志文件里而不是混在数据清单中。$logEntry [{0}] INFO Session opened to {1}. Remote path: {2} -f (Get-Date -Format yyyy-MM-dd HH:mm:ss), $config.HostName, $config.RemoteBasePath Add-Content -Path (Join-Path $logsDir audit_$(Get-Date -Format yyyyMMdd).log) -Value $logEntry日志按天切割方便回顾。日志开头写入脚本版本号这样如果后来脚本逻辑变了也可以定位某一天的清单是用哪个版本生成的。4.2 基线比对与差异报告如果你每周跑一次清单那这周比上周多了哪些文件、少了哪些文件就是最直接的审计信号。差异对比用 PowerShell 可以轻松实现$previousCsv Get-ChildItem $config.LocalOutputDir -Filter sftp-inventory_*.csv | Sort-Object LastWriteTime -Descending | Select-Object -First 2 | Select-Object -Last 1 if ($previousCsv) { $prevFiles Import-Csv $previousCsv.FullName $currFiles Import-Csv $reportPath $newFiles $currFiles | Where-Object { $_.FilePath -notin $prevFiles.FilePath } $goneFiles $prevFiles | Where-Object { $_.FilePath -notin $currFiles.FilePath } if ($newFiles) { Add-Content $logPath New files detected: $newFiles | ForEach-Object { Add-Content $logPath $($_.FilePath) } } if ($goneFiles) { Add-Content $logPath Missing files detected: $goneFiles | ForEach-Object { Add-Content $logPath - $($_.FilePath) } } }注意这里我用的是取最近的两个 CSV 文件做对比而不是让脚本自己维护状态文件。原因很简单CSV 本身就是历史记录不需要额外引入状态文件减少一个环节就减少一份出错的可能性。4.3 异常自动通知这份清单如果只是安静地躺在服务器上异常发现仍然是被动的。我一般会把脚本包在 try-catch 里并在此基础上接入邮件通知。最简单的通知方式catch { $errorMessage $_.Exception.Message # 写入日志 Write-EventLog -LogName Application -Source SFTPAudit -EntryType Error -EventId 1001 -Message $errorMessage -ErrorAction SilentlyContinue # 发邮件可选 }邮件通知容易遭到反垃圾机制干扰所以我不建议把邮件作为唯一通知渠道。实际生产环境中把事件写入 Windows 事件日志再配合一个监控服务去抓事件日志通常更稳妥。4.4 清单文件的只读归档策略生成的 CSV 在后续运行中不应被改写。我的做法是生成后立即把文件属性设置为只读并移动到按月份归档的子目录Set-ItemProperty -Path $reportPath -Name IsReadOnly -Value $true Move-Item -Path $reportPath -Destination $archiveDir这样既防止误删、误改也方便按月份快速回溯。5. 自动化落地任务计划程序与日常维护5.1 脚本的参数化设计要让脚本能在任务计划程序里无人值守运行脚本本身必须支持参数传入或者自动读取配置文件。我用的是配置文件优先策略同时允许命令行参数覆盖param( [string]$ConfigPath .\config.json ) $config Get-Content $ConfigPath -Raw | ConvertFrom-Json5.2 任务计划程序的三个配置要点在 Windows 任务计划程序里创建任务时有三个地方特别容易出错第一执行策略。脚本必须用-ExecutionPolicy Bypass调用否则可能被 PowerShell 执行策略拦截。但要注意不是让你把Bypass写进系统级策略而是在任务计划程序的操作里显式指定程序或脚本: powershell.exe 添加参数: -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\SFTPAudit\sftp-audit.ps1 -ConfigPath C:\Scripts\SFTPAudit\config.json如果使用 PowerShell 7就把程序或脚本改成pwsh.exe参数不变。第二运行账号。建议创建一个权限受限的专用账号来运行这个任务而不是用管理员账号。因为账号权限越受限一旦脚本被篡改扩散面越小。专用账号只需要以下权限读取配置文件和解密凭据所需的用户配置文件权限对报告目录有写入权限出站访问目标 SFTP 服务器 22 端口的权限。第三任务频率。目录清单的频率取决于业务变化速度。如果目录里的文件是按天生成的建议每小时或每半小时跑一次如果是按小时生成的建议每 10 分钟跑一次。太频繁会让 SFTP 服务器产生大量连接记录反而不利于审计人员从日志中追溯有效信息。我在实践中常用的频率是每天 2 次中午 12 点一次晚上 22 点一次。5.3 与日常运维配套的检查脚本稳定运行一段时间之后我会在周检查时做三件事确认日志里没有 ERROR 级条目确认最新 CSV 的时间戳是预期的时间随机抽 3 个远端文件手工比对清单中记录的 Size 和 LastModifiedTime。这三件事加起来不超过 10 分钟但能在问题酿成大错之前暴露出来。6. 完整脚本示例与使用说明为了让你更容易落地我把上面各节讲到的内容整合成一个完整可运行的脚本。这个脚本按 连接 → 遍历 → 生成 CSV → 写日志 → 可选校验和 的顺序执行同时做了最小化的健壮性处理。[CmdletBinding()] param( [string]$ConfigPath .\config.json ) $ErrorActionPreference Stop # 读取配置 if (-not (Test-Path $ConfigPath)) { throw Config file not found: $ConfigPath } $config Get-Content $ConfigPath -Raw | ConvertFrom-Json $scriptDir Split-Path -Parent $MyInvocation.MyCommand.Path $logsDir Join-Path $scriptDir logs $reportsDir Join-Path $scriptDir reports $archiveDir Join-Path $reportsDir archive New-Item -ItemType Directory -Path $logsDir, $reportsDir, $archiveDir -Force | Out-Null $logFile Join-Path $logsDir audit_$(Get-Date -Format yyyyMMdd).log function Write-Log { param([string]$Level, [string]$Message) $line [{0}] [{1}] {2} -f (Get-Date -Format yyyy-MM-dd HH:mm:ss), $Level, $Message Add-Content -Path $logFile -Value $line } Write-Log INFO SFTP audit script started. Config: $ConfigPath try { # 加载 WinSCP .NET 程序集 $winscpDll Join-Path $scriptDir WinSCPnet.dll Add-Type -Path $winscpDll # 读取凭据cred.xml 由 Export-Clixml 生成只对当前用户可解密 $cred Import-Clixml -Path (Join-Path $scriptDir cred.xml) # 构建连接参数 $sessionOptions New-Object WinSCP.SessionOptions -Property { Protocol [WinSCP.Protocol]::Sftp HostName $config.HostName PortNumber $config.Port UserName $config.UserName Password $cred.Password SshHostKeyFingerprint $config.SshHostKeyFingerprint GiveUpSecurityAndAcceptAnySshHostKey $false } $session New-Object WinSCP.Session try { $session.Open($sessionOptions) Write-Log INFO SFTP session opened. Base path: $($config.RemoteBasePath) # 递归遍历远端目录 function Get-RemoteFileList { param( [WinSCP.Session]$session, [string]$path ) $result () $directory $session.ListDirectory($path) foreach ($entry in $directory.Files) { if ($entry.Name -eq . -or $entry.Name -eq ..) { continue } if ($entry.IsDirectory) { $result Get-RemoteFileList -session $session -path ($path / $entry.Name) } else { $item [PSCustomObject]{ FilePath $path / $entry.Name FileSize $entry.Length LastModifiedUtc $entry.LastWriteTime.ToUniversalTime().ToString(o) Sha256 HasChecksum $false } if ($config.EnableChecksum -eq $true) { try { $item.Sha256 $session.CalculateFileChecksum(sha-256, $item.FilePath) $item.HasChecksum $true } catch { Write-Log WARN Checksum calculation failed for $($item.FilePath): $($_.Exception.Message) } } $result $item } } return $result } $fileList Get-RemoteFileList -session $session -path $config.RemoteBasePath if (-not $fileList -or $fileList.Count -eq 0) { Write-Log WARN No files found under $($config.RemoteBasePath) } else { Write-Log INFO Collected $($fileList.Count) files under $($config.RemoteBasePath) } # 保存清单 $timestamp Get-Date -Format yyyyMMdd_HHmmss $reportPath Join-Path $reportsDir sftp-inventory_$timestamp.csv $fileList | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8 Set-ItemProperty -Path $reportPath -Name IsReadOnly -Value $true Write-Log INFO Inventory saved to $reportPath # 与上次清单做差异对比 $previousCsv Get-ChildItem $reportsDir -Filter sftp-inventory_*.csv | Sort-Object LastWriteTime -Descending | Select-Object -First 2 | Select-Object -Last 1 if ($previousCsv) { $prevFiles (Import-Csv $previousCsv.FullName) $currEntries ($fileList) $newFiles $currEntries | Where-Object { $_.FilePath -notin $prevFiles.FilePath } $goneFiles $prevFiles | Where-Object { $_.FilePath -notin $currEntries.FilePath } if ($newFiles) { Write-Log INFO New files detected: $($newFiles.Count) $newFiles | ForEach-Object { Write-Log INFO $($_.FilePath) } } if ($goneFiles) { Write-Log INFO Missing files detected: $($goneFiles.Count) $goneFiles | ForEach-Object { Write-Log INFO - $($_.FilePath) } } if (-not $newFiles -and -not $goneFiles) { Write-Log INFO No differences compared to $($previousCsv.Name) } } } finally { $session.Dispose() Write-Log INFO SFTP session disposed } } catch { $errMsg $_.Exception.ToString() Write-Log ERROR $errMsg throw } Write-Log INFO SFTP audit script completed如果你不需要校验和把config.json里EnableChecksum设为false即可。这样文件遍历时就不会触发CalculateFileChecksum速度会快很多。6. 实际运行效果与问题排查经验6.1 一次真实运行的输出示例脚本跑完后会生成类似这样的 CSVFilePath,FileSize,LastModifiedUtc,Sha256,HasChecksum /upload/archive/20250214/daily_report_001.csv,20480,2025-02-14T04:30:11.0000000Z,abcd...,True /upload/archive/20250214/daily_report_002.csv,16896,2025-02-14T04:31:02.0000000Z,ef12...,True /upload/archive/20250215/daily_report_001.csv,20992,2025-02-15T04:28:45.0000000Z,34ab...,True这份 CSV 可以直接共享给业务方也可以拿来做数据核对。比起截图这显然正规得多。6.2 常见的三个典型故障及对策故障一连接超时。表现为脚本卡在Open()调用上迟迟不返回。通常原因是防火墙限制了出站 22 端口。排查手段是先在命令行手动测试Test-NetConnection sftp.example.com -Port 22故障二指纹校验失败。SFTP 服务器的 SSH 密钥轮换后原来记录的指纹会失配。错误信息类似于Host key does not match。解决办法是重新获取指纹并更新配置文件。这也说明审计方案起作用了——至少你没有在指纹变化后无感连接。故障三CSV 内容包含大量空行或 UTF-8 乱码。这通常和 PowerShell 版本有关。如果你在用 Windows PowerShell 5.1Export-Csv -Encoding UTF8会生成带 BOM 的 UTF-8Excel 能正常打开PowerShell 7 默认utf8NoBOM老版本 Excel 可能识别为 ANSI。如果客户那边要用 Excel 打开可以在配置里增加一个CsvEncoding参数根据运行环境手动指定UTF8或Default。6.3 和 OpenSSH/SFTP 服务器配合的注意事项如果你的 SFTP 服务器是 Windows 上的 OpenSSH需要注意一个细节OpenSSH 的 SFTP 子系统默认把用户锁定在某个主目录里通过ChrootDirectory或内部 SFTP 设置这时脚本里配置的RemoteBasePath是相对于该用户主目录的路径不是服务器上的绝对路径。比如配置/upload/archive实际对应服务器上的C:\ProgramData\ssh\...\upload\archive之类的位置权限配置不当就会遇到Permission denied。这种情况下增设一个专门用于审计的受限账号比直接用业务账号更合理。这个账号只需要对相关目录具备List权限不需要写入权限这样可以防止脚本被反向利用。7. 一些我后来才想明白的经验这套方案从最初的第一版到现在我在实际项目里来回调整过不少地方。挑几个值得说的经验放在最后。关于清单的执行时机如果业务文件是每天凌晨由上游系统推送那么清单最好安排在早上 8 点左右执行这样可以看到完整的昨日文件。如果清单跑得太早可能漏掉后半夜到达的文件产生误导性的文件缺失告警。关于哈希校验的取舍前面说过校验和很耗时。我见过有人因为强行开启 SHA-256 校验导致遍历任务从原来的 5 分钟膨胀到 40 分钟最终任务超时失败。如果你的目录里有超大文件建议先只记录文件大小和修改时间只有在文件大小变化可疑时才用哈希做二次确认而不是每次都全量计算。关于 CSV 的保留周期审计数据不是越多越好。我一般建议 CSV 保留 180 天日志保留 90 天。超过这个期限的可以压缩归档或者定期清理。你需要衡量的是追溯一个文件在历史上是否存在过一般不会追溯到半年前保留较长周期没有实际意义反而占用空间。当然特定行业如果有硬性合规要求保留周期要按合规要求来。关于脚本的版本管理这个脚本虽然小但建议纳入 Git 或 SVN 管理。因为配置、安全策略会随着时间变化如果脚本没有版本记录隔了几个月回看时很难搞清楚某一天生成的清单用的是哪一版逻辑。这本身也是可审计的一部分——不只是结果可审计过程也应可审计。其实这套方案的核心思路不是把 WinSCP 和 PowerShell 用得多炫而是让一次原本无痕的操作变成有据可查的事实记录。SFTP 目录清单听起来是个不起眼的小工具但当某一天你需要证明某个文件在那个时间点确实存在于服务器上或者这个文件不是我们这边删除的时这份 CSV 就是最直接的证据。如果你手头正好有类似需求照着上面的脚本改改配置就能跑起来。遇到什么问题欢迎交流。
返回列表