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


--[[
    CLIENT EXPOSURE AUDITOR v6.1
    Passive security / client-exposure review.

    Behavior:
      1) On execution, ONLY a small "AUDIT" launcher appears.
      2) Clicking it opens the debugger menu.
      3) No heavy scan runs until you press a scan button.
      4) Full Scan captures client-visible content once, then analyzes that snapshot.
      5) Results are paginated, deduplicated and severity-sorted.
      6) Close (X) hides the window; launcher remains available.

    Scope / limitations:
      - Scan logic uses normal Roblox client APIs only.
      - If an executor exposes gethui(), it is used only as a safer GUI parent.
      - Read-only: does NOT fire/invoke remotes, prompts, detectors or mutate game state.
      - Does NOT use executor-specific introspection, hooks or decompilers.
      - A client-only audit CANNOT prove whether server handlers validate requests correctly.
      - With Workspace.StreamingEnabled, Workspace enumeration is only the currently streamed subset.
      - Replicated LocalScripts / ModuleScripts must be treated as inspectable by an exploiter.
]]

local Players = game:GetService("Players")
local CollectionService = game:GetService("CollectionService")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local Lighting = game:GetService("Lighting")
local SoundService = game:GetService("SoundService")
local TextChatService = game:GetService("TextChatService")
local Teams = game:GetService("Teams")
local StarterGui = game:GetService("StarterGui")
local StarterPlayer = game:GetService("StarterPlayer")
local StarterPack = game:GetService("StarterPack")

local player = Players.LocalPlayer
if not player then
    warn("[AUDIT] LocalPlayer unavailable.")
    return
end

local playerGui = player:WaitForChild("PlayerGui")
local auditorGui = nil

----------------------------------------------------------------
-- GUI PARENT / CLEAN PREVIOUS INSTANCE
----------------------------------------------------------------

local GUI_NAME = "client_exposure_auditor_v6_1"

local function resolveGuiParent()
    -- Studio / normal client: PlayerGui.
    -- Injected client: prefer gethui() when the executor exposes it.
    local parent = playerGui

    local ok, hiddenGui = pcall(function()
        if type(gethui) == "function" then
            return gethui()
        end
        return nil
    end)

    if ok and typeof(hiddenGui) == "Instance" then
        parent = hiddenGui
    end

    return parent
end

local guiParent = resolveGuiParent()

local cleanupParents = {playerGui}
if guiParent ~= playerGui then
    table.insert(cleanupParents, guiParent)
end

for _, parent in ipairs(cleanupParents) do
    for _, name in ipairs({
        "client_exposure_auditor_v5",
        "client_exposure_auditor_v6",
        "client_exposure_auditor_v6_1",
    }) do
        local old = parent:FindFirstChild(name)
        if old then
            old:Destroy()
        end
    end
end

----------------------------------------------------------------
-- COLORS / CONSTANTS
----------------------------------------------------------------

local C = {
	bg = Color3.fromRGB(14, 15, 18),
	panel = Color3.fromRGB(21, 23, 28),
	panel2 = Color3.fromRGB(28, 30, 36),
	panel3 = Color3.fromRGB(35, 38, 45),
	text = Color3.fromRGB(236, 238, 242),
	muted = Color3.fromRGB(157, 163, 175),
	accent = Color3.fromRGB(77, 133, 255),
	accent2 = Color3.fromRGB(99, 154, 255),
	ok = Color3.fromRGB(83, 196, 126),
	low = Color3.fromRGB(114, 164, 255),
	warn = Color3.fromRGB(242, 187, 74),
	danger = Color3.fromRGB(239, 97, 97),
	border = Color3.fromRGB(58, 62, 73),
}

local PAGE_SIZE = 80
local MAX_RESULTS_TOTAL = 30000
local SNAPSHOT_YIELD_EVERY = 800

----------------------------------------------------------------
-- UTIL
----------------------------------------------------------------

local function corner(parent, radius)
    local x = Instance.new("UICorner")
    x.CornerRadius = UDim.new(0, radius or 8)
    x.Parent = parent
    return x
end

local function stroke(parent, color, thickness, transparency)
    local x = Instance.new("UIStroke")
    x.Color = color or C.border
    x.Thickness = thickness or 1
    x.Transparency = transparency or 0
    x.Parent = parent
    return x
end

local function safeFullName(obj)
    local ok, value = pcall(function()
        return obj:GetFullName()
    end)
    if ok then
        return value
    end
    return obj.Name
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 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 == "Vector3" or t == "Vector2" or t == "CFrame"
        or t == "Color3" or t == "UDim" or t == "UDim2"
        or t == "NumberRange" or t == "NumberSequence"
        or t == "ColorSequence" or t == "BrickColor"
        or t == "EnumItem" then
        return tostring(value)
    elseif t == "table" then
        return "<table>"
    end

    return tostring(value)
