# ============================================================
# audit — Novatec.one PC Revizija v4.0 CSV
# Pokretanje: irm https://sysinfo.novatec.one/audit | iex
# novatec.one — IT podrska za moderne tvrtke
# ============================================================
# Potrebne su samo dvije datoteke na serveru:
# 1) audit (endpoint /audit)
# 2) upload-revizija-v4.php
#
$CLIENT_VERSION = "4.0-csv"
$EXPECTED_API_VERSION = "4.0-csv"
$API_ENDPOINT = "https://sysinfo.novatec.one/upload-revizija-v4.php"
$AUDIT_SOURCE_URL = "https://sysinfo.novatec.one/audit?release=4.0-csv"
$REPORTS_DIR = "$env:TEMP\novatec_audit\izvjestaji"
# ── DODATNO: slanje izravno na Novatec Dashboard ─────────────────────────────
# Ne mijenja postojece slanje na sysinfo.novatec.one; portal je dodatno
# odrediste. Ako je token prazan, ovaj dio se preskace bez ikakve promjene
# dosadasnjeg ponasanja.
#
# Token generirajte na portalu:
# sudo /opt/novatec-dashboard/current/scripts/configure-sysinfo-token.sh
$PORTAL_ENDPOINT = "https://dash.novatec.one/api/sysinfo/ingest"
$PORTAL_TOKEN = "" # <- ovdje zalijepite token s portala
$PORTAL_TIMEOUT_SEC = 30
$WARRANTY_TIMEOUT_SEC = 12
# Dell TechDirect API - vjerodajnice se NE upisuju u skriptu.
# Po potrebi ih postavi kao system environment varijable:
# NOVATEC_DELL_CLIENT_ID
# NOVATEC_DELL_CLIENT_SECRET
$DELL_TOKEN_ENDPOINT = if ($env:NOVATEC_DELL_TOKEN_URL) { $env:NOVATEC_DELL_TOKEN_URL } else { "https://apigtwb2c.us.dell.com/auth/oauth/v2/token" }
$DELL_WARRANTY_ENDPOINT = if ($env:NOVATEC_DELL_WARRANTY_URL) { $env:NOVATEC_DELL_WARRANTY_URL } else { "https://apigtwb2c.us.dell.com/PROD/sbil/eapi/v5/asset-entitlements" }
# ── Konzolni ispis ───────────────────────────────────────────
function Write-Header($txt) {
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host " $txt" -ForegroundColor Cyan
Write-Host "========================================`n" -ForegroundColor Cyan
}
function Write-OK($txt) { Write-Host "[OK] $txt" -ForegroundColor Green }
function Write-ERR($txt) { Write-Host "[GRESKA] $txt" -ForegroundColor Red }
function Write-INFO($txt) { Write-Host "[INFO] $txt" -ForegroundColor Yellow }
# ── Sigurno zapisivanje i normalizacija ────────────────────────────────────────
function Write-JsonUtf8NoBom {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)] $InputObject,
[Parameter(Mandatory = $true)] [string] $Path,
[ValidateRange(3, 100)] [int] $Depth = 20
)
$directory = Split-Path -Parent $Path
if (-not (Test-Path -LiteralPath $directory)) {
New-Item -ItemType Directory -Path $directory -Force -ErrorAction Stop | Out-Null
}
$tempPath = "$Path.tmp"
try {
$json = $InputObject | ConvertTo-Json -Depth $Depth
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($tempPath, $json, $utf8NoBom)
Move-Item -LiteralPath $tempPath -Destination $Path -Force -ErrorAction Stop
} catch {
Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue
throw
}
}
function Get-DirectorySizeTextWithTimeout {
param(
[Parameter(Mandatory = $true)][string] $Path,
[ValidateRange(3, 120)][int] $TimeoutSeconds = 12
)
if (-not (Test-Path -LiteralPath $Path)) { return "N/A" }
$job = $null
try {
$job = Start-Job -ArgumentList $Path -ScriptBlock {
param($TargetPath)
try {
$sum = (
Get-ChildItem -LiteralPath $TargetPath -File -Recurse -Force -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum
).Sum
if ($null -eq $sum) { return 0L }
return [int64]$sum
} catch {
return $null
}
}
$completed = Wait-Job -Job $job -Timeout $TimeoutSeconds
if (-not $completed) {
return "N/A (timeout ${TimeoutSeconds}s)"
}
$bytes = Receive-Job -Job $job -ErrorAction SilentlyContinue | Select-Object -Last 1
if ($null -eq $bytes) { return "N/A" }
return "$([math]::Round(([double]$bytes / 1GB), 2)) GB"
} catch {
return "N/A"
} finally {
if ($job) {
Stop-Job -Job $job -ErrorAction SilentlyContinue
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
}
}
}
function ConvertTo-IsoDateSafe {
param($Value)
if ($null -eq $Value) { return "" }
if ($Value -is [datetime]) { return $Value.ToString("yyyy-MM-dd") }
$text = ([string]$Value).Trim()
if ([string]::IsNullOrWhiteSpace($text)) { return "" }
$formats = @(
"yyyy-MM-dd",
"yyyy-MM-ddTHH:mm:ss",
"yyyy-MM-ddTHH:mm:ssZ",
"yyyy-MM-ddTHH:mm:ss.fffZ",
"MM/dd/yyyy",
"dd.MM.yyyy",
"yyyyMMdd"
)
foreach ($format in $formats) {
$parsed = [datetime]::MinValue
if ([datetime]::TryParseExact(
$text,
$format,
[System.Globalization.CultureInfo]::InvariantCulture,
[System.Globalization.DateTimeStyles]::AllowWhiteSpaces,
[ref]$parsed
)) {
return $parsed.ToString("yyyy-MM-dd")
}
}
$fallback = [datetime]::MinValue
if ([datetime]::TryParse(
$text,
[System.Globalization.CultureInfo]::InvariantCulture,
[System.Globalization.DateTimeStyles]::AllowWhiteSpaces,
[ref]$fallback
)) {
return $fallback.ToString("yyyy-MM-dd")
}
return ""
}
function New-WarrantyResult {
param(
[string] $Provider,
[ValidateSet("Found", "NotFound", "NotConfigured", "Unsupported", "Skipped", "Error")]
[string] $Status,
[string] $SerialNumber,
[string] $StartDate = "",
[string] $EndDate = "",
[string] $Product = "",
[string] $Message = ""
)
return [PSCustomObject][ordered]@{
Provider = $Provider
Status = $Status
SerialNumber = $SerialNumber
StartDate = (ConvertTo-IsoDateSafe $StartDate)
EndDate = (ConvertTo-IsoDateSafe $EndDate)
Product = $Product
Message = $Message
CheckedAt = (Get-Date).ToUniversalTime().ToString("o")
}
}
# ── Jamstva: provider adapteri, svaki poziv ima timeout i nikad ne ruši reviziju ──
function Get-LenovoWarrantySafe {
param([Parameter(Mandatory = $true)][string] $SerialNumber)
try {
$serial = $SerialNumber.Trim()
$escapedSerial = [System.Uri]::EscapeDataString($serial)
$headers = @{
"User-Agent" = "Novatec-System-Audit/3.1"
"Accept" = "application/json"
"Accept-Language" = "en-US,en;q=0.8"
}
$productResponse = Invoke-RestMethod `
-Uri "https://pcsupport.lenovo.com/us/en/api/v4/mse/getproducts?productId=$escapedSerial" `
-Method Get `
-Headers $headers `
-TimeoutSec $WARRANTY_TIMEOUT_SEC `
-ErrorAction Stop
$productName = [string]$productResponse.Name
$machineType = ""
if ($productName -match "Type\s+([A-Z0-9]+)") {
$machineType = $Matches[1]
}
$requestBody = [ordered]@{
serialNumber = $serial
country = "us"
language = "en"
}
if ($machineType) { $requestBody.machineType = $machineType }
$warrantyResponse = Invoke-RestMethod `
-Uri "https://pcsupport.lenovo.com/us/en/api/v4/upsell/redport/getIbaseInfo" `
-Method Post `
-Headers $headers `
-ContentType "application/json" `
-Body ($requestBody | ConvertTo-Json -Depth 4) `
-TimeoutSec $WARRANTY_TIMEOUT_SEC `
-ErrorAction Stop
$entries = @($warrantyResponse.Data.baseWarranties)
$normalized = @(
foreach ($entry in $entries) {
$start = ConvertTo-IsoDateSafe $entry.startDate
$end = ConvertTo-IsoDateSafe $entry.EndDate
if ($start -or $end) {
[PSCustomObject]@{ StartDate = $start; EndDate = $end }
}
}
)
if ($normalized.Count -eq 0) {
return New-WarrantyResult `
-Provider "Lenovo" `
-Status "NotFound" `
-SerialNumber $serial `
-Product ([string]$warrantyResponse.Data.machineInfo.product) `
-Message "Lenovo nije vratio valjan datum jamstva."
}
$startDate = ($normalized | Where-Object StartDate | Sort-Object StartDate | Select-Object -First 1).StartDate
$endDate = ($normalized | Where-Object EndDate | Sort-Object EndDate -Descending | Select-Object -First 1).EndDate
return New-WarrantyResult `
-Provider "Lenovo" `
-Status "Found" `
-SerialNumber $serial `
-StartDate $startDate `
-EndDate $endDate `
-Product ([string]$warrantyResponse.Data.machineInfo.product) `
-Message "Jamstvo dohvaćeno s Lenovo support servisa."
} catch {
return New-WarrantyResult `
-Provider "Lenovo" `
-Status "Error" `
-SerialNumber $SerialNumber `
-Message ("Lenovo warranty lookup nije uspio: " + $_.Exception.Message)
}
}
function Get-DellWarrantySafe {
param([Parameter(Mandatory = $true)][string] $SerialNumber)
$clientId = $env:NOVATEC_DELL_CLIENT_ID
$clientSecret = $env:NOVATEC_DELL_CLIENT_SECRET
if ([string]::IsNullOrWhiteSpace($clientId) -or [string]::IsNullOrWhiteSpace($clientSecret)) {
return New-WarrantyResult `
-Provider "Dell TechDirect" `
-Status "NotConfigured" `
-SerialNumber $SerialNumber `
-Message "Postavi NOVATEC_DELL_CLIENT_ID i NOVATEC_DELL_CLIENT_SECRET."
}
try {
$tokenResponse = Invoke-RestMethod `
-Uri $DELL_TOKEN_ENDPOINT `
-Method Post `
-ContentType "application/x-www-form-urlencoded" `
-Body @{
client_id = $clientId
client_secret = $clientSecret
grant_type = "client_credentials"
} `
-TimeoutSec $WARRANTY_TIMEOUT_SEC `
-ErrorAction Stop
if ([string]::IsNullOrWhiteSpace([string]$tokenResponse.access_token)) {
throw "Dell OAuth odgovor ne sadrži access_token."
}
$serviceTag = [System.Uri]::EscapeDataString($SerialNumber.Trim())
$uri = "$DELL_WARRANTY_ENDPOINT?servicetags=$serviceTag"
$response = Invoke-RestMethod `
-Uri $uri `
-Method Get `
-Headers @{
Authorization = "Bearer $($tokenResponse.access_token)"
Accept = "application/json"
} `
-TimeoutSec $WARRANTY_TIMEOUT_SEC `
-ErrorAction Stop
$assets = @()
if ($response.assetEntitlementData) {
$assets = @($response.assetEntitlementData)
} elseif ($response.data) {
$assets = @($response.data)
} else {
$assets = @($response)
}
$asset = $assets | Select-Object -First 1
if (-not $asset) {
return New-WarrantyResult `
-Provider "Dell TechDirect" `
-Status "NotFound" `
-SerialNumber $SerialNumber `
-Message "Dell nije vratio uređaj za navedeni Service Tag."
}
$entitlements = @($asset.entitlements)
$dates = @(
foreach ($entitlement in $entitlements) {
$start = ConvertTo-IsoDateSafe $entitlement.startDate
$end = ConvertTo-IsoDateSafe $entitlement.endDate
if ($start -or $end) {
[PSCustomObject]@{ StartDate = $start; EndDate = $end }
}
}
)
$startDate = ($dates | Where-Object StartDate | Sort-Object StartDate | Select-Object -First 1).StartDate
if (-not $startDate) { $startDate = ConvertTo-IsoDateSafe $asset.shipDate }
$endDate = ($dates | Where-Object EndDate | Sort-Object EndDate -Descending | Select-Object -First 1).EndDate
$product = [string]$asset.productLineDescription
if (-not $product) { $product = [string]$asset.productLobDescription }
if (-not $startDate -and -not $endDate) {
return New-WarrantyResult `
-Provider "Dell TechDirect" `
-Status "NotFound" `
-SerialNumber $SerialNumber `
-Product $product `
-Message "Dell je pronašao uređaj, ali nije vratio valjane datume jamstva."
}
return New-WarrantyResult `
-Provider "Dell TechDirect" `
-Status "Found" `
-SerialNumber $SerialNumber `
-StartDate $startDate `
-EndDate $endDate `
-Product $product `
-Message "Jamstvo dohvaćeno preko Dell TechDirect API-ja."
} catch {
return New-WarrantyResult `
-Provider "Dell TechDirect" `
-Status "Error" `
-SerialNumber $SerialNumber `
-Message ("Dell warranty lookup nije uspio: " + $_.Exception.Message)
}
}
function Get-DeviceWarranty {
param(
[string] $Manufacturer,
[string] $SerialNumber
)
$make = ([string]$Manufacturer).Trim().ToUpperInvariant()
$serial = ([string]$SerialNumber).Trim()
if ([string]::IsNullOrWhiteSpace($serial) -or $serial -match "^(N/?A|NONE|UNKNOWN|DEFAULT STRING|TO BE FILLED)") {
return New-WarrantyResult `
-Provider "None" `
-Status "Skipped" `
-SerialNumber $serial `
-Message "Serijski broj nije dostupan ili nije valjan."
}
if ($make -match "LENOVO") {
return Get-LenovoWarrantySafe -SerialNumber $serial
}
if ($make -match "DELL") {
return Get-DellWarrantySafe -SerialNumber $serial
}
if ($make -match "HEWLETT|(^|\s)HP($|\s)|HPE") {
return New-WarrantyResult `
-Provider "HP" `
-Status "NotConfigured" `
-SerialNumber $serial `
-Message "HP nema pouzdan javni unattended lookup bez partnerskog/API pristupa; adapter je namjerno onemogućen umjesto screen-scrapinga."
}
return New-WarrantyResult `
-Provider $Manufacturer `
-Status "Unsupported" `
-SerialNumber $serial `
-Message "Za ovog proizvođača nije konfiguriran pouzdan warranty provider."
}
# ── ITFlow CSV projekcija ──────────────────────────────────────────────────────
function Get-ITFlowAssetType {
try {
$chassisTypes = @((Get-CimInstance Win32_SystemEnclosure -ErrorAction Stop).ChassisTypes)
if ($chassisTypes | Where-Object { $_ -in @(8, 9, 10, 11, 12, 14, 18, 21, 30, 31, 32) }) {
return "Laptop"
}
if ($chassisTypes | Where-Object { $_ -in @(23, 28) }) {
return "Server"
}
} catch {
# Fallback je namjerno Desktop.
}
return "Desktop"
}
function Get-PrimaryNetworkData {
param($NetworkItems)
foreach ($adapter in @($NetworkItems)) {
if (-not $adapter) { continue }
$ipv4 = ""
foreach ($candidate in (([string]$adapter.IP) -split ",")) {
$ip = $candidate.Trim()
if ($ip -match "^(?!127\.)(?!169\.254\.)\d{1,3}(\.\d{1,3}){3}$") {
$ipv4 = $ip
break
}
}
$mac = ([string]$adapter.MAC).Trim()
if ($ipv4 -or $mac) {
return [PSCustomObject]@{ IP = $ipv4; MAC = $mac }
}
}
return [PSCustomObject]@{ IP = ""; MAC = "" }
}
function New-ITFlowNotes {
param(
$Data,
$Meta,
$Warranty
)
$parts = New-Object System.Collections.Generic.List[string]
$cpuNames = @($Data.CPU | ForEach-Object { $_.Naziv } | Where-Object { $_ }) -join "; "
if ($cpuNames) { $parts.Add("CPU: $cpuNames") }
# RAM zapisi mogu biti hashtable ili PSCustomObject. Zbrajaj eksplicitno i
# preskoci neispravne zapise kako ITFlow CSV nikada ne bi blokirao reviziju.
[double]$ramTotal = 0
foreach ($ramModule in @($Data.RAM)) {
if ($null -eq $ramModule) { continue }
$rawSize = $null
if ($ramModule -is [System.Collections.IDictionary]) {
$rawSize = $ramModule['Velicina_GB']
} else {
$sizeProperty = $ramModule.PSObject.Properties['Velicina_GB']
if ($sizeProperty) { $rawSize = $sizeProperty.Value }
}
if ($null -eq $rawSize -or [string]::IsNullOrWhiteSpace([string]$rawSize)) { continue }
[double]$parsedSize = 0
$sizeText = ([string]$rawSize).Trim().Replace(',', '.')
$parsed = [double]::TryParse(
$sizeText,
[System.Globalization.NumberStyles]::Float,
[System.Globalization.CultureInfo]::InvariantCulture,
[ref]$parsedSize
)
if ($parsed -and $parsedSize -gt 0) { $ramTotal += $parsedSize }
}
if ($ramTotal -gt 0) { $parts.Add("RAM: $([math]::Round($ramTotal, 2)) GB") }
$diskNotes = @(
foreach ($disk in @($Data.Diskovi)) {
if (-not $disk) { continue }
$diskText = ([string]$disk.Model).Trim()
if ($disk.Velicina_GB) { $diskText += " $($disk.Velicina_GB) GB" }
if ($disk.Serijal -and $disk.Serijal -ne "N/A") { $diskText += " [$($disk.Serijal)]" }
if ($diskText.Trim()) { $diskText.Trim() }
}
) -join "; "
if ($diskNotes) { $parts.Add("Diskovi: $diskNotes") }
$gpuNames = @($Data.GPU | ForEach-Object { $_.Naziv } | Where-Object { $_ }) -join "; "
if ($gpuNames) { $parts.Add("GPU: $gpuNames") }
$monitorNotes = @(
foreach ($monitor in @($Data.Monitori)) {
if (-not $monitor) { continue }
$monitorText = "$($monitor.Proizvodac) $($monitor.PartNumber)".Trim()
if ($monitor.Serijal -and $monitor.Serijal -ne "N/A") { $monitorText += " [$($monitor.Serijal)]" }
if ($monitorText.Trim()) { $monitorText.Trim() }
}
) -join "; "
if ($monitorNotes) { $parts.Add("Monitori: $monitorNotes") }
$osText = ([string]$Data.OS.Naziv).Trim()
if ($Data.OS.Build) { $osText += " Build $($Data.OS.Build)" }
if ($osText.Trim()) { $parts.Add("OS: $($osText.Trim())") }
$avNames = @($Data.WSC_Antivirus | ForEach-Object { $_.Naziv } | Where-Object { $_ }) -join "; "
if ($avNames) { $parts.Add("Antivirus: $avNames") }
$network = Get-PrimaryNetworkData -NetworkItems $Data.Mreza
if ($network.IP -or $network.MAC) {
$networkText = "Mreza:"
if ($network.IP) { $networkText += " IP $($network.IP)" }
if ($network.MAC) { $networkText += " MAC $($network.MAC)" }
$parts.Add($networkText)
}
if ($Meta.Tvrtka) { $parts.Add("Tvrtka: $($Meta.Tvrtka)") }
if ($Meta.Ime) { $parts.Add("Dodijeljeno: $($Meta.Ime)") }
if ($Meta.Odjel) { $parts.Add("Fizicka lokacija: $($Meta.Odjel)") }
if ($Meta.Napomena) { $parts.Add("Napomena: $($Meta.Napomena)") }
if ($Warranty) {
$warrantyText = "Jamstvo: $($Warranty.Status)"
if ($Warranty.StartDate) { $warrantyText += " od $($Warranty.StartDate)" }
if ($Warranty.EndDate) { $warrantyText += " do $($Warranty.EndDate)" }
if ($Warranty.Provider) { $warrantyText += " ($($Warranty.Provider))" }
if ($Warranty.Message -and $Warranty.Status -ne "Found") { $warrantyText += " - $($Warranty.Message)" }
$parts.Add($warrantyText)
}
return $parts -join " | "
}
function New-ITFlowCsvRow {
param(
$Data,
$Meta,
$Warranty
)
$serial = ([string]$Data.Racunalo.Serijal).Trim()
if (-not $serial -or $serial -match "^(N/?A|NONE|UNKNOWN|DEFAULT STRING|TO BE FILLED)$") {
$serial = ([string]$Data.MaticnaPloca.Serijal).Trim()
}
$assetType = Get-ITFlowAssetType
$description = if ($assetType -eq "Server") { "Server" } else { "Radna stanica" }
# ITFlow template nema zaseban stupac za datum isteka jamstva.
# Purchase Date ostaje prazno jer datum jamstva nije datum kupnje;
# podatak o jamstvu zapisuje se u Notes.
return [PSCustomObject]([ordered]@{
Name = [string]$Data.Racunalo.Naziv
Description = $description
Type = $assetType
Make = [string]$Data.Racunalo.Proizvodac
Model = [string]$Data.Racunalo.Model
Serial = $serial
OS = [string]$Data.OS.Naziv
"Purchase Date" = ""
"Assigned To" = [string]$Meta.Ime
Location = "Primary"
"Physical Location" = [string]$Meta.Odjel
Notes = (New-ITFlowNotes -Data $Data -Meta $Meta -Warranty $Warranty)
})
}
function ConvertTo-ITFlowCsvField {
param($Value)
if ($null -eq $Value) { return "" }
$text = [string]$Value
$escaped = $text.Replace('"', '""')
if ($text -match '[,"\r\n]' -or $text -ne $text.Trim()) {
return '"' + $escaped + '"'
}
return $escaped
}
function Write-ITFlowCsvUtf8NoBom {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)] $Row,
[Parameter(Mandatory = $true)] [string] $Path
)
$directory = Split-Path -Parent $Path
if (-not (Test-Path -LiteralPath $directory)) {
New-Item -ItemType Directory -Path $directory -Force -ErrorAction Stop | Out-Null
}
# Header mora biti identican sluzbenom ITFlow Assets templateu.
$header = 'Name,Description,Type,Make,Model,Serial,OS,"Purchase Date","Assigned To",Location,"Physical Location",Notes'
$fields = @(
(ConvertTo-ITFlowCsvField -Value $Row.Name),
(ConvertTo-ITFlowCsvField -Value $Row.Description),
(ConvertTo-ITFlowCsvField -Value $Row.Type),
(ConvertTo-ITFlowCsvField -Value $Row.Make),
(ConvertTo-ITFlowCsvField -Value $Row.Model),
(ConvertTo-ITFlowCsvField -Value $Row.Serial),
(ConvertTo-ITFlowCsvField -Value $Row.OS),
(ConvertTo-ITFlowCsvField -Value $Row.'Purchase Date'),
(ConvertTo-ITFlowCsvField -Value $Row.'Assigned To'),
(ConvertTo-ITFlowCsvField -Value $Row.Location),
(ConvertTo-ITFlowCsvField -Value $Row.'Physical Location'),
(ConvertTo-ITFlowCsvField -Value $Row.Notes)
)
$tempPath = "$Path.tmp"
try {
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[string[]]$lines = @($header, ($fields -join ','))
[System.IO.File]::WriteAllLines($tempPath, $lines, $utf8NoBom)
Move-Item -LiteralPath $tempPath -Destination $Path -Force -ErrorAction Stop
} catch {
Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue
throw
}
}
# ── Provjera admin prava ─────────────────────────────────────
function Test-Admin {
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$p = [System.Security.Principal.WindowsPrincipal]$id
return $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
# ── Unos podataka tehnicara ───────────────────────────────────
function Get-TechnicianInput {
Write-Header "UNOS PODATAKA DJELATNIKA"
do {
$ime = Read-Host " Ime i prezime djelatnika"
Write-Host ""
} while ([string]::IsNullOrWhiteSpace($ime))
do {
$tvrtka = Read-Host " Ime tvrtke / obrta / krajnjeg korisnika"
Write-Host ""
} while ([string]::IsNullOrWhiteSpace($tvrtka))
do {
$odjel = Read-Host " Odjel / Prostorija"
Write-Host ""
} while ([string]::IsNullOrWhiteSpace($odjel))
$napomena = Read-Host " Napomena (Enter za preskociti)"
Write-Host ""
return @{ Ime = $ime.Trim(); Tvrtka = $tvrtka.Trim(); Odjel = $odjel.Trim(); Napomena = $napomena.Trim() }
}
# ── Windows Product Key iz registra ──────────────────────────
function Get-WindowsKey {
try {
$key = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").DigitalProductId
$keyOffset = 52
$isWin8 = ([math]::Floor($key[66] / 6)) -band 1
$key[66] = ($key[66] -band 0xF7) -bor (($isWin8 -band 2) * 4)
$chars = "BCDFGHJKMPQRTVWXY2346789"
$result = ""
for ($i = 24; $i -ge 0; $i--) {
$cur = 0
for ($j = 14; $j -ge 0; $j--) {
$cur = $cur * 256 -bxor $key[$j + $keyOffset]
$key[$j + $keyOffset] = [math]::Floor($cur / 24)
$cur = $cur % 24
}
$result = $chars[$cur] + $result
if (($i % 5 -eq 0) -and ($i -ne 0)) { $result = "-" + $result }
}
return $result
} catch { return "Nije dostupno" }
}
# ── Prikupljanje podataka ─────────────────────────────────────
function Get-SystemData {
Write-INFO "Prikupljam podatke o sustavu..."
$data = @{}
# OS
Write-INFO " OS i aktivacija..."
$os = Get-CimInstance Win32_OperatingSystem
$lic = Get-CimInstance SoftwareLicensingProduct -Filter "PartialProductKey IS NOT NULL AND ApplicationID='55c92734-d682-4d71-983e-d6ec3f16059f'" -ErrorAction SilentlyContinue | Select-Object -First 1
$bootTime = $os.LastBootUpTime
$uptime = (Get-Date) - $bootTime
$uptimeStr = "$($uptime.Days)d $($uptime.Hours)h $($uptime.Minutes)m"
$data.OS = @{
Naziv = $os.Caption
Verzija = $os.Version
Build = $os.BuildNumber
Arhitektura = $os.OSArchitecture
Instaliran = $os.InstallDate.ToString("dd.MM.yyyy")
Zadnji_boot = $bootTime.ToString("dd.MM.yyyy HH:mm")
Uptime = $uptimeStr
Jezik = $os.MUILanguages -join ", "
Licenca = if ($lic) { switch($lic.LicenseStatus){1{"Aktiviran"}2{"Grace period"}5{"Notifikacija"}default{"Nije aktiviran"}} } else { "Nepoznato" }
ProductKey = Get-WindowsKey
RAM_Ukupno = "$([math]::Round($os.TotalVisibleMemorySize/1MB, 2)) GB"
RAM_Slobodno = "$([math]::Round($os.FreePhysicalMemory/1MB, 2)) GB"
}
# Racunalo & BIOS
Write-INFO " Racunalo i BIOS..."
$cs = Get-CimInstance Win32_ComputerSystem
$bios = Get-CimInstance Win32_BIOS
$data.Racunalo = @{
Naziv = $cs.Name
Proizvodac = $cs.Manufacturer
Model = $cs.Model
Serijal = $bios.SerialNumber
Tip = $cs.SystemType
BIOS_Verzija = $bios.SMBIOSBIOSVersion
BIOS_Datum = $bios.ReleaseDate.ToString("dd.MM.yyyy")
BIOS_Vendor = $bios.Manufacturer
Domena = $cs.Domain
Korisnik = $cs.UserName
RAM_Slotovi = $cs.TotalPhysicalMemory | ForEach-Object { "$([math]::Round($_/1GB,2)) GB ukupno" }
}
# Maticna ploca
Write-INFO " Maticna ploca..."
$mb = Get-CimInstance Win32_BaseBoard
$data.MaticnaPloca = @{
Proizvodac = $mb.Manufacturer
Model = $mb.Product
Serijal = $mb.SerialNumber
Verzija = $mb.Version
}
# CPU
Write-INFO " CPU..."
$cpus = Get-CimInstance Win32_Processor
$data.CPU = $cpus | ForEach-Object {
@{
Naziv = $_.Name.Trim()
Proizvodac = $_.Manufacturer
Jezgre = $_.NumberOfCores
LogJezgre = $_.NumberOfLogicalProcessors
MaxMHz = $_.MaxClockSpeed
TrenutnoMHz = $_.CurrentClockSpeed
Arhitektura = switch($_.Architecture){0{"x86"}9{"x64"}6{"IA64"}default{$_.Architecture}}
Socket = $_.SocketDesignation
L2_Cache_KB = $_.L2CacheSize
L3_Cache_KB = $_.L3CacheSize
}
}
# RAM moduli
Write-INFO " RAM moduli..."
$rams = Get-CimInstance Win32_PhysicalMemory
$data.RAM = $rams | ForEach-Object {
@{
Slot = $_.DeviceLocator
Velicina_GB = [math]::Round($_.Capacity/1GB, 2)
Tip = switch($_.MemoryType){20{"DDR"}21{"DDR2"}24{"DDR3"}26{"DDR4"}34{"DDR5"}default{"Nepoznato"}}
Brzina_MHz = $_.Speed
Proizvodac = $_.Manufacturer
Part_Number = $_.PartNumber.Trim()
Serijal = $_.SerialNumber
Forma = switch($_.FormFactor){8{"DIMM"}12{"SO-DIMM"}default{"Nepoznato"}}
}
}
# GPU
Write-INFO " GPU..."
$gpus = Get-CimInstance Win32_VideoController
$data.GPU = $gpus | ForEach-Object {
@{
Naziv = $_.Name
RAM_MB = [math]::Round($_.AdapterRAM/1MB, 0)
Rezolucija = "$($_.CurrentHorizontalResolution)x$($_.CurrentVerticalResolution)"
Driver_Ver = $_.DriverVersion
Driver_Datum = if($_.DriverDate){$_.DriverDate.ToString("dd.MM.yyyy")}else{"N/A"}
Status = $_.Status
}
}
# Diskovi
Write-INFO " Diskovi..."
$diskovi = Get-CimInstance Win32_DiskDrive
$sveLogicke = Get-CimInstance Win32_LogicalDisk | Where-Object { $_.DriveType -eq 3 }
$data.Diskovi = $diskovi | ForEach-Object {
$disk = $_
$mojiLogicki = @()
try {
$assocPart = Get-CimAssociatedInstance -InputObject $disk -ResultClassName Win32_DiskPartition -ErrorAction Stop
$mojiLogicki = $assocPart | ForEach-Object {
Get-CimAssociatedInstance -InputObject $_ -ResultClassName Win32_LogicalDisk -ErrorAction SilentlyContinue
}
} catch {
$mojiLogicki = $sveLogicke
}
@{
Model = $disk.Model
Serijal = if($disk.SerialNumber){ $disk.SerialNumber.Trim() }else{ "N/A" }
Velicina_GB = [math]::Round($disk.Size/1GB, 2)
Tip = $disk.MediaType
Interface = $disk.InterfaceType
Firmware = $disk.FirmwareRevision
Particije = @($mojiLogicki) | Where-Object { $_ } | ForEach-Object {
@{
Slovo = $_.DeviceID
Naziv = $_.VolumeName
Ukupno_GB = [math]::Round($_.Size/1GB, 2)
Slobodno_GB = [math]::Round($_.FreeSpace/1GB, 2)
Zauzeto_pct = if($_.Size -gt 0){ [math]::Round(($_.Size - $_.FreeSpace)/$_.Size*100, 1) }else{ 0 }
FileSystem = $_.FileSystem
}
}
}
}
# Mreza
Write-INFO " Mrezni adapteri..."
$nic = Get-CimInstance Win32_NetworkAdapterConfiguration | Where-Object { $_.IPEnabled }
$data.Mreza = $nic | ForEach-Object {
@{
Opis = $_.Description
MAC = $_.MACAddress
IP = $_.IPAddress -join ", "
Subnet = $_.IPSubnet -join ", "
Gateway = $_.DefaultIPGateway -join ", "
DNS = $_.DNSServerSearchOrder -join ", "
DHCP = $_.DHCPEnabled
}
}
# Monitori
Write-INFO " Monitori..."
$monitors = Get-CimInstance WmiMonitorID -Namespace root\wmi -ErrorAction SilentlyContinue
$data.Monitori = $monitors | ForEach-Object {
function Decode-MonitorString($arr) {
if (-not $arr) { return "N/A" }
$bytes = $arr | Where-Object { $_ -ne 0 }
if (-not $bytes) { return "N/A" }
return ([System.Text.Encoding]::ASCII.GetString([byte[]]$bytes)).Trim()
}
@{
Proizvodac = Decode-MonitorString $_.ManufacturerName
PartNumber = Decode-MonitorString $_.ProductCodeID
Serijal = Decode-MonitorString $_.SerialNumberID
}
}
# Instalirani softver
Write-INFO " Instalirani softver..."
$paths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
$softver = $paths | ForEach-Object {
Get-ItemProperty $_ -ErrorAction SilentlyContinue
} | Where-Object { $_.DisplayName -and $_.DisplayName.Trim() -ne "" } |
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate |
Sort-Object DisplayName -Unique
$data.Softver = $softver | ForEach-Object {
@{
Naziv = $_.DisplayName
Verzija = $_.DisplayVersion
Izdavac = $_.Publisher
Instaliran = $_.InstallDate
}
}
# Korisnici
Write-INFO " Korisnicki racuni..."
$data.Korisnici = Get-CimInstance Win32_UserAccount -Filter "LocalAccount=True" | ForEach-Object {
@{
Ime = $_.Name
Puni_naziv = $_.FullName
Opis = $_.Description
Aktivan = -not $_.Disabled
Admin = $_.SID -like "*-500" -or (Get-CimInstance Win32_GroupUser | Where-Object { $_.PartComponent -like "*$($_.Name)*" -and $_.GroupComponent -like "*Administrators*" } | Select-Object -First 1)
}
}
# Windows Update
Write-INFO " Zadnja azuriranja..."
$data.Azuriranja = Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 15 | ForEach-Object {
@{ KB = $_.HotFixID; Opis = $_.Description; Datum = if($_.InstalledOn){$_.InstalledOn.ToString("dd.MM.yyyy")}else{"N/A"} }
}
# Startup programi
Write-INFO " Startup programi..."
$startupPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
)
$data.Startup = $startupPaths | ForEach-Object {
$path = $_
try {
$props = Get-ItemProperty $path -ErrorAction Stop
if ($null -eq $props) { return }
$props | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notlike "PS*" } | ForEach-Object {
@{ Naziv = $_.Name; Putanja = $props.$($_.Name); Lokacija = $path }
}
} catch {}
}
# Pisaci
Write-INFO " Pisaci..."
$data.Pisaci = Get-CimInstance Win32_Printer | ForEach-Object {
$statusTxt = switch($_.PrinterStatus) {
1 {"Ostalo"} 2 {"Nepoznato"} 3 {"Spreman"} 4 {"Tiskanje"}
5 {"Zagrijavanje"} 6 {"Zaustavljeno"} 7 {"Offline"} default {"Status $($_.PrinterStatus)"}
}
@{ Naziv = $_.Name; Zadani = $_.Default; Status = $statusTxt; Lokalni = $_.Local; PortName = $_.PortName; IP = "" }
}
# Dohvati IP adrese TCP/IP portova zasebno pa spoji
$tcpPorts = @{}
Get-CimInstance Win32_TCPIPPrinterPort -ErrorAction SilentlyContinue | ForEach-Object {
$tcpPorts[$_.Name] = $_.HostAddress
}
foreach ($p in $data.Pisaci) {
if ($tcpPorts.ContainsKey($p.PortName)) {
$p.IP = $tcpPorts[$p.PortName]
} elseif ($p.PortName -like "USB*") {
$p.IP = "USB"
} elseif ($p.PortName -like "WSD*") {
$p.IP = "WSD (mrezni)"
} elseif ($p.PortName -like "SHRFAX*" -or $p.Naziv -match "Fax") {
$p.IP = "Fax"
} elseif ($p.PortName -match "FILE|PORTPROMPT" -or $p.Naziv -match "PDF|XPS|OneNote|Microsoft") {
$p.IP = "Virtualni pisac"
} else {
$p.IP = $p.PortName
}
}
# Mapirani diskovi
Write-INFO " Mapirani diskovi..."
$data.MapiraniDiskovi = Get-CimInstance Win32_LogicalDisk | Where-Object { $_.DriveType -eq 4 } | ForEach-Object {
@{
Slovo = $_.DeviceID
UNC_Put = $_.ProviderName
Naziv = $_.VolumeName
Velicina_GB = if($_.Size){ [math]::Round($_.Size/1GB, 2) }else{ 0 }
Slobodno_GB = if($_.FreeSpace){ [math]::Round($_.FreeSpace/1GB, 2) }else{ 0 }
}
}
# Trenutni korisnik
Write-INFO " Trenutni korisnik..."
$cuName = $env:USERNAME
$cuDomain = $env:USERDOMAIN
$cuProfile = $env:USERPROFILE
$recentDir = "$cuProfile\AppData\Roaming\Microsoft\Windows\Recent"
$recentFiles = if(Test-Path $recentDir) {
Get-ChildItem $recentDir -File -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 10 |
ForEach-Object { @{ Naziv = $_.BaseName; Datum = $_.LastWriteTime.ToString("dd.MM.yyyy HH:mm") } }
} else { @() }
$oneDrivePath = (Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\OneDrive" -ErrorAction SilentlyContinue).UserFolder
$oneDriveEmail = (Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\OneDrive\Accounts\Business1" -ErrorAction SilentlyContinue).UserEmail
$outlookProfiles = (Get-ChildItem "HKCU:\SOFTWARE\Microsoft\Office\16.0\Outlook\Profiles" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty PSChildName) -join ", "
if (-not $outlookProfiles) {
$outlookProfiles = (Get-ChildItem "HKCU:\SOFTWARE\Microsoft\Office\15.0\Outlook\Profiles" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty PSChildName) -join ", "
}
$browserProgId = (Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice" -ErrorAction SilentlyContinue).ProgId
$browserName = switch -Wildcard ($browserProgId) {
"*Chrome*" { "Google Chrome" }
"*Firefox*" { "Mozilla Firefox" }
"*Edge*" { "Microsoft Edge" }
"*Opera*" { "Opera" }
"*Brave*" { "Brave" }
default { if($browserProgId){ $browserProgId }else{ "Nepoznato" } }
}
# Rekurzivno mjerenje profila može trajati vrlo dugo na velikim profilima.
# Izvršava se u zasebnom jobu i prekida nakon definiranog timeouta.
$profileSize = Get-DirectorySizeTextWithTimeout -Path $cuProfile -TimeoutSeconds 12
$data.TrenutniKorisnik = @{
Ime = $cuName
Domena = $cuDomain
ProfilPutanja = $cuProfile
VelicinaProfile = $profileSize
AppData = $env:APPDATA
Temp = $env:TEMP
DefaultBrowser = $browserName
OneDrivePutanja = if($oneDrivePath){ $oneDrivePath }else{ "Nije konfigurirano" }
OneDriveEmail = if($oneDriveEmail){ $oneDriveEmail }else{ "N/A" }
OutlookProfili = if($outlookProfiles){ $outlookProfiles }else{ "Nije konfigurirano" }
NedavniDokumenti = $recentFiles
}
# Outlook — PST/OST datoteke i email računi po profilu
Write-INFO " Outlook PST/OST datoteke i email racuni..."
# --- Pretraga PST/OST — samo poznate zadane putanje (brzo) ---
$pstOstFiles = @()
# Dohvati sve korisnicke profile na sistemu
$userProfiles = Get-CimInstance Win32_UserProfile |
Where-Object { -not $_.Special -and (Test-Path $_.LocalPath) } |
Select-Object -ExpandProperty LocalPath
foreach ($uPath in $userProfiles) {
$uName = Split-Path $uPath -Leaf
# Poznate putanje za OST (IMAP/Exchange cache)
$ostPaths = @(
"$uPath\AppData\Local\Microsoft\Outlook"
)
# Poznate putanje za PST (arhive, POP3)
$pstPaths = @(
"$uPath\Documents\Outlook Files",
"$uPath\Moji dokumenti\Outlook Files",
"$uPath\AppData\Local\Microsoft\Outlook",
"$uPath\AppData\Roaming\Microsoft\Outlook"
)
# Skeniraj OST putanje
foreach ($p in $ostPaths) {
if (-not (Test-Path $p)) { continue }
Get-ChildItem -Path $p -Filter "*.ost" -File -ErrorAction SilentlyContinue | ForEach-Object {
$pstOstFiles += @{
Naziv = $_.Name
Tip = "OST"
Putanja = $_.FullName
Velicina_MB = [math]::Round($_.Length / 1MB, 1)
Velicina_GB = [math]::Round($_.Length / 1GB, 2)
Zadnja_izmjena = $_.LastWriteTime.ToString("dd.MM.yyyy HH:mm")
Korisnik = $uName
}
}
}
# Skeniraj PST putanje
foreach ($p in ($pstPaths | Select-Object -Unique)) {
if (-not (Test-Path $p)) { continue }
Get-ChildItem -Path $p -Filter "*.pst" -File -ErrorAction SilentlyContinue | ForEach-Object {
# Izbjegni duplikate (ista datoteka u vise putanja)
$fullPath = $_.FullName
if (-not ($pstOstFiles | Where-Object { $_.Putanja -eq $fullPath })) {
$pstOstFiles += @{
Naziv = $_.Name
Tip = "PST"
Putanja = $_.FullName
Velicina_MB = [math]::Round($_.Length / 1MB, 1)
Velicina_GB = [math]::Round($_.Length / 1GB, 2)
Zadnja_izmjena = $_.LastWriteTime.ToString("dd.MM.yyyy HH:mm")
Korisnik = $uName
}
}
}
}
# Provjeri i registar za nestandardne putanje PST-a
$regPaths = @(
"HKCU:\SOFTWARE\Microsoft\Office\16.0\Outlook\Search\Catalog",
"HKCU:\SOFTWARE\Microsoft\Office\15.0\Outlook\Search\Catalog",
"HKCU:\SOFTWARE\Microsoft\Office\14.0\Outlook\Search\Catalog"
)
foreach ($rp in $regPaths) {
if (-not (Test-Path $rp)) { continue }
try {
$regVals = Get-ItemProperty $rp -ErrorAction SilentlyContinue
$regVals.PSObject.Properties | Where-Object { $_.Name -like "*.pst" -or $_.Name -like "*.ost" } | ForEach-Object {
$fPath = $_.Name
if ((Test-Path $fPath) -and -not ($pstOstFiles | Where-Object { $_.Putanja -eq $fPath })) {
$fi = Get-Item $fPath -ErrorAction SilentlyContinue
$tip = [System.IO.Path]::GetExtension($fPath).TrimStart('.').ToUpper()
if ($fi) {
$pstOstFiles += @{
Naziv = $fi.Name
Tip = $tip
Putanja = $fi.FullName
Velicina_MB = [math]::Round($fi.Length / 1MB, 1)
Velicina_GB = [math]::Round($fi.Length / 1GB, 2)
Zadnja_izmjena = $fi.LastWriteTime.ToString("dd.MM.yyyy HH:mm")
Korisnik = $uName
}
}
}
}
} catch {}
}
}
$data.PstOstDatoteke = $pstOstFiles
# --- Outlook email računi po profilu iz registra ---
# Podržava Office 2010 (14.0), 2013 (15.0), 2016/2019/365 (16.0)
$outlookVersions = @("16.0","15.0","14.0")
$outlookProfileData = @()
foreach ($ver in $outlookVersions) {
$profRoot = "HKCU:\SOFTWARE\Microsoft\Office\$ver\Outlook\Profiles"
if (-not (Test-Path $profRoot)) { continue }
$profiles = Get-ChildItem $profRoot -ErrorAction SilentlyContinue
foreach ($prof in $profiles) {
$profName = $prof.PSChildName
$emailAddresses = @()
$accountNames = @()
# Pretraži sve podkljuceve profila za email adrese
try {
$allSubKeys = Get-ChildItem -Path $prof.PSPath -Recurse -ErrorAction SilentlyContinue
foreach ($sk in $allSubKeys) {
try {
$props = Get-ItemProperty -Path $sk.PSPath -ErrorAction SilentlyContinue
# Email adresa može biti u više polja
$emailFields = @('Email','Email Address','Account Name','SMTP Email Address',
'New Signature','ReplyToAddress')
foreach ($ef in $emailFields) {
$val = $props.$ef
if ($val -and $val -match '@' -and $val -notmatch 'microsoft\.com$') {
$emailAddresses += $val.Trim()
}
}
# Binarno polje za email (čest format u Outlooku)
$binFields = @('Email','SMTP Email Address','Account Name')
foreach ($bf in $binFields) {
$binVal = $props.$bf
if ($binVal -is [byte[]]) {
$decoded = [System.Text.Encoding]::Unicode.GetString($binVal).TrimEnd([char]0)
if ($decoded -match '@' -and $decoded -notmatch 'microsoft\.com$') {
$emailAddresses += $decoded.Trim()
}
}
}
# Display name / account name
$dn = $props.'Display Name'
if (-not $dn) { $dn = $props.'Account Name' }
if ($dn -and $dn.Trim() -ne '' -and $dn -notmatch '^[{(]') {
$accountNames += $dn.Trim()
}
} catch {}
}
} catch {}
$emailAddresses = $emailAddresses | Where-Object { $_ } | Sort-Object -Unique
$accountNames = $accountNames | Where-Object { $_ } | Sort-Object -Unique
# Fallback: pokušaj naći email iz Office 365 / MAPI account sekcija
if ($emailAddresses.Count -eq 0) {
try {
$acctPath = "$($prof.PSPath)\9375CFF0413111d3B88A00104B2A6676"
if (Test-Path $acctPath) {
Get-ChildItem $acctPath -ErrorAction SilentlyContinue | ForEach-Object {
$p = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
if ($p.'Account Name' -match '@') { $emailAddresses += $p.'Account Name'.Trim() }
if ($p.'Email' -match '@') { $emailAddresses += $p.'Email'.Trim() }
}
}
} catch {}
}
$outlookProfileData += @{
OfficeVerzija = $ver
ProfilIme = $profName
EmailAdrese = ($emailAddresses | Select-Object -Unique) -join "; "
NaziviRacuna = ($accountNames | Select-Object -Unique) -join "; "
}
}
}
$data.OutlookProfili = $outlookProfileData
# Licencirani i AV softver detekcija
Write-INFO " Licencirani softver i AV detekcija..."
$licKeywords = @(
"ESET","Kaspersky","Symantec","Norton","McAfee","Bitdefender","Avast","AVG",
"Malwarebytes","Sophos","Trend Micro","F-Secure","G Data","Panda","Webroot",
"Comodo","Cylance","CrowdStrike","SentinelOne","Carbon Black","Windows Defender",
"Avira","Emsisoft","Vipre","Bullguard","ZoneAlarm","Total AV","Dr.Web",
"Microsoft Office","Microsoft 365","Office 365","Microsoft Word","Microsoft Excel",
"Microsoft PowerPoint","Microsoft Outlook","Microsoft Teams","Microsoft Access",
"Microsoft Publisher","Microsoft Visio","Microsoft Project",
"Adobe Acrobat","Adobe Reader","Adobe Photoshop","Adobe Illustrator",
"Adobe Premiere","Adobe After Effects","Adobe InDesign","Adobe Lightroom",
"Adobe Creative Cloud","Adobe XD","Adobe Dreamweaver","Adobe Audition",
"Adobe Bridge","Adobe Animate","Adobe Substance",
"AutoCAD","Autodesk","SolidWorks","CATIA","Revit","Inventor","3ds Max","Maya",
"SketchUp Pro","Rhino","ArchiCAD","Vectorworks",
"SAP","Oracle","Pantheon","RRIF","Minimax","Vario","Navision",
"Microsoft Dynamics","QuickBooks","Sage","DATEV","Lexware",
"e-Racun","eConto","Moj-eRacun",
"TeamViewer","AnyDesk","RustDesk","LogMeIn","Splashtop","Citrix",
"Visual Studio","JetBrains","IntelliJ","PyCharm","WebStorm","Rider",
"Eclipse","NetBeans","Sublime Text",
"VMware","VirtualBox","Parallels",
"Acronis","Veeam","Backup Exec","Macrium","AOMEI","EaseUS",
"Slack","Zoom","Webex","GoToMeeting","Skype",
"WinZip","WinRAR","PDF-XChange","Foxit","Nitro PDF",
"Snagit","Camtasia","Bandicam",
"LastPass","1Password","Bitwarden","KeePass",
"Dropbox","Google Drive","Box"
)
$avKeywords = @(
"ESET","Kaspersky","Symantec","Norton","McAfee","Bitdefender","Avast","AVG",
"Malwarebytes","Sophos","Trend Micro","F-Secure","G Data","Panda","Webroot",
"Comodo","Cylance","CrowdStrike","SentinelOne","Carbon Black","Windows Defender",
"Avira","Emsisoft","Vipre","Bullguard","ZoneAlarm","Total AV","Dr.Web"
)
$data.LicenciraniSoftver = @($data.Softver) | Where-Object {
$naziv = $_.Naziv
$licKeywords | Where-Object { $naziv -like "*$_*" }
}
$data.AntivirusniProgrami = @($data.Softver) | Where-Object {
$naziv = $_.Naziv
$avKeywords | Where-Object { $naziv -like "*$_*" }
}
$wscAV = Get-CimInstance -Namespace "root\SecurityCenter2" -ClassName AntiVirusProduct -ErrorAction SilentlyContinue
$data.WSC_Antivirus = $wscAV | ForEach-Object {
$stateHex = "{0:X6}" -f $_.productState
$realtime = if($stateHex[2] -eq '1'){"Ukljucen"}else{"Iskljucen"}
$upToDate = if($stateHex[4] -eq '0'){"Azuran"}else{"Nije azuran"}
@{ Naziv = $_.displayName; Realtime = $realtime; Azurnost = $upToDate }
}
return $data
}
# ── Generiranje HTML izvjestaja ───────────────────────────────
function New-HTMLReport($data, $meta, $hostname, $timestamp) {
function Row($label, $value) {
if (-not $value -or $value -eq "") { $value = "N/A" }
return "
| $label | $value |
"
}
$sectionCounter = 0
function Section($title, $icon) {
$script:sectionCounter++
$num = $script:sectionCounter.ToString("D2")
return "$num$title
"
}
$css = @"
"@
# Statistike
$swCount = @($data.Softver).Count
$diskCount = @($data.Diskovi).Count
$ramTotal = ($data.RAM | ForEach-Object { $_.Velicina_GB } | Measure-Object -Sum).Sum
# Diskovi HTML
$diskoviHtml = ""
foreach ($d in @($data.Diskovi)) {
$diskoviHtml += ""
$diskoviHtml += Row "Model" $d.Model
$diskoviHtml += Row "Serijski broj" $d.Serijal
$diskoviHtml += Row "Velicina" "$($d.Velicina_GB) GB"
$diskoviHtml += Row "Tip medija" $d.Tip
$diskoviHtml += Row "Interface" $d.Interface
$diskoviHtml += Row "Firmware" $d.Firmware
foreach ($p in @($d.Particije)) {
$barPct = [math]::Min($p.Zauzeto_pct, 100)
$barColor = if($p.Zauzeto_pct -gt 90){"#c0392b"}elseif($p.Zauzeto_pct -gt 70){"#e67e22"}else{"#27ae60"}
$zauzetoGB = [math]::Round($p.Ukupno_GB - $p.Slobodno_GB, 2)
$diskoviHtml += "| Particija $($p.Slovo) |
$($p.Naziv) ($($p.FileSystem))
Ukupno: $($p.Ukupno_GB) GB |
Slobodno: $($p.Slobodno_GB) GB |
Zauzeto: $zauzetoGB GB ($($p.Zauzeto_pct)%)
|
"
}
$diskoviHtml += "
"
}
# RAM HTML
$ramHtml = "| Slot | Velicina | Tip | Brzina | Proizvodac | Part Number | Serijski broj |
"
foreach ($r in @($data.RAM)) {
$ramHtml += "| $($r.Slot) | $($r.Velicina_GB) GB | $($r.Tip) $($r.Forma) | $($r.Brzina_MHz) MHz | $($r.Proizvodac) | $($r.Part_Number) | $($r.Serijal) |
"
}
$ramHtml += "
"
# CPU HTML
$cpuHtml = ""
foreach ($c in @($data.CPU)) {
$cpuHtml += ""
$cpuHtml += Row "Naziv" "$($c.Naziv)"
$cpuHtml += Row "Jezgre / Logicke" "$($c.Jezgre) jezgri / $($c.LogJezgre) logickih"
$cpuHtml += Row "Maks. frekvencija" "$($c.MaxMHz) MHz"
$cpuHtml += Row "Socket" $c.Socket
$cpuHtml += Row "L2 Cache" "$($c.L2_Cache_KB) KB"
$cpuHtml += Row "L3 Cache" "$($c.L3_Cache_KB) KB"
$cpuHtml += "
"
}
# GPU HTML
$gpuHtml = ""
foreach ($g in @($data.GPU)) {
$gpuHtml += ""
$gpuHtml += Row "Naziv" "$($g.Naziv)"
$gpuHtml += Row "VRAM" "$($g.RAM_MB) MB"
$gpuHtml += Row "Rezolucija" $g.Rezolucija
$gpuHtml += Row "Driver verzija" $g.Driver_Ver
$gpuHtml += Row "Driver datum" $g.Driver_Datum
$gpuHtml += Row "Status" $g.Status
$gpuHtml += "
"
}
# Mreza HTML
$mrezaHtml = ""
foreach ($n in @($data.Mreza)) {
$mrezaHtml += ""
$mrezaHtml += Row "Adapter" $n.Opis
$mrezaHtml += Row "MAC adresa" "$($n.MAC)"
$mrezaHtml += Row "IP adresa" $n.IP
$mrezaHtml += Row "Subnet" $n.Subnet
$mrezaHtml += Row "Gateway" $n.Gateway
$mrezaHtml += Row "DNS" $n.DNS
$mrezaHtml += Row "DHCP" $(if($n.DHCP){"Ukljucen"}else{"Iskljucen"})
$mrezaHtml += "
"
}
# Monitori HTML
$monHtml = "| Proizvodac | Part Number | Serijski broj |
"
foreach ($m in @($data.Monitori)) {
$monHtml += "| $($m.Proizvodac) | $($m.PartNumber) | $($m.Serijal) |
"
}
$monHtml += "
"
# Antivirus HTML
$avHtml = ""
if (@($data.WSC_Antivirus).Count -gt 0) {
$avHtml += "Windows Security Center
"
$avHtml += "| Naziv | Real-time zastita | Azurnost definicija |
"
foreach ($av in @($data.WSC_Antivirus)) {
$rtBadge = if($av.Realtime -eq "Ukljucen"){"Ukljucen"}else{"Iskljucen"}
$azBadge = if($av.Azurnost -eq "Azuran"){"Azuran"}else{"Nije azuran"}
$avHtml += "| $($av.Naziv) | $rtBadge | $azBadge |
"
}
$avHtml += "
"
}
if (@($data.AntivirusniProgrami).Count -gt 0) {
$avHtml += "Detektirani AV u popisu softvera
"
foreach ($av in @($data.AntivirusniProgrami)) {
$avHtml += "
$($av.Naziv)
"
if ($av.Verzija) { $avHtml += "
v$($av.Verzija)
" }
if ($av.Izdavac) { $avHtml += "
$($av.Izdavac)
" }
$avHtml += "
"
}
$avHtml += "
"
}
if (-not $avHtml) { $avHtml = "Nije detektiran nijedan antivirusni program.
" }
# Licencirani softver HTML
$licSwCount = @($data.LicenciraniSoftver).Count
if ($licSwCount -gt 0) {
$licSwHtml = ""
foreach ($s in @($data.LicenciraniSoftver)) {
$licSwHtml += "
$($s.Naziv)
"
if ($s.Verzija) { $licSwHtml += "
v$($s.Verzija)
" }
if ($s.Izdavac) { $licSwHtml += "
$($s.Izdavac)
" }
$licSwHtml += "
"
}
$licSwHtml += "
"
} else { $licSwHtml = "Nisu detektirani licencirani programi.
" }
# Mapirani diskovi HTML
if (@($data.MapiraniDiskovi).Count -gt 0) {
$mapDiskHtml = "| Slovo | UNC putanja | Naziv | Ukupno | Slobodno |
"
foreach ($md in @($data.MapiraniDiskovi)) {
$mapDiskHtml += "| $($md.Slovo) | $($md.UNC_Put) | $($md.Naziv) | $($md.Velicina_GB) GB | $($md.Slobodno_GB) GB |
"
}
$mapDiskHtml += "
"
} else { $mapDiskHtml = "Nema mapiranih mreznih diskova.
" }
# Trenutni korisnik HTML
$cu = $data.TrenutniKorisnik
$curUserHtml = ""
$curUserHtml += Row "Korisnicko ime" "$($cu.Ime)"
$curUserHtml += Row "Domena" $cu.Domena
$curUserHtml += Row "Putanja profila" "$($cu.ProfilPutanja)"
$curUserHtml += Row "Velicina profila" "$($cu.VelicinaProfile)"
$curUserHtml += Row "AppData" "$($cu.AppData)"
$curUserHtml += Row "Temp mapa" "$($cu.Temp)"
$curUserHtml += Row "Zadani preglednik" $cu.DefaultBrowser
$curUserHtml += Row "OneDrive putanja" "$($cu.OneDrivePutanja)"
$curUserHtml += Row "OneDrive email" $cu.OneDriveEmail
$curUserHtml += Row "Outlook profili" $cu.OutlookProfili
$curUserHtml += "
"
if (@($cu.NedavniDokumenti).Count -gt 0) {
$curUserHtml += "Zadnjih 10 nedavnih dokumenata
"
$curUserHtml += "| Naziv | Zadnja izmjena |
"
foreach ($rd in @($cu.NedavniDokumenti)) {
$curUserHtml += "| $($rd.Naziv) | $($rd.Datum) |
"
}
$curUserHtml += "
"
}
# PST/OST HTML
$pstOstHtml = ""
$pstFiles = @($data.PstOstDatoteke) | Where-Object { $_.Tip -eq "PST" }
$ostFiles = @($data.PstOstDatoteke) | Where-Object { $_.Tip -eq "OST" }
if (@($data.PstOstDatoteke).Count -gt 0) {
if ($pstFiles.Count -gt 0) {
$pstOstHtml += "PST arhive ($($pstFiles.Count))
"
$pstOstHtml += "| Naziv | Korisnik | Velicina | Zadnja izmjena | Putanja |
"
foreach ($f in ($pstFiles | Sort-Object Velicina_MB -Descending)) {
$size = if($f.Velicina_GB -ge 1){"$($f.Velicina_GB) GB"}else{"$($f.Velicina_MB) MB"}
$pstOstHtml += "| $($f.Naziv) | $($f.Korisnik) | $size | $($f.Zadnja_izmjena) | $($f.Putanja) |
"
}
$pstOstHtml += "
"
}
if ($ostFiles.Count -gt 0) {
$pstOstHtml += "OST datoteke ($($ostFiles.Count))
"
$pstOstHtml += "| Naziv | Korisnik | Velicina | Zadnja izmjena |
"
foreach ($f in ($ostFiles | Sort-Object Velicina_MB -Descending)) {
$size = if($f.Velicina_GB -ge 1){"$($f.Velicina_GB) GB"}else{"$($f.Velicina_MB) MB"}
$pstOstHtml += "| $($f.Naziv) | $($f.Korisnik) | $size | $($f.Zadnja_izmjena) |
"
}
$pstOstHtml += "
"
$pstOstHtml += "Napomena: OST su lokalne kopije Exchange/M365 mailboxa. Putanja nije prikazana jer je uvijek unutar AppData profila.
"
}
} else {
$pstOstHtml = "Nisu pronadene PST/OST datoteke na ovom racunalu.
"
}
# Outlook profili HTML
$outlookProfHtml = ""
if (@($data.OutlookProfili).Count -gt 0) {
$outlookProfHtml = "| Profil | Email adrese | Nazivi racuna | Office verzija |
"
foreach ($op in @($data.OutlookProfili)) {
$emails = if($op.EmailAdrese){ $op.EmailAdrese }else{ "Nije pronadeno" }
$names = if($op.NaziviRacuna){ $op.NaziviRacuna }else{ "N/A" }
$outlookProfHtml += "| $($op.ProfilIme) | $emails | $names | Office $($op.OfficeVerzija) |
"
}
$outlookProfHtml += "
"
} else {
$outlookProfHtml = "Outlook profili nisu pronadeni (Outlook mozda nije instaliran ili nije konfiguriran).
"
}
# Softver HTML
$swHtml = ""
foreach ($s in @($data.Softver)) {
$swHtml += "
$($s.Naziv)
"
if ($s.Verzija) { $swHtml += "
v$($s.Verzija)
" }
if ($s.Izdavac) { $swHtml += "
$($s.Izdavac)
" }
$swHtml += "
"
}
$swHtml += "
"
# Azuriranja HTML
$azHtml = "| KB | Opis | Datum |
"
foreach ($a in @($data.Azuriranja)) {
$azHtml += "$($a.KB) | $($a.Opis) | $($a.Datum) |
"
}
$azHtml += "
"
# Startup HTML
$startHtml = "| Naziv | Putanja | Lokacija |
"
foreach ($s in @($data.Startup)) {
$startHtml += "| $($s.Naziv) | $($s.Putanja) | $($s.Lokacija) |
"
}
$startHtml += "
"
# Pisaci HTML
$pisaciHtml = "| Naziv | IP adresa | Port | Status | Zadani | Lokalni |
"
foreach ($p in @($data.Pisaci)) {
$zadani = if($p.Zadani){"Da"}else{"Ne"}
$ipDisplay = switch -Regex ($p.IP) {
'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$' { "$($p.IP)" }
'USB' { "USB" }
'Virtualni pisac' { "Virtualni" }
'WSD' { "WSD" }
'Fax' { "Fax" }
'Ispis u datoteku' { "Datoteka" }
default { "$($p.IP)" }
}
$pisaciHtml += "| $($p.Naziv) | $ipDisplay | $($p.PortName) | $($p.Status) | $zadani | $($p.Lokalni) |
"
}
$pisaciHtml += "
"
# Korisnici HTML
$korHtml = "| Korisnik | Puni naziv | Aktivan |
"
foreach ($k in @($data.Korisnici)) {
$akt = if($k.Aktivan){"Da"}else{"Ne"}
$korHtml += "| $($k.Ime) | $($k.Puni_naziv) | $akt |
"
}
$korHtml += "
"
$licBadge = switch($data.OS.Licenca) {
"Aktiviran" { "Aktiviran" }
default { "$($data.OS.Licenca)" }
}
$html = @"
PC Revizija - $hostname
$css
NOVATEC.ONE
PC Revizija · Interni alat
$swCount
Instaliranih programa
$(@($data.CPU)[0].Jezgre)
CPU jezgri
$(Section "Osnovno o racunalu" "🖥️")
$(Row "Naziv racunala" $data.Racunalo.Naziv)
$(Row "Proizvodac" $data.Racunalo.Proizvodac)
$(Row "Model" "$($data.Racunalo.Model)")
$(Row "Serijski broj" "$($data.Racunalo.Serijal)")
$(Row "Tip sustava" $data.Racunalo.Tip)
$(Row "Domena" $data.Racunalo.Domena)
$(Row "Prijavljeni korisnik" $data.Racunalo.Korisnik)
$(Section "Operacijski sustav" "🪟")
$(Row "Naziv" "$($data.OS.Naziv)")
$(Row "Verzija / Build" "$($data.OS.Verzija) (Build $($data.OS.Build))")
$(Row "Arhitektura" $data.OS.Arhitektura)
$(Row "Datum instalacije" $data.OS.Instaliran)
$(Row "Zadnje pokretanje" $data.OS.Zadnji_boot)
$(Row "Uptime" "$($data.OS.Uptime)")
$(Row "Aktivacija" $licBadge)
$(Row "Windows Product Key" "$($data.OS.ProductKey)
")
$(Row "RAM (ukupno / slobodno)" "$($data.OS.RAM_Ukupno) / $($data.OS.RAM_Slobodno)")
$(Section "BIOS / UEFI" "⚙️")
$(Row "Verzija" $data.Racunalo.BIOS_Verzija)
$(Row "Datum" $data.Racunalo.BIOS_Datum)
$(Row "Vendor" $data.Racunalo.BIOS_Vendor)
$(Section "Maticna ploca" "🔌")
$(Row "Proizvodac" $data.MaticnaPloca.Proizvodac)
$(Row "Model" "$($data.MaticnaPloca.Model)")
$(Row "Serijski broj" "$($data.MaticnaPloca.Serijal)")
$(Row "Verzija" $data.MaticnaPloca.Verzija)
$(Section "Procesor (CPU)" "đź§ ")
$cpuHtml
$(Section "Radna memorija (RAM)" "đź’ľ")
$ramHtml
$(Section "Graficka kartica (GPU)" "🎮")
$gpuHtml
$(Section "Diskovi i particije" "đź’ż")
$diskoviHtml
$(Section "Monitori" "🖥️")
$monHtml
$(Section "Mrezni adapteri" "đźŚ")
$mrezaHtml
$(Section "Korisnici" "👤")
$korHtml
$(Section "Pisaci" "🖨️")
$pisaciHtml
$(Section "Startup programi" "🚀")
$startHtml
$(Section "Zadnjih 15 azuriranja" "🔄")
$azHtml
$(Section "Antivirusna zastita" "🛡️")
$avHtml
$(Section "Licencirani softver" "🔑")
$licSwHtml
$(Section "Mapirani mrezni diskovi" "🗂️")
$mapDiskHtml
$(Section "Outlook profili i email racuni" "đź“§")
$outlookProfHtml
$(Section "PST i OST datoteke" "📨")
$pstOstHtml
$(Section "Trenutni korisnik" "👤")
$curUserHtml
$(Section "Instalirani softver ($swCount programa)" "📦")
$swHtml
"@
return $html
}
# ── Upload na API ─────────────────────────────────────────────
function Send-ToPortal($jsonPath) {
# Salje isti JSON koji vec ide na FTP, izravno portalu.
#
# Namjerno je izolirano od Send-Report: ako portal nije dostupan, kriv token
# ili je izostavljen, revizija se nastavlja normalno i FTP upload nije
# dotaknut. Ovaj poziv nikada ne smije oboriti skriptu.
if ([string]::IsNullOrWhiteSpace($PORTAL_TOKEN)) {
return $false
}
if (-not (Test-Path $jsonPath)) {
Write-ERR "Portal: JSON nije pronadjen: $jsonPath"
return $false
}
try {
Write-INFO "Saljem na portal $PORTAL_ENDPOINT ..."
$jsonBody = [System.IO.File]::ReadAllText($jsonPath, [System.Text.Encoding]::UTF8)
Add-Type -AssemblyName System.Net.Http
$portalClient = [System.Net.Http.HttpClient]::new()
$portalClient.Timeout = [TimeSpan]::FromSeconds($PORTAL_TIMEOUT_SEC)
# Token ide u zaglavlje, ne u URL: URL-ovi zavrsavaju u logovima proxyja.
$portalClient.DefaultRequestHeaders.Add("X-Novatec-Token", $PORTAL_TOKEN)
$content = [System.Net.Http.StringContent]::new(
$jsonBody, [System.Text.Encoding]::UTF8, "application/json")
try {
$portalResponse = $portalClient.PostAsync($PORTAL_ENDPOINT, $content).GetAwaiter().GetResult()
$portalBody = $portalResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult()
$portalCode = [int]$portalResponse.StatusCode
if ($portalCode -ge 200 -and $portalCode -lt 300) {
Write-OK "Portal: izvjestaj zaprimljen (HTTP $portalCode)."
return $true
}
elseif ($portalCode -eq 401) {
Write-ERR "Portal: token nije prihvacen. Provjerite \$PORTAL_TOKEN."
}
elseif ($portalCode -eq 429) {
Write-ERR "Portal: previse zahtjeva, pokusajte kasnije."
}
else {
Write-ERR "Portal: HTTP $portalCode - $portalBody"
}
return $false
}
finally {
$content.Dispose()
$portalClient.Dispose()
}
}
catch {
# Namjerno samo upozorenje: portal je dodatak, a ne uvjet za reviziju.
Write-ERR "Portal: slanje neuspjesno - $_"
return $false
}
}
function Send-Report($meta, $htmlPath, $jsonPath, $itFlowCsvPath, $timestamp, $hostname, $lozinkaText) {
Write-Header "UPLOAD NA SERVER"
$requiredFiles = @(
@{ Label = "HTML"; Path = $htmlPath },
@{ Label = "JSON"; Path = $jsonPath },
@{ Label = "ITFlow CSV"; Path = $itFlowCsvPath }
)
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_.Path -PathType Leaf) })
if ($missingFiles.Count -gt 0) {
foreach ($missing in $missingFiles) {
Write-ERR "$($missing.Label) datoteka nije pronadjena: $($missing.Path)"
}
Write-ERR "Upload nije pokrenut jer nedostaju obavezne datoteke."
return $false
}
# Koristi HttpClient — pouzdaniji za upload datoteka od Invoke-WebRequest
Add-Type -AssemblyName System.Net.Http
$client = [System.Net.Http.HttpClient]::new()
$client.Timeout = [TimeSpan]::FromSeconds(180)
$mpContent = [System.Net.Http.MultipartFormDataContent]::new()
function Add-StringField($name, $value) {
$sc = [System.Net.Http.StringContent]::new($value, [System.Text.Encoding]::UTF8)
$mpContent.Add($sc, $name)
}
function Add-FileField($fieldName, $filePath, $mimeType) {
$bytes = [System.IO.File]::ReadAllBytes($filePath)
$fc = [System.Net.Http.ByteArrayContent]::new($bytes)
$fc.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::new($mimeType)
$fileName = [System.IO.Path]::GetFileName($filePath)
$mpContent.Add($fc, $fieldName, $fileName)
}
# Tekstualna polja
Add-StringField "lozinka" $lozinkaText
Add-StringField "client_version" $CLIENT_VERSION
Add-StringField "ime_prezime" $meta.Ime
Add-StringField "tvrtka" $meta.Tvrtka
Add-StringField "odjel" $meta.Odjel
Add-StringField "napomena" $meta.Napomena
Add-StringField "racunalo" $hostname
Add-StringField "datum_revizije" $timestamp
Add-StringField "os_verzija" ((Get-CimInstance Win32_OperatingSystem).Caption)
# HTML izvjestaj
if (Test-Path $htmlPath) {
Write-INFO "Dodajem HTML izvjestaj ($([math]::Round((Get-Item $htmlPath).Length/1KB, 1)) KB)..."
Add-FileField "html_izvjestaj" $htmlPath "text/html"
Write-OK "HTML dodan."
} else {
Write-ERR "HTML izvjestaj nije pronadjen: $htmlPath"
}
# Izvorni JSON podaci
if (Test-Path $jsonPath) {
Write-INFO "Dodajem JSON podatke ($([math]::Round((Get-Item $jsonPath).Length/1KB, 1)) KB)..."
Add-FileField "json_podaci" $jsonPath "application/json"
Write-OK "JSON dodan."
} else {
Write-ERR "JSON nije pronadjen: $jsonPath"
}
# ITFlow CSV prema Assets templateu
if (Test-Path $itFlowCsvPath) {
Write-INFO "Dodajem ITFlow CSV ($([math]::Round((Get-Item $itFlowCsvPath).Length/1KB, 1)) KB)..."
Add-FileField "itflow_csv" $itFlowCsvPath "text/csv"
Write-OK "ITFlow CSV dodan."
} else {
Write-ERR "ITFlow CSV nije pronadjen: $itFlowCsvPath"
}
$uploadSucceeded = $false
try {
Write-INFO "Saljem na $API_ENDPOINT ..."
$response = $client.PostAsync($API_ENDPOINT, $mpContent).GetAwaiter().GetResult()
$respBody = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
$code = [int]$response.StatusCode
if ($code -ge 200 -and $code -lt 300) {
$parsedResponse = $null
try { $parsedResponse = $respBody | ConvertFrom-Json -ErrorAction Stop } catch {}
if ($null -eq $parsedResponse -or [string]$parsedResponse.api_version -ne $EXPECTED_API_VERSION) {
$returnedVersion = if ($parsedResponse) { [string]$parsedResponse.api_version } else { "nepoznata" }
Write-ERR "Server je vratio HTTP $code, ali API verzija nije valjana: $returnedVersion"
Write-ERR "Ocekivano: $EXPECTED_API_VERSION. Lokalni izvjestaji ostaju sacuvani."
Write-ERR "Odgovor: $respBody"
} else {
Write-OK "Upload uspjesan! HTTP $code, API $($parsedResponse.api_version)"
Write-INFO "Odgovor: $respBody"
$uploadSucceeded = $true
}
} else {
Write-ERR "Server vratio: HTTP $code"
Write-ERR "Odgovor: $respBody"
}
} catch {
Write-ERR "Upload neuspjesan: $_"
} finally {
$mpContent.Dispose()
$client.Dispose()
}
return $uploadSucceeded
}
# ════════════════════════════════════════════════════════════
# MAIN
# ════════════════════════════════════════════════════════════
Clear-Host
Write-Host ""
Write-Host " NOVATEC.ONE - PC REVIZIJA v4.0 CSV" -ForegroundColor Cyan
Write-Host " ================================" -ForegroundColor Cyan
Write-Host " https://sysinfo.novatec.one" -ForegroundColor DarkCyan
Write-Host ""
if (-not (Test-Admin)) {
Write-INFO "Pokretanje s administratorskim pravima..."
# Ponovno dohvati tocno v4 izdanje bez proxy/browser cachea i pokreni ga kao admin.
$scriptContent = (Invoke-WebRequest `
-Uri $AUDIT_SOURCE_URL `
-UseBasicParsing `
-Headers @{ "Cache-Control" = "no-cache"; "Pragma" = "no-cache" } `
-TimeoutSec 30
).Content
$tmpScript = "$env:TEMP
ovatec_audit_v4_run.ps1"
$scriptContent | Out-File -FilePath $tmpScript -Encoding UTF8 -Force
Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$tmpScript`"" -Verb RunAs
exit 0
}
# ── Provjera lozinke ─────────────────────────────────────────
Write-Header "AUTENTIFIKACIJA"
$maxPokusaja = 3
$pokusaj = 0
$autentificiran = $false
while ($pokusaj -lt $maxPokusaja) {
$pokusaj++
$lozinka = Read-Host " Lozinka" -AsSecureString
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($lozinka)
try {
$lozinkaText = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
} finally {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
# Provjeri lozinku na serveru
try {
$provjera = Invoke-WebRequest `
-Uri "$API_ENDPOINT" `
-Method POST `
-Body @{ action = "provjera_lozinke"; lozinka = $lozinkaText; client_version = $CLIENT_VERSION } `
-UseBasicParsing `
-TimeoutSec 15
$odgovor = $provjera.Content | ConvertFrom-Json
if ($odgovor.status -eq "ok") {
$serverVersion = [string]$odgovor.api_version
if ($serverVersion -ne $EXPECTED_API_VERSION) {
Write-ERR "Nepodudarne verzije: audit=$CLIENT_VERSION, server=$serverVersion."
Write-ERR "Na server postavi upload-revizija-v4.php i ponovno dohvati /audit?release=4.0-csv."
break
}
Write-OK "Autentifikacija uspjesna. API verzija: $serverVersion"
$autentificiran = $true
break
} else {
$preostalo = $maxPokusaja - $pokusaj
if ($preostalo -gt 0) {
Write-ERR "Pogresna lozinka. Preostalo pokusaja: $preostalo"
}
}
} catch {
$statusCode = $null
$serverBody = ""
try {
if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
$responseStream = $_.Exception.Response.GetResponseStream()
if ($responseStream) {
$reader = New-Object System.IO.StreamReader($responseStream)
try { $serverBody = $reader.ReadToEnd() } finally { $reader.Dispose() }
}
}
} catch {
# Dijagnostika ne smije prekinuti glavni error handler.
}
if ($statusCode) {
Write-ERR "Server je vratio HTTP $statusCode."
} else {
Write-ERR "Nije moguce kontaktirati server: $($_.Exception.Message)"
}
if (-not [string]::IsNullOrWhiteSpace($serverBody)) {
Write-ERR "Odgovor servera: $serverBody"
} else {
Write-ERR "Server nije vratio detalje. Provjeri PHP error log i je li upload-revizija.php na tocnoj putanji."
}
break
}
}
if (-not $autentificiran) {
Write-ERR "Autentifikacija neuspjesna. Skripta se zaustavlja."
Start-Sleep -Seconds 3
exit 1
}
$meta = Get-TechnicianInput
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$hostname = $env:COMPUTERNAME
$outDir = "$REPORTS_DIR\${hostname}_${timestamp}"
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
Write-Header "PRIKUPLJANJE PODATAKA"
$data = Get-SystemData
Write-INFO "Dohvacam podatke o jamstvu (maks. $WARRANTY_TIMEOUT_SEC s po pozivu)..."
$warranty = Get-DeviceWarranty `
-Manufacturer ([string]$data.Racunalo.Proizvodac) `
-SerialNumber ([string]$data.Racunalo.Serijal)
$data.Jamstvo = $warranty
switch ($warranty.Status) {
"Found" {
Write-OK "Jamstvo: $($warranty.Provider), $($warranty.StartDate) do $($warranty.EndDate)"
}
"Error" {
Write-ERR "Jamstvo nije dohvaćeno: $($warranty.Message)"
}
default {
Write-INFO "Jamstvo: $($warranty.Status) - $($warranty.Message)"
}
}
Write-INFO "Generiram HTML izvjestaj..."
$html = New-HTMLReport $data $meta $hostname $timestamp
$reportPath = "$outDir\revizija_${hostname}_${timestamp}.html"
$html | Out-File -FilePath $reportPath -Encoding UTF8
Write-OK "HTML izvjestaj spremljen: $reportPath"
# Spremi puni JSON sa svim prikupljenim podacima
Write-INFO "Generiram puni JSON..."
$fullJson = @{
meta = @{
ime_prezime = $meta.Ime
tvrtka = $meta.Tvrtka
odjel = $meta.Odjel
napomena = $meta.Napomena
racunalo = $hostname
datum_revizije = $timestamp
os_verzija = $data.OS.Naziv
}
os = $data.OS
racunalo = $data.Racunalo
maticna_ploca = $data.MaticnaPloca
cpu = $data.CPU
ram = $data.RAM
gpu = $data.GPU
diskovi = $data.Diskovi
mapirani_diskovi = $data.MapiraniDiskovi
monitori = $data.Monitori
mreza = $data.Mreza
korisnici = $data.Korisnici
trenutni_korisnik = $data.TrenutniKorisnik
pisaci = $data.Pisaci
startup = $data.Startup
azuriranja = $data.Azuriranja
softver = $data.Softver
licencirani_softver = $data.LicenciraniSoftver
antivirusni = $data.AntivirusniProgrami
wsc_antivirus = $data.WSC_Antivirus
pst_ost = $data.PstOstDatoteke
outlook_profili = $data.OutlookProfili
jamstvo = $data.Jamstvo
}
$jsonPath = "$outDir\podaci_${hostname}_${timestamp}.json"
Write-JsonUtf8NoBom -InputObject $fullJson -Path $jsonPath -Depth 20
Write-OK "JSON spremljen: $jsonPath ($([math]::Round((Get-Item $jsonPath).Length/1KB,1)) KB)"
# Dodatni ITFlow CSV: isto osnovno ime kao izvorni JSON, uz prefiks "IT-Flow".
# Primjer: podaci_PC_20260716_120000.json -> IT-Flowpodaci_PC_20260716_120000.csv
Write-INFO "Generiram ITFlow CSV..."
$itFlowRow = New-ITFlowCsvRow -Data $data -Meta $meta -Warranty $warranty
$itFlowFileName = "IT-Flow" + [System.IO.Path]::GetFileNameWithoutExtension($jsonPath) + ".csv"
$itFlowCsvPath = Join-Path $outDir $itFlowFileName
Write-ITFlowCsvUtf8NoBom -Row $itFlowRow -Path $itFlowCsvPath
Write-OK "ITFlow CSV spremljen: $itFlowCsvPath ($([math]::Round((Get-Item $itFlowCsvPath).Length/1KB,1)) KB)"
$uploadSucceeded = Send-Report $meta $reportPath $jsonPath $itFlowCsvPath $timestamp $hostname $lozinkaText
# Dodatno slanje portalu. Rezultat se biljezi, ali ne utjece na $uploadSucceeded
# niti na ostatak revizije - portal je drugo odrediste, ne zamjena za FTP.
$portalSucceeded = Send-ToPortal $jsonPath
Write-Header "ZAVRSENO"
# Kopiraj HTML u temp prije brisanja (da browser moze otvoriti)
$tempHtml = "$env:TEMP\novatec_revizija_${hostname}_${timestamp}.html"
Copy-Item -Path $reportPath -Destination $tempHtml -Force
Write-INFO "Otvaram izvjestaj u browseru..."
Start-Process $tempHtml
Start-Sleep -Seconds 1
if ($uploadSucceeded) {
Write-INFO "Brisem lokalnu kopiju nakon uspjesnog uploada..."
try {
Remove-Item -Path $outDir -Recurse -Force -ErrorAction Stop
Write-OK "Lokalna kopija obrisana."
} catch {
Write-ERR "Nije moguce obrisati lokalnu kopiju: $_"
}
} else {
Write-ERR "Upload nije potvrden. Lokalni izvjestaji NISU obrisani."
Write-INFO "Sacuvano za debug/retry: $outDir"
}
# Obriši temp HTML i ps1 skriptu nakon 30 sekundi u pozadini
$tmpScript = "$env:TEMP\novatec_audit_v4_run.ps1"
Start-Job -ScriptBlock {
param($html, $ps1)
Start-Sleep -Seconds 30
Remove-Item -Path $html -Force -ErrorAction SilentlyContinue
Remove-Item -Path $ps1 -Force -ErrorAction SilentlyContinue
} -ArgumentList $tempHtml, $tmpScript | Out-Null
Write-Host ""
Start-Sleep -Seconds 2