Загрузка данных


--[[
    CLIENT EXPOSURE AUDITOR v7 - CONSOLE EDITION

    Purpose:
      - One small AUDIT button only.
      - Clicking it performs one passive client-side exposure scan.
      - Findings are printed to the Roblox / Delta console.
      - No remotes/prompts are fired and game state is not intentionally mutated.
      - No executor introspection, hooks, decompilers or remote firing are used.

    Important limitations:
      - This cannot see server-only code or prove server-side validation quality.
      - With Workspace.StreamingEnabled, only currently streamed Workspace content is visible.
      - Replicated client code/data should be treated as inspectable by an exploiter.
]]

local PREFIX = "[AUDIT v7]"
local VERSION = "7.0-console"
local YIELD_EVERY = 500
local MAX_RESULTS = 20000
local MAX_LINE_LENGTH = 1200
local PRINT_ALL_INFO = false -- false = tablet-friendly; LOW/MEDIUM/HIGH + system/limitations are printed in detail

local function out(message)
    pcall(function()
        print(PREFIX .. " " .. tostring(message))
    end)
end

local function outWarn(message)
    pcall(function()
        warn(PREFIX .. " " .. tostring(message))
    end)
end

local function safeGetService(name)
    local ok, service = pcall(function()
        return game:GetService(name)
    end)
    if ok then
        return service
    end
    return nil
end

local Players = safeGetService("Players")
local RunService = safeGetService("RunService")
local CollectionService = safeGetService("CollectionService")
local ReplicatedStorage = safeGetService("ReplicatedStorage")
local ReplicatedFirst = safeGetService("ReplicatedFirst")
local Lighting = safeGetService("Lighting")
local SoundService = safeGetService("SoundService")
local TextChatService = safeGetService("TextChatService")
local Teams = safeGetService("Teams")
local StarterGui = safeGetService("StarterGui")
local StarterPlayer = safeGetService("StarterPlayer")
local StarterPack = safeGetService("StarterPack")

out("BOOT 01 script entered")

if not Players then
    outWarn("FATAL: Players service unavailable")
    return
end

local player = Players.LocalPlayer
if not player then
    out("BOOT 02 waiting for LocalPlayer")
    local deadline = os.clock() + 15
    repeat
        if RunService then
            RunService.Heartbeat:Wait()
        else
            task.wait(0.05)
        end
        player = Players.LocalPlayer
    until player or os.clock() >= deadline
end

if not player then
    outWarn("FATAL: LocalPlayer unavailable after 15 seconds")
    return
end

out("BOOT 02 LocalPlayer=" .. tostring(player.Name))

local playerGui = player:FindFirstChildOfClass("PlayerGui")
if not playerGui then
    local ok, value = pcall(function()
        return player:WaitForChild("PlayerGui", 15)
    end)
    if ok then
        playerGui = value
    end
end

if not playerGui then
    outWarn("FATAL: PlayerGui unavailable")
    return
end

out("BOOT 03 PlayerGui ready")

-- Clean previous console-auditor launchers only.
for _, name in ipairs({
    "client_exposure_auditor_v7_console",
    "client_exposure_auditor_v6_2",
    "client_exposure_auditor_v6_1",
    "client_exposure_auditor_v6",
    "client_exposure_auditor_v5",
}) do
    local old = playerGui:FindFirstChild(name)
    if old then
        pcall(function()
            old:Destroy()
        end)
    end
end

local gui = Instance.new("ScreenGui")
gui.Name = "client_exposure_auditor_v7_console"
gui.ResetOnSpawn = false
gui.IgnoreGuiInset = true
gui.DisplayOrder = 999999
gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
gui.Parent = playerGui

local button = Instance.new("TextButton")
button.Name = "audit"
button.Size = UDim2.fromOffset(116, 46)
button.Position = UDim2.fromOffset(16, 70)
button.BackgroundColor3 = Color3.fromRGB(57, 108, 230)
button.BorderSizePixel = 0
button.Text = "AUDIT"
button.TextColor3 = Color3.fromRGB(255, 255, 255)
button.TextSize = 16
button.Font = Enum.Font.GothamBold
button.AutoButtonColor = true
button.ZIndex = 100
button.Parent = gui

local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 10)
corner.Parent = button

local stroke = Instance.new("UIStroke")
stroke.Color = Color3.fromRGB(130, 166, 255)
stroke.Thickness = 1
stroke.Transparency = 0.25
stroke.Parent = button