end

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 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 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 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, nil
    end

    local s = string.lower(value)
    local patterns = {
        {"discord.com/api/webhooks/", "webhook URL"},
        {"discordapp.com/api/webhooks/", "webhook URL"},
        {"hooks.slack.com/services/", "webhook URL"},
        {"-----begin private key-----", "private key"},
        {"-----begin rsa private key-----", "private key"},
        {"bearer ", "bearer credential"},
        {"xoxb-", "Slack token-like value"},
        {"sk_live_", "secret-key-like value"},
    }

    for _, pair in ipairs(patterns) do
        if string.find(s, pair[1], 1, true) then
            return true, pair[2]
        end
    end

    return false, nil
end

local function classifyName(pathOrName)
    local hasSecret = containsAny(pathOrName, SECRET_NAME_WORDS)
    if hasSecret then
        return "MEDIUM"
    end

    local needsReview = containsAny(pathOrName, REVIEW_WORDS)
    if needsReview then
        return "LOW"
    end

    return "INFO"
end

local function classifyState(name, value)
    local hasSecretName = containsAny(name, SECRET_NAME_WORDS)
    local valueLooksSecret = stringLooksSensitive(value)

    if valueLooksSecret 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)
    local severity = classifyState(name, value)
    if severity == "HIGH" then
        local t = typeof(value)
        if t == "string" then
            return string.format("<REDACTED STRING len=%d>", #value)
        end
        return "<REDACTED " .. string.upper(t) .. ">"
    end
    return valueToString(value)
end

local function isAuditorObject(obj)
    if not auditorGui then
        return false
    end

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

local function isCharacterObject(obj)
    local current = obj
    while current and current ~= workspace do
        if current:IsA("Model") and current:FindFirstChildOfClass("Humanoid") then
            return true
        end
        current = current.Parent
    end
    return false
end

local function nearestBasePart(obj)
    local current = obj
    while current and current ~= game do
        if current:IsA("BasePart") then
            return current
        end
        current = current.Parent
    end
    return nil
end

local function assemblyRoot(part)
    if not part or not part:IsA("BasePart") then
        return nil
    end

    local root = safeRead(part, "AssemblyRootPart")
    if root and root:IsA("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 = owner.Name
            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 = {
        ReplicatedFirst,
        ReplicatedStorage,
        workspace,
        Players,
        Lighting,
        SoundService,
        TextChatService,
        Teams,
        StarterGui,
        StarterPlayer,
        StarterPack,
    }

    return roots
end

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

    for _, root in ipairs(buildScanRoots()) do
        if root and 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 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


----------------------------------------------------------------
-- GUI ROOT
----------------------------------------------------------------

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

----------------------------------------------------------------
-- LAUNCH BUTTON
----------------------------------------------------------------

local launcher = Instance.new("TextButton")
launcher.Name = "launcher"
launcher.Size = UDim2.fromOffset(92, 40)
launcher.Position = UDim2.new(0, 18, 0.5, -20)
launcher.BackgroundColor3 = C.accent
launcher.BorderSizePixel = 0
launcher.AutoButtonColor = true
launcher.Text = "AUDIT"
launcher.TextColor3 = Color3.new(1, 1, 1)
launcher.TextSize = 15
launcher.Font = Enum.Font.GothamBold
launcher.ZIndex = 100
launcher.Parent = gui
corner(launcher, 10)
stroke(launcher, Color3.fromRGB(125, 166, 255), 1, 0.35)

----------------------------------------------------------------
-- MAIN WINDOW
----------------------------------------------------------------

local main = Instance.new("Frame")
main.Name = "main"
main.Size = UDim2.new(0.88, 0, 0.82, 0)
main.Position = UDim2.new(0.06, 0, 0.09, 0)
main.BackgroundColor3 = C.bg
main.BorderSizePixel = 0
main.Visible = false
main.ZIndex = 20
main.Parent = gui
corner(main, 12)
stroke(main, C.border, 1, 0.2)

local sizeConstraint = Instance.new("UISizeConstraint")
sizeConstraint.MaxSize = Vector2.new(1450, 900)
sizeConstraint.Parent = main

-- Bind opening IMMEDIATELY. If a later compatibility error occurs,
-- the launcher still opens this base window instead of becoming dead.
local function openMain()
    gui.Enabled = true
    main.Visible = true
    launcher.Visible = false
end

local function hideMain()
    main.Visible = false
    launcher.Visible = true
end

local function bindButton(button, callback)
    -- MouseButton1Down is widely reliable in injected GUI environments.
    -- Activated remains as a touch/gamepad/normal-client fallback.
    local lastFire = -math.huge

    local function fire()
        local now = os.clock()
        if now - lastFire < 0.35 then
            return
        end
        lastFire = now
        callback()
    end

    button.MouseButton1Down:Connect(fire)
    button.Activated:Connect(fire)
end

-- Multiple idempotent input paths are intentional for executor compatibility.
launcher.MouseButton1Down:Connect(openMain)
launcher.MouseButton1Click:Connect(openMain)
launcher.Activated:Connect(openMain)

local bootNotice = Instance.new("TextLabel")
bootNotice.Name = "boot_notice"
bootNotice.Size = UDim2.new(1, -32, 1, -32)
bootNotice.Position = UDim2.fromOffset(16, 16)
bootNotice.BackgroundColor3 = C.panel
bootNotice.BorderSizePixel = 0
bootNotice.Text = "AUDITOR INITIALIZING...\n\nIf this message stays on screen, execution stopped during setup.\nCheck the executor console / Roblox developer console for the first error."
bootNotice.TextColor3 = C.text
bootNotice.TextSize = 15
bootNotice.Font = Enum.Font.GothamMedium
bootNotice.TextWrapped = true
bootNotice.ZIndex = 1000
bootNotice.Parent = main
corner(bootNotice, 10)
stroke(bootNotice, C.border, 1, 0.15)

----------------------------------------------------------------
-- TOP BAR
----------------------------------------------------------------

local top = Instance.new("Frame")
top.Name = "top"
top.Size = UDim2.new(1, 0, 0, 54)
top.BackgroundColor3 = C.panel
top.BorderSizePixel = 0
top.ZIndex = 21
top.Parent = main

local title = Instance.new("TextLabel")
title.Size = UDim2.new(1, -180, 1, 0)
title.Position = UDim2.fromOffset(18, 0)
title.BackgroundTransparency = 1
title.Text = "CLIENT EXPOSURE AUDITOR"
title.TextColor3 = C.text
title.Font = Enum.Font.GothamBold
title.TextSize = 18
title.TextXAlignment = Enum.TextXAlignment.Left
title.ZIndex = 22
title.Parent = top

local subtitle = Instance.new("TextLabel")
subtitle.Size = UDim2.new(0, 380, 0, 18)
subtitle.Position = UDim2.new(0, 18, 1, -18)
subtitle.BackgroundTransparency = 1
subtitle.Text = "Passive client audit • server validation cannot be proven from the client"
subtitle.TextColor3 = C.muted
subtitle.Font = Enum.Font.Gotham
subtitle.TextSize = 11
subtitle.TextXAlignment = Enum.TextXAlignment.Left
subtitle.ZIndex = 22
subtitle.Parent = top

local minimize = Instance.new("TextButton")
minimize.Size = UDim2.fromOffset(38, 34)
minimize.Position = UDim2.new(1, -86, 0, 10)
minimize.BackgroundColor3 = C.panel3
minimize.BorderSizePixel = 0
minimize.Text = "—"
minimize.TextColor3 = C.text
minimize.TextSize = 20
minimize.Font = Enum.Font.GothamBold
minimize.ZIndex = 30
minimize.Parent = top
corner(minimize, 8)

local close = Instance.new("TextButton")
close.Name = "close"
close.Size = UDim2.fromOffset(38, 34)
close.Position = UDim2.new(1, -44, 0, 10)
close.BackgroundColor3 = Color3.fromRGB(65, 36, 41)
close.BorderSizePixel = 0
close.Text = "×"
close.TextColor3 = Color3.fromRGB(255, 208, 208)
close.TextSize = 22
close.Font = Enum.Font.GothamBold
close.ZIndex = 30
close.Parent = top
corner(close, 8)

----------------------------------------------------------------
-- LEFT SIDEBAR
----------------------------------------------------------------

local sidebar = Instance.new("Frame")
sidebar.Size = UDim2.new(0, 210, 1, -54)
sidebar.Position = UDim2.fromOffset(0, 54)
sidebar.BackgroundColor3 = C.panel
sidebar.BorderSizePixel = 0
sidebar.ZIndex = 21
sidebar.Parent = main

local sidebarPadding = Instance.new("UIPadding")
sidebarPadding.PaddingTop = UDim.new(0, 12)
sidebarPadding.PaddingLeft = UDim.new(0, 10)
sidebarPadding.PaddingRight = UDim.new(0, 10)
sidebarPadding.Parent = sidebar

local sidebarList = Instance.new("UIListLayout")
sidebarList.Padding = UDim.new(0, 7)
sidebarList.SortOrder = Enum.SortOrder.LayoutOrder
sidebarList.Parent = sidebar

local function makeNav(text, order)
	local b = Instance.new("TextButton")
	b.Size = UDim2.new(1, 0, 0, 38)
	b.BackgroundColor3 = C.panel2
	b.BorderSizePixel = 0
	b.Text = text
	b.TextColor3 = C.text
	b.TextSize = 13
	b.Font = Enum.Font.GothamMedium
	b.TextXAlignment = Enum.TextXAlignment.Left
	b.AutoButtonColor = true
	b.LayoutOrder = order
	b.ZIndex = 22
	b.Parent = sidebar
	corner(b, 8)

	local p = Instance.new("UIPadding")
	p.PaddingLeft = UDim.new(0, 12)
	p.Parent = b

	return b
end

local btnOverview = makeNav("Overview", 1)
local btnRemotes = makeNav("Scan Remotes", 2)
local btnCode = makeNav("Scan Client Code", 3)
local btnState = makeNav("Scan State / Values", 4)
local btnInteractions = makeNav("Scan Interactions", 5)
local btnPhysics = makeNav("Scan Physics", 6)
local btnAssets = makeNav("Scan Assets", 7)
local btnRisks = makeNav("Show Risks", 8)

local separator = Instance.new("Frame")
separator.Size = UDim2.new(1, 0, 0, 1)
separator.BackgroundColor3 = C.border
separator.BorderSizePixel = 0
separator.LayoutOrder = 9
separator.ZIndex = 22
separator.Parent = sidebar

local btnFull = makeNav("FULL SCAN", 10)
btnFull.BackgroundColor3 = Color3.fromRGB(44, 74, 132)

local btnClear = makeNav("Clear Results", 11)

----------------------------------------------------------------
-- CONTENT
----------------------------------------------------------------

local content = Instance.new("Frame")
content.Size = UDim2.new(1, -210, 1, -54)
content.Position = UDim2.fromOffset(210, 54)
content.BackgroundColor3 = C.bg
content.BorderSizePixel = 0
content.ZIndex = 21
content.Parent = main

local statusBar = Instance.new("Frame")
statusBar.Size = UDim2.new(1, -24, 0, 44)
statusBar.Position = UDim2.fromOffset(12, 12)
statusBar.BackgroundColor3 = C.panel
statusBar.BorderSizePixel = 0
statusBar.ZIndex = 22
statusBar.Parent = content
corner(statusBar, 9)

local status = Instance.new("TextLabel")
status.Size = UDim2.new(1, -220, 1, 0)
status.Position = UDim2.fromOffset(14, 0)
status.BackgroundTransparency = 1
status.Text = "READY — choose a scan"
status.TextColor3 = C.ok
status.Font = Enum.Font.GothamBold
status.TextSize = 12
status.TextXAlignment = Enum.TextXAlignment.Left
status.ZIndex = 23
status.Parent = statusBar

local countLabel = Instance.new("TextLabel")
countLabel.Size = UDim2.fromOffset(205, 44)
countLabel.Position = UDim2.new(1, -213, 0, 0)
countLabel.BackgroundTransparency = 1
countLabel.Text = "0 results"
countLabel.TextColor3 = C.muted
countLabel.Font = Enum.Font.Gotham
countLabel.TextSize = 12
countLabel.TextXAlignment = Enum.TextXAlignment.Right
countLabel.ZIndex = 23
countLabel.Parent = statusBar

local sectionTitle = Instance.new("TextLabel")
sectionTitle.Size = UDim2.new(1, -24, 0, 30)
sectionTitle.Position = UDim2.fromOffset(12, 66)
sectionTitle.BackgroundTransparency = 1
sectionTitle.Text = "Overview"
sectionTitle.TextColor3 = C.text
sectionTitle.Font = Enum.Font.GothamBold
sectionTitle.TextSize = 17
sectionTitle.TextXAlignment = Enum.TextXAlignment.Left
sectionTitle.ZIndex = 22
sectionTitle.Parent = content

local resultsFrame = Instance.new("Frame")
resultsFrame.Size = UDim2.new(1, -24, 1, -146)
resultsFrame.Position = UDim2.fromOffset(12, 98)
resultsFrame.BackgroundColor3 = C.panel
resultsFrame.BorderSizePixel = 0
resultsFrame.ClipsDescendants = true
resultsFrame.ZIndex = 22
resultsFrame.Parent = content
corner(resultsFrame, 9)

local scrolling = Instance.new("ScrollingFrame")
scrolling.Size = UDim2.new(1, -10, 1, -50)
scrolling.Position = UDim2.fromOffset(5, 5)
scrolling.BackgroundTransparency = 1
scrolling.BorderSizePixel = 0
scrolling.ScrollBarThickness = 6
scrolling.ScrollBarImageColor3 = C.border
scrolling.CanvasSize = UDim2.fromOffset(0, 0)
scrolling.AutomaticCanvasSize = Enum.AutomaticSize.Y
scrolling.ZIndex = 23
scrolling.Parent = resultsFrame

local resultList = Instance.new("UIListLayout")
resultList.Padding = UDim.new(0, 4)
resultList.SortOrder = Enum.SortOrder.LayoutOrder
resultList.Parent = scrolling

local resultPadding = Instance.new("UIPadding")
resultPadding.PaddingLeft = UDim.new(0, 5)
resultPadding.PaddingRight = UDim.new(0, 5)
resultPadding.PaddingTop = UDim.new(0, 3)
resultPadding.PaddingBottom = UDim.new(0, 3)
resultPadding.Parent = scrolling

local footer = Instance.new("Frame")
footer.Size = UDim2.new(1, 0, 0, 40)
footer.Position = UDim2.new(0, 0, 1, -40)
footer.BackgroundColor3 = C.panel2
footer.BorderSizePixel = 0
footer.ZIndex = 24
footer.Parent = resultsFrame

local prevPage = Instance.new("TextButton")
prevPage.Size = UDim2.fromOffset(72, 28)
prevPage.Position = UDim2.fromOffset(8, 6)
prevPage.BackgroundColor3 = C.panel3
prevPage.BorderSizePixel = 0
prevPage.Text = "< Prev"
prevPage.TextColor3 = C.text
prevPage.TextSize = 12
prevPage.Font = Enum.Font.GothamMedium
prevPage.ZIndex = 25
prevPage.Parent = footer
corner(prevPage, 7)

local nextPage = Instance.new("TextButton")
nextPage.Size = UDim2.fromOffset(72, 28)
nextPage.Position = UDim2.new(1, -80, 0, 6)
nextPage.BackgroundColor3 = C.panel3
nextPage.BorderSizePixel = 0
nextPage.Text = "Next >"
nextPage.TextColor3 = C.text
nextPage.TextSize = 12
nextPage.Font = Enum.Font.GothamMedium
nextPage.ZIndex = 25
nextPage.Parent = footer
corner(nextPage, 7)

local pageLabel = Instance.new("TextLabel")
pageLabel.Size = UDim2.new(1, -180, 1, 0)
pageLabel.Position = UDim2.fromOffset(90, 0)
pageLabel.BackgroundTransparency = 1
pageLabel.Text = "Page 1 / 1"
pageLabel.TextColor3 = C.muted
pageLabel.TextSize = 12
pageLabel.Font = Enum.Font.Gotham
pageLabel.ZIndex = 25
pageLabel.Parent = footer

----------------------------------------------------------------
-- RESULT STATE
----------------------------------------------------------------

local results = {}
local resultKeys = {}
local currentPage = 1
local currentSection = "Overview"
local scanning = false
local omittedResults = 0
local minimumStoredRank = 1

local function setStatus(text, color)
    status.Text = text
    status.TextColor3 = color or C.muted
end

local function yieldProgress(i, total, label)
    if i % SNAPSHOT_YIELD_EVERY == 0 then
        setStatus(string.format("%s — %d / %d", label, i, total), C.warn)
        RunService.Heartbeat:Wait()
    end
end

local function clearRenderedRows()
    for _, child in ipairs(scrolling:GetChildren()) do
        if child:IsA("TextButton") or child:IsA("TextLabel") then
            child:Destroy()
        end
    end
end

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

    local rank = SEVERITY_RANK[severity] or 1
    if rank < minimumStoredRank then
        return
    end

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

    if #results >= MAX_RESULTS_TOTAL then
        omittedResults = omittedResults + 1
        return
    end

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

local function severityColor(level)
    if level == "HIGH" then
        return C.danger
    elseif level == "MEDIUM" then
        return C.warn
    elseif level == "LOW" then
        return C.low
    end
    return C.muted
end

local function sortResults()
    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)
end

local function resultSummary()
    local counts = {HIGH = 0, MEDIUM = 0, LOW = 0, INFO = 0}
    for _, item in ipairs(results) do
        if counts[item.severity] ~= nil then
            counts[item.severity] = counts[item.severity] + 1
        end
    end

    return string.format(
        "%d • H%d M%d L%d I%d",
        #results,
        counts.HIGH,
        counts.MEDIUM,
        counts.LOW,
        counts.INFO
    )
end

local function renderPage()
    clearRenderedRows()

    local total = #results
    local totalPages = math.max(1, math.ceil(total / PAGE_SIZE))

    if currentPage > totalPages then
        currentPage = totalPages
    end

    if currentPage < 1 then
        currentPage = 1
    end

    local first = (currentPage - 1) * PAGE_SIZE + 1
    local last = math.min(total, first + PAGE_SIZE - 1)

    if total == 0 then
        local empty = Instance.new("TextLabel")
        empty.Size = UDim2.new(1, -10, 0, 92)
        empty.BackgroundTransparency = 1
        empty.Text = currentSection == "Overview"
            and "Choose a scan from the left.\nNothing invasive runs automatically."
            or "No results in this section."
        empty.TextColor3 = C.muted
        empty.TextSize = 14
        empty.Font = Enum.Font.Gotham
        empty.TextWrapped = true
        empty.ZIndex = 24
        empty.Parent = scrolling
    else
        for i = first, last do
            local item = results[i]
            local expanded = false

            local row = Instance.new("TextButton")
            row.Size = UDim2.new(1, -4, 0, 50)
            row.BackgroundColor3 = C.panel2
            row.BorderSizePixel = 0
            row.AutoButtonColor = false
            row.Text = ""
            row.LayoutOrder = i
            row.ZIndex = 24
            row.Parent = scrolling
            corner(row, 7)

            local sev = Instance.new("TextLabel")
            sev.Size = UDim2.fromOffset(72, 18)
            sev.Position = UDim2.fromOffset(8, 6)
            sev.BackgroundTransparency = 1
            sev.Text = item.severity
            sev.TextColor3 = severityColor(item.severity)
            sev.TextSize = 10
            sev.Font = Enum.Font.GothamBold
            sev.TextXAlignment = Enum.TextXAlignment.Left
            sev.ZIndex = 25
            sev.Parent = row

            local cat = Instance.new("TextLabel")
            cat.Size = UDim2.fromOffset(150, 18)
            cat.Position = UDim2.fromOffset(78, 6)
            cat.BackgroundTransparency = 1
            cat.Text = item.category
            cat.TextColor3 = C.accent2
            cat.TextSize = 10
            cat.Font = Enum.Font.GothamBold
            cat.TextXAlignment = Enum.TextXAlignment.Left
            cat.ZIndex = 25
            cat.Parent = row

            local pathLabel = Instance.new("TextLabel")
            pathLabel.Size = UDim2.new(1, -16, 0, 18)
            pathLabel.Position = UDim2.fromOffset(8, 25)
            pathLabel.BackgroundTransparency = 1
            pathLabel.Text = item.path
            pathLabel.TextColor3 = C.text
            pathLabel.TextSize = 11
            pathLabel.Font = Enum.Font.Code
            pathLabel.TextXAlignment = Enum.TextXAlignment.Left
            pathLabel.TextYAlignment = Enum.TextYAlignment.Top
            pathLabel.TextTruncate = Enum.TextTruncate.AtEnd
            pathLabel.ZIndex = 25
            pathLabel.Parent = row

            bindButton(row, function()
                expanded = not expanded
                if expanded then
                    pathLabel.Text = item.path .. (item.detail ~= "" and ("\n" .. item.detail) or "")
                    pathLabel.TextWrapped = true
                    pathLabel.TextTruncate = Enum.TextTruncate.None
                    row.Size = UDim2.new(1, -4, 0, 86)
                    pathLabel.Size = UDim2.new(1, -16, 0, 56)
                else
                    pathLabel.Text = item.path
                    pathLabel.TextWrapped = false
                    pathLabel.TextTruncate = Enum.TextTruncate.AtEnd
                    row.Size = UDim2.new(1, -4, 0, 50)
                    pathLabel.Size = UDim2.new(1, -16, 0, 18)
                end
            end)
        end
    end

    countLabel.Text = resultSummary()
    pageLabel.Text = string.format("Page %d / %d", currentPage, totalPages)
end

local function resetResults(sectionName, minimumRank)
    results = {}
    resultKeys = {}
    omittedResults = 0
    currentPage = 1
    currentSection = sectionName
    minimumStoredRank = minimumRank or 1
    sectionTitle.Text = sectionName
    renderPage()
end

local function finishResults(label)
    sortResults()

    local streamingPartial = currentSection ~= "Overview" and safeRead(workspace, "StreamingEnabled") == true
    if omittedResults > 0 then
        setStatus(string.format("%s — %d RESULT(S) OMITTED BY LIMIT", label, omittedResults), C.warn)
    elseif streamingPartial then
        setStatus(label .. " — WORKSPACE PARTIAL (STREAMING)", C.warn)
    else
        setStatus(label, C.ok)
    end
    renderPage()
end

----------------------------------------------------------------
-- SCANNERS
----------------------------------------------------------------

local assetProperties = {
    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 remoteSeverity(path)
    local hasSecret = containsAny(path, SECRET_NAME_WORDS)
    if hasSecret then
        return "MEDIUM"
    end

    local gameplayEffect = containsAny(path, REMOTE_REVIEW_WORDS)
    if gameplayEffect then
        return "MEDIUM"
    end

    return "LOW"
end
local function processRemote(obj)
    if obj:IsA("RemoteEvent") or obj:IsA("RemoteFunction") or obj:IsA("UnreliableRemoteEvent") then
        local path = safeFullName(obj)
        addResult(
            "REMOTE SURFACE",
            remoteSeverity(path),
            path,
            obj.ClassName .. " | Client can address this remote; security depends on server validation/rate limits."
        )
    elseif obj:IsA("BindableEvent") or obj:IsA("BindableFunction") then
        addResult(
            "LOCAL BUS",
            "INFO",
            safeFullName(obj),
            obj.ClassName .. " | Same-side communication only; not a client-server remote by itself."
        )
    end
end

local function processCode(obj)
    local path = safeFullName(obj)

    if obj:IsA("LocalScript") then
        local sev = classifyName(path)
        if SEVERITY_RANK[sev] < SEVERITY_RANK.LOW then
            sev = "LOW"
        end
        addResult(
            "CLIENT CODE",
            sev,
            path,
            "LocalScript is replicated client-side. Assume its logic/constants can be inspected or altered by an exploiter."
        )
    elseif obj:IsA("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 even if the client never requires it."
        )
    elseif obj:IsA("Script") then
        local rc = safeRead(obj, "RunContext")
        if rc and tostring(rc):find("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 is client-side: " .. tostring(rc)
            )
        end
    end
end

local function processState(obj)
    local path = safeFullName(obj)

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

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

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

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

local function processInteraction(obj)
    local path = safeFullName(obj)
    local severity = classifyName(path)
    if SEVERITY_RANK[severity] < SEVERITY_RANK.LOW then
        severity = "LOW"
    end

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

    if obj:IsA("ProximityPrompt") then
        local detail = string.format(
            "Action=%s | Distance=%s | Hold=%s | Enabled=%s | Server must validate authorization/state/rate; client distance/hold is not a security boundary%s",
            valueToString(safeRead(obj, "ActionText")),
            valueToString(safeRead(obj, "MaxActivationDistance")),
            valueToString(safeRead(obj, "HoldDuration")),
            valueToString(safeRead(obj, "Enabled")),
            unanchoredContext and " | Parent assembly is unanchored" or ""
        )
        addResult("PROMPT", severity, path, detail)
    elseif obj:IsA("ClickDetector") then
        local detail = "Distance=" .. valueToString(safeRead(obj, "MaxActivationDistance"))
            .. " | Treat as client-triggerable interaction; validate server effects"
            .. (unanchoredContext and " | Parent assembly is unanchored" or "")
        addResult("CLICK", severity, path, detail)
    elseif obj:IsA("DragDetector") then
        addResult(
            "DRAG",
            severity,
            path,
            "Client interaction / manipulation surface"
                .. (unanchoredContext and " | Parent assembly is unanchored" or "")
        )
    end
end

local function processPhysics(obj, seenRoots)
    if obj:IsA("BasePart") then
        local root = assemblyRoot(obj)
        if not root or seenRoots[root] then
            return
        end
        seenRoots[root] = true

        if isCharacterObject(root) then
            return
        end

        local anchored = safeRead(root, "Anchored")
        if anchored ~= false then
            return
        end

        local path = safeFullName(root)
        local ownerText, autoText, localOwned = networkInfo(root)
        local riskyName = containsAny(path, PHYSICS_REVIEW_WORDS)
        local canTouch = safeRead(root, "CanTouch")

        local severity = "LOW"
        if riskyName and (localOwned or canTouch == true) then
            severity = "MEDIUM"
        end

        local detail = string.format(
            "AssemblyRoot | Owner=%s | AutoOwnership=%s | CanTouch=%s | CanCollide=%s | Massless=%s",
            ownerText,
            autoText,
            valueToString(canTouch),
            valueToString(safeRead(root, "CanCollide")),
            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, path, detail)
        return
    end

    if obj:IsA("Constraint")
        or obj:IsA("BodyMover")
        or obj:IsA("LinearVelocity")
        or obj:IsA("AngularVelocity")
        or obj:IsA("VectorForce")
        or obj:IsA("AlignPosition")
        or obj:IsA("AlignOrientation") then
        if not isCharacterObject(obj) then
            local path = safeFullName(obj)
            local severity = classifyName(path)
            if SEVERITY_RANK[severity] < SEVERITY_RANK.LOW then
                severity = "LOW"
            end
            addResult("PHYSICS CTRL", severity, path, obj.ClassName)
        end
    end
end

local function processAssets(obj)
    for className, props in pairs(assetProperties) do
        if obj:IsA(className) then
            for _, prop in ipairs(props) do
                local value = safeRead(obj, prop)
                if value ~= nil and tostring(value) ~= "" then
                    addResult(
                        "ASSET",
                        "INFO",
                        safeFullName(obj) .. "." .. prop,
                        valueToString(value)
                    )
                end
            end
        end
    end
end

local function addStreamingNotice()
    local streaming = safeRead(workspace, "StreamingEnabled")
    if streaming == true then
        addResult(
            "LIMITATION",
            "LOW",
            "Workspace.StreamingEnabled",
            "Workspace results are only the subset currently streamed to this client. A complete world inventory requires a server-side audit."
        )
    end
end

local function runScanner(sectionName, statusName, processor)
    if scanning then
        setStatus("SCAN ALREADY RUNNING", C.danger)
        return
    end

    scanning = true
    resetResults(sectionName, 1)
    addStreamingNotice()

    local ok, err = pcall(function()
        setStatus("CAPTURING CLIENT SNAPSHOT...", C.warn)
        local list = captureSnapshot()
        setStatus(statusName .. "...", C.warn)

        local context = {}
        for i, obj in ipairs(list) do
            processor(obj, context)
            yieldProgress(i, #list, statusName)
        end
    end)

    scanning = false
    if ok then
        finishResults(statusName .. " COMPLETE")
    else
        setStatus("ERROR: " .. tostring(err), C.danger)
        warn("[AUDIT] " .. tostring(err))
        renderPage()
    end
end

local function scanRemotes()
    runScanner("Remotes", "REMOTE SCAN", function(obj)
        processRemote(obj)
    end)
end

local function scanCode()
    runScanner("Client Code", "CLIENT CODE SCAN", function(obj)
        processCode(obj)
    end)
end

local function scanState()
    runScanner("State / Values", "STATE SCAN", function(obj)
        processState(obj)
    end)
end

local function scanInteractions()
    runScanner("Interactions", "INTERACTION SCAN", function(obj)
        processInteraction(obj)
    end)
end

local function scanPhysics()
    local seenRoots = {}
    runScanner("Physics", "PHYSICS SCAN", function(obj)
        processPhysics(obj, seenRoots)
    end)
end

local function scanAssets()
    runScanner("Assets", "ASSET SCAN", function(obj)
        processAssets(obj)
    end)
end

local function runCombined(sectionName, statusName, minimumRank)
    if scanning then
        setStatus("SCAN ALREADY RUNNING", C.danger)
        return
    end

    scanning = true
    resetResults(sectionName, minimumRank or 1)
    addStreamingNotice()

    local ok, err = pcall(function()
        setStatus("CAPTURING CLIENT SNAPSHOT...", C.warn)
        local list = captureSnapshot()
        local physicsSeen = {}

        setStatus(statusName .. "...", C.warn)
        for i, obj in ipairs(list) do
            processRemote(obj)
            processCode(obj)
            processState(obj)
            processInteraction(obj)
            processPhysics(obj, physicsSeen)
            processAssets(obj)
            yieldProgress(i, #list, statusName)
        end
    end)

    scanning = false
    if ok then
        finishResults(statusName .. " COMPLETE")
    else
        setStatus("ERROR: " .. tostring(err), C.danger)
        warn("[AUDIT] " .. tostring(err))
        renderPage()
    end
end

local function showRisks()
    runCombined("Risk Review", "RISK REVIEW", SEVERITY_RANK.MEDIUM)
end

local function runFullScan()
    runCombined("Full Scan", "FULL SCAN", SEVERITY_RANK.INFO)
end

----------------------------------------------------------------
-- OVERVIEW
----------------------------------------------------------------

local function showOverview()
    resetResults("Overview", 1)

    local streaming = safeRead(workspace, "StreamingEnabled")

    addResult("SYSTEM", "INFO", "PlaceId", tostring(game.PlaceId))
    addResult("SYSTEM", "INFO", "GameId", tostring(game.GameId))
    addResult("SYSTEM", "INFO", "StreamingEnabled", tostring(streaming))
    addResult(
        "MODEL",
        "INFO",
        "Client visibility != vulnerability",
        "RemoteEvents, replicated values and client code are expected surfaces. Security depends on server-side validation and authority."
    )
    addResult(
        "LIMITATION",
        "LOW",
        "Server validation is not visible here",
        "This passive client script cannot prove whether OnServerEvent/OnServerInvoke handlers validate types, permissions, distance, state or rate limits."
    )
    addResult(
        "LIMITATION",
        "LOW",
        "Replicated code",
        "Treat every LocalScript and replicated ModuleScript as inspectable/modifiable by an exploiter."
    )

    if streaming == true then
        addResult(
            "LIMITATION",
            "LOW",
            "Workspace streaming",
            "Workspace scans can miss objects that are not currently streamed to this client."
        )
    end

    finishResults("READY")
end

----------------------------------------------------------------
-- BUTTON BINDINGS
----------------------------------------------------------------

-- X and minimize both intentionally hide, never destroy.
bindButton(close, hideMain)
bindButton(minimize, hideMain)

bindButton(btnOverview, showOverview)
bindButton(btnRemotes, scanRemotes)
bindButton(btnCode, scanCode)
bindButton(btnState, scanState)
bindButton(btnInteractions, scanInteractions)
bindButton(btnPhysics, scanPhysics)
bindButton(btnAssets, scanAssets)
bindButton(btnRisks, showRisks)
bindButton(btnFull, runFullScan)

bindButton(btnClear, function()
	resetResults("Overview")
	setStatus("RESULTS CLEARED", C.ok)
end)

bindButton(prevPage, function()
	currentPage = currentPage - 1
	renderPage()
end)

bindButton(nextPage, function()
	currentPage = currentPage + 1
	renderPage()
end)

----------------------------------------------------------------
-- HOTKEY
----------------------------------------------------------------

UserInputService.InputBegan:Connect(function(input, processed)
	if processed then
		return
	end

	if input.KeyCode == Enum.KeyCode.RightShift then
		if main.Visible then
			hideMain()
		else
			openMain()
		end
	end
end)

----------------------------------------------------------------
-- LAUNCHER NOTE
----------------------------------------------------------------
-- Dragging is intentionally disabled in v6.1. Some injected input layers
-- report tiny mouse movement during a click, which could suppress opening.

----------------------------------------------------------------
-- START STATE
----------------------------------------------------------------

local overviewOk, overviewErr = pcall(showOverview)
if overviewOk then
    bootNotice.Visible = false
else
    bootNotice.Text = "AUDITOR SETUP ERROR\n\n" .. tostring(overviewErr) .. "\n\nThe launcher still works so this error remains visible."
    bootNotice.TextColor3 = C.danger
    warn("[AUDIT] setup error: " .. tostring(overviewErr))
end

main.Visible = false
launcher.Visible = true

print("[AUDIT] v6.1 loaded. Studio + injector compatible launcher ready.")