add upload feature to DSA manifest creation
This commit is contained in:
+40
-13
@@ -2,15 +2,18 @@
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Downloads and runs the DSA manifest collector.
|
||||
Downloads and runs the DSA manifest collector, with optional upload and cleanup.
|
||||
.DESCRIPTION
|
||||
Tools.ps1 must be dot-sourced by the caller first. Set $DSAManifestSource
|
||||
in the caller's scope before using Run-Script -LivePSScript Get-DSAManifest.
|
||||
The output path is generated in $Global:OutputDirectory with the filename
|
||||
dsa-manifest-HOSTNAME-yyyyMMddHHmmss.csv, using the computer name and current local date and time.
|
||||
Set $Zip to 'true', '$true', 'yes', 'y', or '1' to include --zip.
|
||||
Use 'false', '$false', 'no', 'n', or '0' to disable ZIP; an empty value also disables it.
|
||||
Values are case-insensitive and surrounding whitespace is ignored.
|
||||
$Zip enables ZIP compression; $Upload sends the result to the FileDrop;
|
||||
$DeleteAfterUpload removes the local result only after a confirmed upload.
|
||||
All three accept 'true', '$true', 'yes', 'y', or '1' to enable, and
|
||||
'false', '$false', 'no', 'n', or '0' to disable. Empty values disable the option.
|
||||
Values are case-insensitive and surrounding whitespace is ignored. When
|
||||
$Upload is false, the local CSV or ZIP is retained regardless of $DeleteAfterUpload.
|
||||
#>
|
||||
|
||||
If ( [string]::IsNullOrWhiteSpace([string]$DSAManifestSource) ) {
|
||||
@@ -18,16 +21,20 @@ If ( [string]::IsNullOrWhiteSpace([string]$DSAManifestSource) ) {
|
||||
}
|
||||
|
||||
# Normalize RMM text values without treating every nonempty string as true.
|
||||
$DsaZipValue = ([string]$Zip).Trim().ToLowerInvariant()
|
||||
If ( $DsaZipValue -in @('true', '$true', 'yes', 'y', '1') ) {
|
||||
$DsaZipEnabled = $true
|
||||
}
|
||||
ElseIf ( $DsaZipValue -in @('', 'false', '$false', 'no', 'n', '0') ) {
|
||||
$DsaZipEnabled = $false
|
||||
}
|
||||
Else {
|
||||
Throw 'Invalid $Zip value. Use true, $true, yes, y, 1, false, $false, no, n, or 0.'
|
||||
$DsaOptions = @{ Zip = $Zip; Upload = $Upload; DeleteAfterUpload = $DeleteAfterUpload }
|
||||
ForEach ( $DsaOptionName in @('Zip', 'Upload', 'DeleteAfterUpload') ) {
|
||||
$DsaOptionValue = ([string]$DsaOptions[$DsaOptionName]).Trim().ToLowerInvariant()
|
||||
If ( $DsaOptionValue -in @('true', '$true', 'yes', 'y', '1') ) {
|
||||
$DsaOptions[$DsaOptionName] = $true
|
||||
}
|
||||
ElseIf ( $DsaOptionValue -in @('', 'false', '$false', 'no', 'n', '0') ) {
|
||||
$DsaOptions[$DsaOptionName] = $false
|
||||
}
|
||||
Else {
|
||||
Throw ('Invalid ${0} value. Use true, $true, yes, y, 1, false, $false, no, n, or 0.' -f $DsaOptionName)
|
||||
}
|
||||
}
|
||||
$DsaZipEnabled = $DsaOptions.Zip
|
||||
|
||||
$DSAManifestOutput = Join-Path -Path $Global:OutputDirectory -ChildPath (
|
||||
'dsa-manifest-{0}-{1}.csv' -f $Env:COMPUTERNAME, (Get-Date -Format 'yyyyMMddHHmmss')
|
||||
@@ -65,6 +72,7 @@ $DsaCommandLine = ($DsaArguments | ForEach-Object {
|
||||
'"{0}"' -f ($_ -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1')
|
||||
}) -join ' '
|
||||
|
||||
$DsaCollectionTime = Get-Date
|
||||
Write-Output "Running dsa-collect.exe (ZIP compression: ${DsaZipEnabled})"
|
||||
$DsaProcess = Start-Process -FilePath $DsaExecutablePath -ArgumentList $DsaCommandLine `
|
||||
-Wait -PassThru -WindowStyle Hidden -ErrorAction Stop
|
||||
@@ -80,3 +88,22 @@ If ( !(Test-Path -LiteralPath $DsaResultPath -PathType Leaf) ) {
|
||||
Throw "dsa-collect.exe did not create the expected output: ${DsaResultPath}"
|
||||
}
|
||||
Write-Output "DSA manifest created: ${DsaResultPath}"
|
||||
|
||||
If ( $DsaOptions.Upload ) {
|
||||
Write-Output "Uploading DSA manifest: ${DsaResultPath}"
|
||||
$DsaUploadReceipt = Upload-File -Path $DsaResultPath `
|
||||
-Subject "DSA manifest file uploaded from ${Env:COMPUTERNAME}" `
|
||||
-Message "DSA collection performed on ${DSAManifestSource} at $($DsaCollectionTime.ToString('yyyy-MM-dd HH:mm:ss zzz'))" `
|
||||
-ErrorAction Stop
|
||||
If ( [string]::IsNullOrWhiteSpace([string]$DsaUploadReceipt.AttachmentId) -or
|
||||
[string]::IsNullOrWhiteSpace([string]$DsaUploadReceipt.Status) ) {
|
||||
Throw 'The upload did not return a confirmation receipt. The local manifest has been retained.'
|
||||
}
|
||||
Write-Output "DSA manifest uploaded: ${DsaResultPath}"
|
||||
|
||||
If ( $DsaOptions.DeleteAfterUpload ) {
|
||||
Remove-Item -LiteralPath $DsaResultPath -Force -ErrorAction Stop
|
||||
Write-Output "Deleted uploaded DSA manifest: ${DsaResultPath}"
|
||||
}
|
||||
$DsaUploadReceipt
|
||||
}
|
||||
|
||||
@@ -14,11 +14,10 @@ After dot-sourcing `Tools.ps1`, scripts can upload a completed file to the Ember
|
||||
|
||||
```powershell
|
||||
Upload-File -Path 'C:\ProgramData\Emberkom\Output\report.zip'
|
||||
|
||||
# Inside Get-DSAManifest.ps1, after successful collection:
|
||||
Upload-File -Path $DsaResultPath -Subject "DSA manifest from ${Env:COMPUTERNAME}"
|
||||
```
|
||||
|
||||
`Get-DSAManifest.ps1` uploads its completed CSV or ZIP when `$Upload` is enabled. If `$DeleteAfterUpload` is also enabled, it deletes that same local file only after a confirmed successful upload. Disabled or failed uploads retain the file. `$Zip`, `$Upload`, and `$DeleteAfterUpload` all accept `true`, `$true`, `yes`, `y`, or `1`; `false`, `$false`, `no`, `n`, `0`, or an empty value disable the option. Casing and surrounding whitespace are ignored. The notification subject identifies the computer, and its message includes the source path and collection start time (local time with UTC offset).
|
||||
|
||||
The default destination is `https://xfer.emberkom.com/filedrop/cmd`. The sender defaults to the computer's hostname plus its primary DNS suffix, followed by `@ek-upload.net` (for example, `pc01.example.com@ek-upload.net`). Without a DNS suffix, it uses the hostname. Override these with `-FileDropUrl` and `-From`; `-Subject` and `-Message` are also optional.
|
||||
|
||||
`Upload-File` uses the [LiquidFiles FileDrop API](https://docs.liquidfiles.com/api/v4.3/filedrop/) to obtain a temporary token, upload the file, and submit the message that triggers FileDrop delivery and its configured email notification. It returns a receipt with the path, destination, sender, attachment ID, byte count, and server status. It retains the local file and throws on failure.
|
||||
@@ -31,4 +30,5 @@ Offline tests (no uploads or notifications):
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .\tests\Upload-File.Tests.ps1
|
||||
powershell.exe -NoProfile -File .\tests\Get-DSAManifest.Tests.ps1
|
||||
```
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#Requires -Version 5.0
|
||||
# Offline tests: collector and upload calls are mocked; only temporary files are deleted.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$DsaScriptUnderTest = Join-Path (Split-Path $PSScriptRoot -Parent) 'Get-DSAManifest.ps1'
|
||||
$DsaTokens = $null
|
||||
$DsaErrors = $null
|
||||
$null = [Management.Automation.Language.Parser]::ParseFile($DsaScriptUnderTest, [ref]$DsaTokens, [ref]$DsaErrors)
|
||||
If ($DsaErrors.Count) { Throw ($DsaErrors | Out-String) }
|
||||
|
||||
Function Assert-Dsa { param([bool]$Condition, [string]$Description) If (!$Condition) { Throw $Description } }
|
||||
Function Download-File {
|
||||
[CmdletBinding()]
|
||||
param($URL, $File)
|
||||
$script:DsaDownloadCalls++
|
||||
[IO.File]::WriteAllText($File, 'Mock executable; never executed.')
|
||||
Return $File
|
||||
}
|
||||
Function Start-Process {
|
||||
[CmdletBinding()]
|
||||
param($FilePath, $ArgumentList, [switch]$Wait, [switch]$PassThru, $WindowStyle)
|
||||
$DsaMatch = [regex]::Match($ArgumentList, '"--output"\s+"([^"]+)"')
|
||||
If (!$DsaMatch.Success) { Throw 'Collector output argument missing.' }
|
||||
$script:DsaCsvPath = $DsaMatch.Groups[1].Value
|
||||
$script:DsaGeneratedPath = $DsaCsvPath
|
||||
If ($ArgumentList -match '"--zip"') { $script:DsaGeneratedPath = [IO.Path]::ChangeExtension($DsaCsvPath, '.zip') }
|
||||
If (!$DsaGeneratedPath.StartsWith($script:DsaTestRoot + '\', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
Throw 'Mock collector output is outside the test directory.'
|
||||
}
|
||||
If ($script:DsaTestMode -eq 'collector-failure') { Return [pscustomobject]@{ExitCode=7} }
|
||||
If ($script:DsaTestMode -ne 'missing-output') {
|
||||
[IO.File]::WriteAllText($DsaGeneratedPath, 'Synthetic manifest.')
|
||||
If ($DsaGeneratedPath -ne $DsaCsvPath) {
|
||||
# A same-basename CSV must not be deleted when the uploaded result is ZIP.
|
||||
[IO.File]::WriteAllText($DsaCsvPath, 'Unrelated CSV sentinel.')
|
||||
}
|
||||
}
|
||||
Return [pscustomobject]@{ExitCode=0}
|
||||
}
|
||||
Function Upload-File {
|
||||
[CmdletBinding()]
|
||||
param($Path, $Subject, $Message)
|
||||
Assert-Dsa (Test-Path -LiteralPath $Path -PathType Leaf) 'File was deleted before upload.'
|
||||
$script:DsaRecordedUploads += [pscustomobject]@{Path=$Path; Subject=$Subject; Message=$Message}
|
||||
If ($script:DsaTestMode -eq 'upload-failure') { Throw 'Simulated upload failure.' }
|
||||
If ($script:DsaTestMode -eq 'no-receipt') { Return }
|
||||
If ($script:DsaTestMode -eq 'incomplete-receipt') { Return [pscustomobject]@{AttachmentId='test-attachment'} }
|
||||
Return [pscustomobject]@{Path=$Path; AttachmentId='test-attachment'; Status='Filedrop message sent successfully'}
|
||||
}
|
||||
|
||||
Function Invoke-DsaCase {
|
||||
param([bool]$ZipExpected, [bool]$UploadExpected, [bool]$DeleteExpected, [string]$Mode='success', [hashtable]$Inputs)
|
||||
$Zip = If ($ZipExpected) { ' YeS ' } Else { ' n ' }
|
||||
$Upload = If ($UploadExpected) { ' $TrUe ' } Else { ' 0 ' }
|
||||
$DeleteAfterUpload = If ($DeleteExpected) { ' 1 ' } Else { ' $FaLsE ' }
|
||||
If ($null -ne $Inputs) {
|
||||
$Zip = $Inputs.Zip
|
||||
$Upload = $Inputs.Upload
|
||||
$DeleteAfterUpload = $Inputs.DeleteAfterUpload
|
||||
}
|
||||
$DSAManifestSource = 'C:\Synthetic Source'
|
||||
$Global:ToolsDirectory = Join-Path $script:DsaTestRoot 'Tools'
|
||||
$Global:OutputDirectory = Join-Path $script:DsaTestRoot ([guid]::NewGuid().ToString('N'))
|
||||
$script:DsaTestMode = $Mode
|
||||
$script:DsaRecordedUploads = @()
|
||||
$script:DsaDownloadCalls = 0
|
||||
$script:DsaGeneratedPath = $null
|
||||
$DsaCaught = $null
|
||||
Try { . $DsaScriptUnderTest | Out-Null } Catch { $DsaCaught = $_ }
|
||||
|
||||
If ($Mode -eq 'invalid-input') {
|
||||
Assert-Dsa ($null -ne $DsaCaught -and $DsaCaught.Exception.Message -like 'Invalid $* value*') 'Invalid flag was not rejected.'
|
||||
Assert-Dsa ($script:DsaDownloadCalls -eq 0) 'Invalid flag must fail before downloading.'
|
||||
Return
|
||||
}
|
||||
If ($Mode -in @('collector-failure','missing-output')) {
|
||||
Assert-Dsa ($null -ne $DsaCaught -and $script:DsaRecordedUploads.Count -eq 0) 'Collection failure must prevent upload.'
|
||||
Return
|
||||
}
|
||||
If ($Mode -eq 'success') { Assert-Dsa ($null -eq $DsaCaught) "Unexpected failure: ${DsaCaught}" }
|
||||
Else { Assert-Dsa ($null -ne $DsaCaught) 'Expected upload/receipt failure.' }
|
||||
|
||||
$DsaExpectedExtension = If ($ZipExpected) { '.zip' } Else { '.csv' }
|
||||
Assert-Dsa ([IO.Path]::GetExtension($script:DsaGeneratedPath) -eq $DsaExpectedExtension) 'Incorrect collected file type.'
|
||||
$DsaExpectedCalls = If ($UploadExpected) { 1 } Else { 0 }
|
||||
Assert-Dsa ($script:DsaRecordedUploads.Count -eq $DsaExpectedCalls) 'Unexpected upload call count.'
|
||||
If ($UploadExpected) {
|
||||
Assert-Dsa ($DsaRecordedUploads[0].Path -eq $script:DsaGeneratedPath) 'Upload targeted the wrong file.'
|
||||
Assert-Dsa ($DsaRecordedUploads[0].Subject -ceq "DSA manifest file uploaded from ${Env:COMPUTERNAME}") 'Subject changed.'
|
||||
$DsaExpectedMessage = "DSA collection performed on ${DSAManifestSource} at $($DsaCollectionTime.ToString('yyyy-MM-dd HH:mm:ss zzz'))"
|
||||
Assert-Dsa ($DsaRecordedUploads[0].Message -ceq $DsaExpectedMessage) 'Message changed.'
|
||||
}
|
||||
$DsaShouldRetain = !($UploadExpected -and $DeleteExpected -and $Mode -eq 'success')
|
||||
Assert-Dsa ((Test-Path -LiteralPath $script:DsaGeneratedPath) -eq $DsaShouldRetain) 'Incorrect deletion or retention behavior.'
|
||||
If ($ZipExpected) { Assert-Dsa (Test-Path -LiteralPath $script:DsaCsvPath) 'Cleanup incorrectly deleted the CSV neighbor.' }
|
||||
}
|
||||
|
||||
$script:DsaTestRoot = Join-Path ([IO.Path]::GetTempPath()) ('dsa-options-tests-' + [guid]::NewGuid().ToString('N'))
|
||||
$null = New-Item -Path $script:DsaTestRoot -ItemType Directory
|
||||
$DsaOriginalToolsDirectory = $Global:ToolsDirectory
|
||||
$DsaOriginalOutputDirectory = $Global:OutputDirectory
|
||||
Try {
|
||||
Foreach ($ZipExpected in @($false,$true)) {
|
||||
Foreach ($UploadExpected in @($false,$true)) {
|
||||
Foreach ($DeleteExpected in @($false,$true)) {
|
||||
Invoke-DsaCase $ZipExpected $UploadExpected $DeleteExpected
|
||||
Write-Host "PASS: ZIP=$ZipExpected Upload=$UploadExpected DeleteAfterUpload=$DeleteExpected"
|
||||
}
|
||||
}
|
||||
Foreach ($Mode in @('upload-failure','no-receipt','incomplete-receipt','collector-failure','missing-output')) {
|
||||
Invoke-DsaCase $ZipExpected $true $true $Mode
|
||||
Write-Host "PASS: ZIP=$ZipExpected $Mode prevents deletion."
|
||||
}
|
||||
}
|
||||
Invoke-DsaCase $false $false $false -Inputs @{}
|
||||
Foreach ($Name in @('Zip','Upload','DeleteAfterUpload')) {
|
||||
$Inputs = @{Zip='no'; Upload='no'; DeleteAfterUpload='no'}
|
||||
$Inputs[$Name] = 'maybe'
|
||||
Invoke-DsaCase $false $false $false 'invalid-input' $Inputs
|
||||
}
|
||||
Write-Host "PASS: empty options default to false; invalid options fail early."
|
||||
Write-Host "All Get-DSAManifest tests passed on PowerShell $($PSVersionTable.PSVersion), $([IntPtr]::Size * 8)-bit."
|
||||
}
|
||||
Finally {
|
||||
$Global:ToolsDirectory = $DsaOriginalToolsDirectory
|
||||
$Global:OutputDirectory = $DsaOriginalOutputDirectory
|
||||
$DsaResolvedRoot = (Resolve-Path -LiteralPath $script:DsaTestRoot).Path
|
||||
$DsaTempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\'
|
||||
If (!$DsaResolvedRoot.StartsWith($DsaTempRoot, [StringComparison]::OrdinalIgnoreCase) -or
|
||||
(Split-Path $DsaResolvedRoot -Leaf) -notlike 'dsa-options-tests-*') { Throw 'Unexpected cleanup directory.' }
|
||||
Remove-Item -LiteralPath $DsaResolvedRoot -Recurse -Force
|
||||
}
|
||||
Reference in New Issue
Block a user