out("BOOT 04 AUDIT button created")

local runAudit = nil
local initError = nil
local scanning = false
local lastButtonFire = 0

local function invokeButton()
    local now = os.clock()
    if now - lastButtonFire < 0.18 then
        return
    end
    lastButtonFire = now

    if scanning then
        outWarn("scan already running")
        return
    end

    if runAudit then
        task.spawn(runAudit)
    else
        outWarn("scanner is not ready" .. (initError and (" | init error: " .. tostring(initError)) or ""))
    end
end

-- Bind immediately. If later initialization fails, the button still reports it.
pcall(function()
    button.MouseButton1Down:Connect(invokeButton)
end)
pcall(function()
    button.MouseButton1Click:Connect(invokeButton)
end)
pcall(function()
    button.Activated:Connect(invokeButton)
end)

out("BOOT 05 button handlers connected")

local okInit, initErr = pcall(function()
    local SEVERITY_RANK = {
        INFO = 1,
        LOW = 2,
        MEDIUM = 3,
        HIGH = 4,
    }

    local SECRET_NAME_WORDS = {
        "password", "passwd", "secret", "token", "apikey", "api_key",
        "privatekey", "private_key", "authorization", "auth_token",
        "access_token", "refresh_token", "session_token", "webhook",
    }

    local REVIEW_WORDS = {
        "admin", "moderator", "owner", "money", "cash", "coin", "currency",
        "reward", "price", "damage", "cooldown", "inventory", "item", "weapon",
        "level", "xp", "experience", "ban", "anticheat", "anti_cheat", "purchase",
        "buy", "sell", "give", "grant", "teleport", "trade", "loot", "pickup",
        "finish", "goal", "claim", "redeem", "save", "data", "profile",
    }

    local REMOTE_REVIEW_WORDS = {
        "admin", "moderator", "owner", "money", "cash", "coin", "currency",
        "reward", "price", "damage", "cooldown", "inventory", "weapon", "ban",
        "purchase", "buy", "sell", "give", "grant", "teleport", "trade", "loot",
        "pickup", "finish", "goal", "claim", "redeem", "save", "delete", "kick",
    }

    local PHYSICS_REVIEW_WORDS = {
        "damage", "hit", "projectile", "bullet", "rocket", "weapon", "sword",
        "reward", "cash", "coin", "money", "pickup", "collect", "loot", "finish",
        "goal", "sell", "buy", "vehicle", "car", "ball", "objective",
    }

    local ASSET_PROPERTIES = {
        Sound = {"SoundId"},
        Animation = {"AnimationId"},
        Decal = {"Texture"},
        Texture = {"Texture"},
        MeshPart = {"MeshId", "TextureID"},
        SpecialMesh = {"MeshId", "TextureId"},
        ImageLabel = {"Image"},
        ImageButton = {"Image"},
        VideoFrame = {"Video"},
        Shirt = {"ShirtTemplate"},
        Pants = {"PantsTemplate"},
        SurfaceAppearance = {"ColorMap", "NormalMap", "MetalnessMap", "RoughnessMap"},
    }

    local function isA(obj, className)
        local ok, value = pcall(function()
            return obj:IsA(className)
        end)
        return ok and value == true
    end

    local function safeRead(obj, property)
        local ok, value = pcall(function()
            return obj[property]
        end)
        if ok then
            return value
        end
        return nil
    end

    local function safeFullName(obj)
        local ok, value = pcall(function()
            return obj:GetFullName()
        end)
        if ok and value then
            return value
        end
        return tostring(safeRead(obj, "Name") or obj)
    end

    local function valueToString(value)
        local t = typeof(value)
        if t == "Instance" then
            return safeFullName(value)
        elseif t == "string" then
            if #value > 240 then
                return value:sub(1, 240) .. "..."
            end
            return value
        elseif t == "table" then
            return "<table>"
        end
        return tostring(value)
    end

    local function containsAny(text, words)
        local s = string.lower(tostring(text or ""))
        for _, word in ipairs(words) do
            if string.find(s, word, 1, true) then
                return true, word
            end
        end
        return false, nil
    end

    local function stringLooksSensitive(value)
        if typeof(value) ~= "string" then
            return false
        end
        local s = string.lower(value)
        local patterns = {
            "discord.com/api/webhooks/",
            "discordapp.com/api/webhooks/",
            "hooks.slack.com/services/",
            "-----begin private key-----",
            "-----begin rsa private key-----",
            "bearer ",
            "xoxb-",
            "sk_live_",
        }
        for _, pattern in ipairs(patterns) do
            if string.find(s, pattern, 1, true) then
                return true
            end
        end
        return false
    end

    local function classifyName(path)
        if containsAny(path, SECRET_NAME_WORDS) then
            return "MEDIUM"
        end
        if containsAny(path, REVIEW_WORDS) then
            return "LOW"
        end
        return "INFO"
    end

    local function classifyState(name, value)
        local hasSecretName = containsAny(name, SECRET_NAME_WORDS)
        if stringLooksSensitive(value) then
            return "HIGH"
        end
        if hasSecretName and value ~= nil and tostring(value) ~= "" then
            return "HIGH"
        end
        if hasSecretName then
            return "MEDIUM"
        end
        return classifyName(name)
    end

    local function stateValueForDisplay(name, value)
        if classifyState(name, value) == "HIGH" then
            if typeof(value) == "string" then
                return "<REDACTED STRING len=" .. tostring(#value) .. ">"
            end
            return "<REDACTED " .. string.upper(typeof(value)) .. ">"
        end
        return valueToString(value)
    end

    local function remoteSeverity(path)
        if containsAny(path, SECRET_NAME_WORDS) then
            return "MEDIUM"
        end
        if containsAny(path, REMOTE_REVIEW_WORDS) then
            return "MEDIUM"
        end
        return "LOW"
    end

    local function isAuditorObject(obj)
        local ok, value = pcall(function()
            return obj == gui or obj:IsDescendantOf(gui)
        end)
        return ok and value or false
    end

    local function isCharacterObject(obj)
        local current = obj
        local guard = 0
        while current and current ~= workspace and guard < 64 do
            guard = guard + 1
            if isA(current, "Model") then
                local ok, humanoid = pcall(function()
                    return current:FindFirstChildOfClass("Humanoid")
                end)
                if ok and humanoid then
                    return true
                end
            end
            current = safeRead(current, "Parent")
        end
        return false
    end

    local function nearestBasePart(obj)
        local current = obj
        local guard = 0
        while current and current ~= game and guard < 64 do
            guard = guard + 1
            if isA(current, "BasePart") then
                return current
            end
            current = safeRead(current, "Parent")
        end
        return nil
    end

    local function assemblyRoot(part)
        if not part or not isA(part, "BasePart") then
            return nil
        end
        local root = safeRead(part, "AssemblyRootPart")
        if root and isA(root, "BasePart") then
            return root
        end
        return part
    end

    local function networkInfo(part)
        local ownerText = "unavailable"
        local autoText = "unavailable"
        local localOwned = false

        local okOwner, owner = pcall(function()
            return part:GetNetworkOwner()
        end)
        if okOwner then
            if owner then
                ownerText = tostring(safeRead(owner, "Name") or owner)
                localOwned = owner == player
            else
                ownerText = "server / nil"
            end
        end

        local okAuto, isAuto = pcall(function()
            return part:GetNetworkOwnershipAuto()
        end)
        if okAuto then
            autoText = tostring(isAuto)
        end

        return ownerText, autoText, localOwned
    end

    local function buildScanRoots()
        local roots = {}
        local candidates = {
            ReplicatedFirst,
            ReplicatedStorage,
            workspace,
            Players,
            Lighting,
            SoundService,
            TextChatService,
            Teams,
            StarterGui,
            StarterPlayer,
            StarterPack,
        }
        for _, root in ipairs(candidates) do
            if root then
                table.insert(roots, root)
            end
        end
        return roots
    end

    local function captureSnapshot()
        local output = {}
        local seen = {}

        for _, root in ipairs(buildScanRoots()) do
            if not seen[root] then
                seen[root] = true
                if not isAuditorObject(root) then
                    table.insert(output, root)
                end

                local ok, descendants = pcall(function()
                    return root:GetDescendants()
                end)
                if ok and descendants then
                    for _, obj in ipairs(descendants) do
                        if not seen[obj] then
                            seen[obj] = true
                            if not isAuditorObject(obj) then
                                table.insert(output, obj)
                            end
                        end
                    end
                end
            end
        end

        return output
    end

    local function oneLine(text)
        text = tostring(text or "")
        text = text:gsub("[\r\n\t]+", " ")
        if #text > MAX_LINE_LENGTH then
            return text:sub(1, MAX_LINE_LENGTH) .. "..."
        end
        return text
    end

    runAudit = function()
        if scanning then
            outWarn("scan already running")
            return
        end

        scanning = true
        button.Text = "SCANNING..."
        button.AutoButtonColor = false

        local scanOk, scanErr = pcall(function()
            local started = os.clock()
            local results = {}
            local resultKeys = {}
            local omitted = 0
            local objectErrors = 0
            local counts = {
                HIGH = 0,
                MEDIUM = 0,
                LOW = 0,
                INFO = 0,
            }

            local categoryCounts = {}

            local function addResult(category, severity, path, detail)
                severity = severity or "INFO"
                path = tostring(path or "")
                detail = tostring(detail or "")

                local key = table.concat({category, severity, path, detail}, "\31")
                if resultKeys[key] then
                    return
                end
                resultKeys[key] = true

                if #results >= MAX_RESULTS then
                    omitted = omitted + 1
                    return
                end

                table.insert(results, {
                    category = category,
                    severity = severity,
                    path = path,
                    detail = detail,
                })

                if counts[severity] ~= nil then
                    counts[severity] = counts[severity] + 1
                end
                categoryCounts[category] = (categoryCounts[category] or 0) + 1
            end

            local streaming = safeRead(workspace, "StreamingEnabled")

            addResult("SYSTEM", "INFO", "Version", VERSION)
            addResult("SYSTEM", "INFO", "PlaceId", tostring(game.PlaceId))
            addResult("SYSTEM", "INFO", "GameId", tostring(game.GameId))
            addResult("SYSTEM", "INFO", "Player", tostring(player.Name))
            addResult("SYSTEM", "INFO", "StreamingEnabled", tostring(streaming))
            addResult(
                "LIMITATION",
                "LOW",
                "Server validation is not visible from client",
                "This scan cannot prove whether OnServerEvent/OnServerInvoke handlers validate types, permissions, distance, state or rate limits."
            )
            if streaming == true then
                addResult(
                    "LIMITATION",
                    "LOW",
                    "Workspace is streamed",
                    "Workspace findings cover only objects currently streamed to this client."
                )
            end

            out("============================================================")
            out("FULL PASSIVE SCAN STARTED")
            out("capturing client-visible snapshot...")

            local snapshot = captureSnapshot()
            out("snapshot captured: " .. tostring(#snapshot) .. " objects")

            local seenPhysicsRoots = {}

            for i, obj in ipairs(snapshot) do
                local okObject, errObject = pcall(function()
                    local path = safeFullName(obj)

                    -- Network / local communication surfaces.
                    if isA(obj, "RemoteEvent") or isA(obj, "RemoteFunction") or isA(obj, "UnreliableRemoteEvent") then
                        addResult(
                            "REMOTE SURFACE",
                            remoteSeverity(path),
                            path,
                            tostring(safeRead(obj, "ClassName") or "Remote") .. " | Client can address this remote; review its server-side validation and rate limits."
                        )
                    elseif isA(obj, "BindableEvent") or isA(obj, "BindableFunction") then
                        addResult(
                            "LOCAL BUS",
                            "INFO",
                            path,
                            tostring(safeRead(obj, "ClassName") or "Bindable") .. " | Same-side communication only; not a client-server remote by itself."
                        )
                    end

                    -- Replicated client code.
                    if isA(obj, "LocalScript") then
                        local sev = classifyName(path)
                        if SEVERITY_RANK[sev] < SEVERITY_RANK.LOW then
                            sev = "LOW"
                        end
                        addResult("CLIENT CODE", sev, path, "LocalScript is client-visible; assume logic/constants can be inspected or altered.")
                    elseif isA(obj, "ModuleScript") then
                        local sev = classifyName(path)
                        if SEVERITY_RANK[sev] < SEVERITY_RANK.LOW then
                            sev = "LOW"
                        end
                        addResult("CLIENT MODULE", sev, path, "Replicated ModuleScript should be treated as inspectable by the client.")
                    elseif isA(obj, "Script") then
                        local rc = safeRead(obj, "RunContext")
                        if rc and string.find(tostring(rc), "Client", 1, true) then
                            local sev = classifyName(path)
                            if SEVERITY_RANK[sev] < SEVERITY_RANK.LOW then
                                sev = "LOW"
                            end
                            addResult("CLIENT SCRIPT", sev, path, "Script.RunContext=" .. tostring(rc))
                        end
                    end

                    -- Attributes / tags / ValueBase state.
                    local okAttrs, attrs = pcall(function()
                        return obj:GetAttributes()
                    end)
                    if okAttrs and attrs then
                        for name, value in pairs(attrs) do
                            local keyPath = path .. ".@" .. tostring(name)
                            addResult("ATTRIBUTE", classifyState(keyPath, value), keyPath, stateValueForDisplay(keyPath, value))
                        end
                    end

                    if CollectionService then
                        local okTags, tags = pcall(function()
                            return CollectionService:GetTags(obj)
                        end)
                        if okTags and tags then
                            for _, tag in ipairs(tags) do
                                addResult("TAG", classifyName(path .. "." .. tostring(tag)), path, tostring(tag))
                            end
                        end
                    end

                    if isA(obj, "ValueBase") then
                        local value = safeRead(obj, "Value")
                        addResult("VALUE", classifyState(path, value), path, stateValueForDisplay(path, value))
                    end

                    if isA(obj, "Tool") then
                        addResult("TOOL", "INFO", path, "Client-visible Tool")
                    end

                    -- Interaction surfaces.
                    if isA(obj, "ProximityPrompt") or isA(obj, "ClickDetector") or isA(obj, "DragDetector") then
                        local severity = classifyName(path)
                        if SEVERITY_RANK[severity] < SEVERITY_RANK.LOW then
                            severity = "LOW"
                        end

                        local part = nearestBasePart(obj)
                        local root = assemblyRoot(part)
                        local unanchored = root and safeRead(root, "Anchored") == false
                        if unanchored then
                            severity = "MEDIUM"
                        end

                        if isA(obj, "ProximityPrompt") then
                            local detail = "Action=" .. valueToString(safeRead(obj, "ActionText"))
                                .. " | Distance=" .. valueToString(safeRead(obj, "MaxActivationDistance"))
                                .. " | Hold=" .. valueToString(safeRead(obj, "HoldDuration"))
                                .. " | Enabled=" .. valueToString(safeRead(obj, "Enabled"))
                                .. " | Server must validate authorization/state/rate"
                            if unanchored then
                                detail = detail .. " | parent assembly unanchored"
                            end
                            addResult("PROMPT", severity, path, detail)
                        elseif isA(obj, "ClickDetector") then
                            local detail = "Distance=" .. valueToString(safeRead(obj, "MaxActivationDistance"))
                                .. " | Treat server effects as client-triggerable"
                            if unanchored then
                                detail = detail .. " | parent assembly unanchored"
                            end
                            addResult("CLICK", severity, path, detail)
                        else
                            addResult("DRAG", severity, path, "Client manipulation surface" .. (unanchored and " | parent assembly unanchored" or ""))
                        end
                    end

                    -- Physics assemblies and controllers.
                    if isA(obj, "BasePart") then
                        local root = assemblyRoot(obj)
                        if root and not seenPhysicsRoots[root] then
                            seenPhysicsRoots[root] = true
                            if not isCharacterObject(root) and safeRead(root, "Anchored") == false then
                                local rootPath = safeFullName(root)
                                local ownerText, autoText, localOwned = networkInfo(root)
                                local riskyName = containsAny(rootPath, PHYSICS_REVIEW_WORDS)
                                local canTouch = safeRead(root, "CanTouch")
                                local severity = "LOW"
                                if riskyName and (localOwned or canTouch == true) then
                                    severity = "MEDIUM"
                                end

                                local detail = "Owner=" .. ownerText
                                    .. " | AutoOwnership=" .. autoText
                                    .. " | CanTouch=" .. valueToString(canTouch)
                                    .. " | CanCollide=" .. valueToString(safeRead(root, "CanCollide"))
                                    .. " | Massless=" .. valueToString(safeRead(root, "Massless"))
                                if localOwned then
                                    detail = detail .. " | CURRENTLY OWNED BY LOCAL CLIENT"
                                end
                                if riskyName then
                                    detail = detail .. " | gameplay-sensitive name: review server-side physics/touch validation"
                                end
                                addResult("PHYSICS ASSEMBLY", severity, rootPath, detail)
                            end
                        end
                    elseif isA(obj, "Constraint")
                        or isA(obj, "BodyMover")
                        or isA(obj, "LinearVelocity")
                        or isA(obj, "AngularVelocity")
                        or isA(obj, "VectorForce")
                        or isA(obj, "AlignPosition")
                        or isA(obj, "AlignOrientation") then
                        if not isCharacterObject(obj) then
                            local severity = classifyName(path)
                            if SEVERITY_RANK[severity] < SEVERITY_RANK.LOW then
                                severity = "LOW"
                            end
                            addResult("PHYSICS CTRL", severity, path, tostring(safeRead(obj, "ClassName") or "Physics controller"))
                        end
                    end

                    -- Asset references.
                    for className, props in pairs(ASSET_PROPERTIES) do
                        if isA(obj, className) then
                            for _, prop in ipairs(props) do
                                local value = safeRead(obj, prop)
                                if value ~= nil and tostring(value) ~= "" then
                                    addResult("ASSET", "INFO", path .. "." .. prop, valueToString(value))
                                end
                            end
                        end
                    end
                end)

                if not okObject then
                    objectErrors = objectErrors + 1
                    if objectErrors <= 25 then
                        outWarn("object scan error #" .. tostring(objectErrors) .. " at index " .. tostring(i) .. ": " .. oneLine(errObject))
                    end
                end

                if i % YIELD_EVERY == 0 then
                    out("progress " .. tostring(i) .. "/" .. tostring(#snapshot))
                    if RunService then
                        RunService.Heartbeat:Wait()
                    else
                        task.wait()
                    end
                end
            end

            table.sort(results, function(a, b)
                local ar = SEVERITY_RANK[a.severity] or 1
                local br = SEVERITY_RANK[b.severity] or 1
                if ar ~= br then
                    return ar > br
                end
                if a.category ~= b.category then
                    return a.category < b.category
                end
                return a.path < b.path
            end)

            out("============================================================")
            out("SCAN COMPLETE")
            out("objects=" .. tostring(#snapshot)
                .. " | results=" .. tostring(#results)
                .. " | HIGH=" .. tostring(counts.HIGH)
                .. " | MEDIUM=" .. tostring(counts.MEDIUM)
                .. " | LOW=" .. tostring(counts.LOW)
                .. " | INFO=" .. tostring(counts.INFO)
                .. " | object_errors=" .. tostring(objectErrors)
                .. " | omitted=" .. tostring(omitted))
            out("elapsed=" .. string.format("%.2f", os.clock() - started) .. "s")

            local categoryNames = {}
            for category in pairs(categoryCounts) do
                table.insert(categoryNames, category)
            end
            table.sort(categoryNames)
            for _, category in ipairs(categoryNames) do
                out("COUNT " .. category .. "=" .. tostring(categoryCounts[category]))
            end

            out("-------------------- FINDINGS -------------------------------")
            local printed = 0
            local hiddenInfo = 0
            for index, item in ipairs(results) do
                local show = PRINT_ALL_INFO
                    or item.severity ~= "INFO"
                    or item.category == "SYSTEM"
                    or item.category == "LIMITATION"

                if show then
                    printed = printed + 1
                    local line = string.format(
                        "[%04d] [%s] [%s] %s%s",
                        index,
                        item.severity,
                        item.category,
                        oneLine(item.path),
                        item.detail ~= "" and (" | " .. oneLine(item.detail)) or ""
                    )

                    if item.severity == "HIGH" or item.severity == "MEDIUM" then
                        outWarn(line)
                    else
                        out(line)
                    end
                else
                    hiddenInfo = hiddenInfo + 1
                end
            end
            out("printed_findings=" .. tostring(printed) .. " | hidden_INFO=" .. tostring(hiddenInfo)
                .. " | set PRINT_ALL_INFO=true at top if you need every informational asset/value/tag line")

            if omitted > 0 then
                outWarn(tostring(omitted) .. " additional result(s) omitted because MAX_RESULTS=" .. tostring(MAX_RESULTS))
            end
            if objectErrors > 25 then
                outWarn(tostring(objectErrors - 25) .. " additional per-object scan errors were suppressed")
            end
            if streaming == true then
                outWarn("Workspace.StreamingEnabled=true: Workspace inventory is partial on this client")
            end

            out("============================================================")
            out("END OF REPORT")
        end)

        scanning = false
        button.AutoButtonColor = true

        if scanOk then
            button.Text = "AUDIT AGAIN"
        else
            button.Text = "AUDIT ERROR"
            outWarn("FATAL SCAN ERROR: " .. tostring(scanErr))
        end
    end
end)

if not okInit then
    initError = tostring(initErr)
    button.Text = "AUDIT ERROR"
    outWarn("INIT ERROR: " .. tostring(initErr))
else
    button.Text = "AUDIT"
    out("BOOT 06 scanner initialized")
    out("READY - press AUDIT; full report will be printed to console")
end