Initial OfficeCom Sentinel client and deployment assets
This commit is contained in:
307
scripts/run-attacktracer-ninja-server.ps1
Normal file
307
scripts/run-attacktracer-ninja-server.ps1
Normal file
@@ -0,0 +1,307 @@
|
||||
param(
|
||||
[string]$ConfigPath = ".\config\attacktracer-server-settings.json"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Resolve-PathLike {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$PathValue,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$BasePath
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($PathValue)) {
|
||||
return $PathValue
|
||||
}
|
||||
|
||||
if ([System.IO.Path]::IsPathRooted($PathValue) -or $PathValue.StartsWith("\\")) {
|
||||
return [System.IO.Path]::GetFullPath($PathValue)
|
||||
}
|
||||
|
||||
return [System.IO.Path]::GetFullPath((Join-Path $BasePath $PathValue))
|
||||
}
|
||||
|
||||
function Join-Url {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$BaseUrl,
|
||||
[Parameter(Mandatory)][string]$RelativePath
|
||||
)
|
||||
|
||||
$base = $BaseUrl.TrimEnd('/')
|
||||
$relative = $RelativePath.TrimStart('/')
|
||||
return "$base/$relative"
|
||||
}
|
||||
|
||||
function ConvertTo-PlainText {
|
||||
param([Parameter(Mandatory)][string]$EncryptedValue)
|
||||
|
||||
$secure = ConvertTo-SecureString $EncryptedValue
|
||||
$credential = New-Object System.Management.Automation.PSCredential("ignored", $secure)
|
||||
return $credential.GetNetworkCredential().Password
|
||||
}
|
||||
|
||||
function Normalize-NinjaScope {
|
||||
param([AllowEmptyString()][string]$Scope)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Scope)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
$tokens = @(
|
||||
$Scope -split '[,\s;]+' |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||
ForEach-Object {
|
||||
switch ($_.Trim().ToLowerInvariant()) {
|
||||
"monitoring" { "monitoring"; break }
|
||||
"uberwachen" { "monitoring"; break }
|
||||
"ueberwachen" { "monitoring"; break }
|
||||
"management" { "management"; break }
|
||||
"verwalten" { "management"; break }
|
||||
"control" { "control"; break }
|
||||
"steuerung" { "control"; break }
|
||||
"offline_access" { "offline_access"; break }
|
||||
default { $_.Trim().ToLowerInvariant() }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return ($tokens | Select-Object -Unique) -join " "
|
||||
}
|
||||
|
||||
function Get-OrganizationStatus {
|
||||
param([pscustomobject[]]$Reports)
|
||||
|
||||
if (@($Reports | Where-Object { $_.AlertState -eq "critical" }).Count -gt 0) {
|
||||
return "critical"
|
||||
}
|
||||
|
||||
if (@($Reports | Where-Object { $_.AlertState -eq "warning" -or $_.TotalEvents -gt 0 -or $_.VulnerabilityCorrelation.CriticalCount -gt 0 }).Count -gt 0) {
|
||||
return "warning"
|
||||
}
|
||||
|
||||
return "ok"
|
||||
}
|
||||
|
||||
function Build-OrganizationSummaryText {
|
||||
param([pscustomobject[]]$Reports)
|
||||
|
||||
$deviceCount = @($Reports).Count
|
||||
$alertingCount = @($Reports | Where-Object { $_.AlertState -ne "ok" -or $_.TotalEvents -gt 0 -or $_.VulnerabilityCorrelation.CriticalCount -gt 0 }).Count
|
||||
$criticalCount = @($Reports | Where-Object { $_.AlertState -eq "critical" }).Count
|
||||
$totalEvents = (@($Reports | Measure-Object -Property TotalEvents -Sum).Sum)
|
||||
$totalEvents = if ($null -eq $totalEvents) { 0 } else { [int]$totalEvents }
|
||||
|
||||
$topSystems = @(
|
||||
$Reports |
|
||||
Where-Object { $_.AlertState -ne "ok" -or $_.TotalEvents -gt 0 -or $_.VulnerabilityCorrelation.CriticalCount -gt 0 } |
|
||||
Sort-Object @{ Expression = { $_.AlertState -eq "critical" }; Descending = $true }, @{ Expression = { [int]$_.TotalEvents }; Descending = $true } |
|
||||
Select-Object -ExpandProperty MachineName -First 5
|
||||
)
|
||||
|
||||
$summary = "$deviceCount Geraete gescannt, $alertingCount auffaellig, $criticalCount kritisch, $totalEvents Events."
|
||||
if ($topSystems.Count -gt 0) {
|
||||
$summary += " Top-Systeme: $($topSystems -join ', ')."
|
||||
}
|
||||
|
||||
return $summary
|
||||
}
|
||||
|
||||
function Get-LatestReports {
|
||||
param([Parameter(Mandatory)][string]$ReportsRootPath)
|
||||
|
||||
$reportFiles = @()
|
||||
if (Test-Path $ReportsRootPath -PathType Leaf) {
|
||||
$reportFiles = @($ReportsRootPath)
|
||||
}
|
||||
elseif (Test-Path $ReportsRootPath -PathType Container) {
|
||||
$reportFiles = @(Get-ChildItem -Path $ReportsRootPath -Recurse -Filter *.json | Select-Object -ExpandProperty FullName)
|
||||
}
|
||||
|
||||
if ($reportFiles.Count -eq 0) {
|
||||
throw "No report JSON files found under $ReportsRootPath"
|
||||
}
|
||||
|
||||
$reports = foreach ($file in $reportFiles) {
|
||||
try {
|
||||
$report = Get-Content $file -Raw | ConvertFrom-Json
|
||||
if ($report.MachineName) {
|
||||
$report
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Skipping invalid report file ${file}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if (@($reports).Count -eq 0) {
|
||||
throw "No valid AttackTracer report files were parsed."
|
||||
}
|
||||
|
||||
return @(
|
||||
$reports |
|
||||
Group-Object MachineName |
|
||||
ForEach-Object {
|
||||
$_.Group |
|
||||
Sort-Object {
|
||||
try {
|
||||
[DateTimeOffset]::Parse([string]$_.GeneratedAtLocal)
|
||||
}
|
||||
catch {
|
||||
[DateTimeOffset]::MinValue
|
||||
}
|
||||
} -Descending |
|
||||
Select-Object -First 1
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Get-NinjaAccessToken {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$BaseUrl,
|
||||
[Parameter(Mandatory)][string]$ClientId,
|
||||
[Parameter(Mandatory)][string]$ClientSecret,
|
||||
[string]$Scope = ""
|
||||
)
|
||||
|
||||
$body = @{
|
||||
grant_type = "client_credentials"
|
||||
client_id = $ClientId
|
||||
client_secret = $ClientSecret
|
||||
}
|
||||
|
||||
$normalizedScope = Normalize-NinjaScope -Scope $Scope
|
||||
if (-not [string]::IsNullOrWhiteSpace($normalizedScope)) {
|
||||
$body.scope = $normalizedScope
|
||||
}
|
||||
|
||||
$tokenEndpoints = @(
|
||||
(Join-Url -BaseUrl $BaseUrl -RelativePath "ws/oauth/token"),
|
||||
(Join-Url -BaseUrl $BaseUrl -RelativePath "oauth/token")
|
||||
)
|
||||
|
||||
$failures = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
foreach ($tokenEndpoint in $tokenEndpoints) {
|
||||
try {
|
||||
Write-Host "Requesting NinjaOne OAuth token from $tokenEndpoint"
|
||||
$response = Invoke-RestMethod -Method Post -Uri $tokenEndpoint -Body $body -ContentType "application/x-www-form-urlencoded" -TimeoutSec 60
|
||||
if (-not $response.access_token) {
|
||||
throw "OAuth token response did not contain an access_token."
|
||||
}
|
||||
|
||||
Write-Host "NinjaOne OAuth token acquired successfully"
|
||||
return [string]$response.access_token
|
||||
}
|
||||
catch {
|
||||
$message = "Token endpoint $tokenEndpoint failed: " + $_.Exception.Message
|
||||
if ($_.ErrorDetails.Message) {
|
||||
$message += " | " + $_.ErrorDetails.Message
|
||||
}
|
||||
|
||||
$failures.Add($message)
|
||||
}
|
||||
}
|
||||
|
||||
throw "Failed to obtain NinjaOne OAuth token. " + ($failures -join " || ")
|
||||
}
|
||||
|
||||
function Invoke-NinjaOrganizationFieldPatch {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$BaseUrl,
|
||||
[Parameter(Mandatory)][int]$OrganizationId,
|
||||
[Parameter(Mandatory)][string]$AccessToken,
|
||||
[Parameter(Mandatory)][hashtable]$FieldValues
|
||||
)
|
||||
|
||||
$endpoint = (Join-Url -BaseUrl $BaseUrl -RelativePath "v2/organization/$OrganizationId/custom-fields")
|
||||
$headers = @{
|
||||
Authorization = "Bearer $AccessToken"
|
||||
Accept = "application/json"
|
||||
}
|
||||
|
||||
$payloadCandidates = @(
|
||||
$FieldValues,
|
||||
@{ customFields = $FieldValues },
|
||||
@{ fields = @($FieldValues.GetEnumerator() | ForEach-Object { @{ name = $_.Key; value = $_.Value } }) }
|
||||
)
|
||||
|
||||
$failures = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
foreach ($payload in $payloadCandidates) {
|
||||
try {
|
||||
$json = $payload | ConvertTo-Json -Depth 8
|
||||
$fieldNames = @($FieldValues.Keys) -join ", "
|
||||
Write-Host "Organization custom fields endpoint: $endpoint"
|
||||
Write-Host "Updating NinjaOne organization custom fields: $fieldNames"
|
||||
Write-Host "PATCH payload size: $($json.Length) characters"
|
||||
Invoke-RestMethod -Method Patch -Uri $endpoint -Headers $headers -ContentType "application/json" -Body $json -TimeoutSec 60 | Out-Null
|
||||
Write-Host "NinjaOne organization custom fields updated successfully"
|
||||
return
|
||||
}
|
||||
catch {
|
||||
$message = $_.Exception.Message
|
||||
if ($_.ErrorDetails.Message) {
|
||||
$message += " | " + $_.ErrorDetails.Message
|
||||
}
|
||||
|
||||
$failures.Add($message)
|
||||
}
|
||||
}
|
||||
|
||||
throw "Failed to update NinjaOne organization custom fields. " + ($failures -join " || ")
|
||||
}
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$configBasePath = $PSScriptRoot
|
||||
$configFullPath = Resolve-PathLike -PathValue $ConfigPath -BasePath $configBasePath
|
||||
if (-not (Test-Path $configFullPath)) {
|
||||
throw "Server config file not found: $configFullPath"
|
||||
}
|
||||
|
||||
$config = Get-Content $configFullPath -Raw | ConvertFrom-Json
|
||||
$reportsRootPath = Resolve-PathLike -PathValue $config.reportsRoot -BasePath $repoRoot
|
||||
$htmlOutputPath = Resolve-PathLike -PathValue $config.htmlOutputPath -BasePath $repoRoot
|
||||
$orgReportsScript = Join-Path $repoRoot "scripts\build-attacktracer-org-report.ps1"
|
||||
|
||||
& powershell -ExecutionPolicy Bypass -File $orgReportsScript -ReportsRoot $reportsRootPath -OutputPath $htmlOutputPath -MaxAlertRows $config.maxAlertRows
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
$latestReports = Get-LatestReports -ReportsRootPath $reportsRootPath
|
||||
$orgStatus = Get-OrganizationStatus -Reports $latestReports
|
||||
$orgSummary = Build-OrganizationSummaryText -Reports $latestReports
|
||||
$orgLastUpdate = (Get-Date).ToString("o")
|
||||
$htmlContent = Get-Content $htmlOutputPath -Raw
|
||||
Write-Host "Organization summary prepared"
|
||||
Write-Host "HTML report size: $($htmlContent.Length) characters"
|
||||
$clientSecret = ConvertTo-PlainText -EncryptedValue ([string]$config.clientSecretEncrypted)
|
||||
$accessToken = Get-NinjaAccessToken -BaseUrl ([string]$config.ninjaBaseUrl) -ClientId ([string]$config.clientId) -ClientSecret $clientSecret -Scope ([string]$config.oauthScope)
|
||||
|
||||
$fieldValues = @{
|
||||
([string]$config.statusFieldName) = $orgStatus
|
||||
([string]$config.summaryFieldName) = $orgSummary
|
||||
([string]$config.lastUpdateFieldName) = $orgLastUpdate
|
||||
}
|
||||
|
||||
Invoke-NinjaOrganizationFieldPatch -BaseUrl ([string]$config.ninjaBaseUrl) -OrganizationId ([int]$config.organizationId) -AccessToken $accessToken -FieldValues $fieldValues
|
||||
|
||||
if ($config.updateHtmlField -and -not [string]::IsNullOrWhiteSpace([string]$config.htmlFieldName)) {
|
||||
$htmlFieldName = [string]$config.htmlFieldName
|
||||
Write-Host "Attempting separate HTML organization field update for '$htmlFieldName'"
|
||||
|
||||
try {
|
||||
Invoke-NinjaOrganizationFieldPatch -BaseUrl ([string]$config.ninjaBaseUrl) -OrganizationId ([int]$config.organizationId) -AccessToken $accessToken -FieldValues @{
|
||||
$htmlFieldName = $htmlContent
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "HTML organization field update failed for '$htmlFieldName'. Keeping local HTML report only. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Organization HTML report written to $htmlOutputPath"
|
||||
Write-Host "Organization custom fields updated via NinjaOne API"
|
||||
Write-Host "Status field: $($config.statusFieldName)=$orgStatus"
|
||||
Reference in New Issue
Block a user