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


--[[
    CLIENT EXPOSURE AUDITOR v5
    Menu-first build.

    Behavior:
      1) On execution, ONLY a small "AUDIT" launcher appears.
      2) Clicking it opens the debugger menu.
      3) No scan runs until you press a scan button.
      4) Results are grouped by scan type and paginated.
      5) Close (X) hides the window; launcher remains available.

    Scope:
      - Uses normal Roblox client APIs only.
      - Does NOT use executor-specific introspection such as getgc,
        getsenv, debug registry, hooks, decompilers, or remote firing.
      - Read-only audit: it does not invoke remotes/prompts or mutate game state.
]]

local Players = game:GetService("Players")
local CollectionService = game:GetService("CollectionService")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")

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

local playerGui = player:WaitForChild("PlayerGui")

----------------------------------------------------------------
-- CLEAN PREVIOUS INSTANCE
----------------------------------------------------------------

local GUI_NAME = "client_exposure_auditor_v5"

local old = playerGui:FindFirstChild(GUI_NAME)
if old then
	old:Destroy()
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),
	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_PER_SCAN = 20000

----------------------------------------------------------------
-- 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)
	return ok and value or 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 sensitiveWords = {
	"secret", "token", "password", "apikey", "api_key", "private",
	"admin", "owner", "moderator", "money", "cash", "coin", "currency",
	"reward", "price", "damage", "cooldown", "inventory", "item",
	"weapon", "level", "xp", "experience", "config", "settings",
	"ban", "anticheat", "anti_cheat", "remote", "purchase", "give", "grant",
}

local function riskLevel(text)
	local s = string.lower(text)

	for _, word in ipairs({"password", "secret", "token", "apikey", "api_key"}) do
		if string.find(s, word, 1, true) then
			return "HIGH"
		end
	end

	for _, word in ipairs(sensitiveWords) do
		if string.find(s, word, 1, true) then
			return "MEDIUM"
		end
	end

	return "INFO"
end

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

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

----------------------------------------------------------------
-- 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.MinSize = Vector2.new(760, 480)
sizeConstraint.MaxSize = Vector2.new(1450, 900)
sizeConstraint.Parent = main

----------------------------------------------------------------
-- 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 = "No automatic scan • choose a module on the left"
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, -170, 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(150, 44)
countLabel.Position = UDim2.new(1, -158, 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 currentPage = 1
local currentSection = "Overview"
local scanning = false

local function setStatus(text, color)
	status.Text = text
	status.TextColor3 = color or C.muted
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)
	if #results >= MAX_RESULTS_PER_SCAN then
		return
	end

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

local function severityColor(level)
	if level == "HIGH" then
		return C.danger
	elseif level == "MEDIUM" then
		return C.warn
	else
		return C.muted
	end
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, 72)
		empty.BackgroundTransparency = 1
		empty.Text = currentSection == "Overview"
			and "Choose a scan from the left.\nNothing 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 row = Instance.new("TextButton")
			row.Size = UDim2.new(1, -4, 0, 48)
			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(120, 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, 24)
			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.TextTruncate = Enum.TextTruncate.AtEnd
			pathLabel.ZIndex = 25
			pathLabel.Parent = row

			row.Activated:Connect(function()
				pathLabel.Text = item.path .. (item.detail ~= "" and ("  |  " .. item.detail) or "")
				pathLabel.TextWrapped = true
				row.Size = UDim2.new(1, -4, 0, 72)
				pathLabel.Size = UDim2.new(1, -16, 0, 42)
			end)
		end
	end

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

local function resetResults(sectionName)
	results = {}
	currentPage = 1
	currentSection = sectionName
	sectionTitle.Text = sectionName
	renderPage()
end

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

local function allDescendants()
	local list = game:GetDescendants()
	return list
end

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

local function scanRemotes(append)
	if not append then
		resetResults("Remotes")
	end

	local list = allDescendants()
	setStatus("SCANNING REMOTES...", C.warn)

	for i, obj in ipairs(list) do
		if obj:IsA("RemoteEvent")
			or obj:IsA("RemoteFunction")
			or obj:IsA("UnreliableRemoteEvent")
			or obj:IsA("BindableEvent")
			or obj:IsA("BindableFunction")
		then
			local path = safeFullName(obj)
			local sev = riskLevel(path)

			if obj:IsA("RemoteEvent") or obj:IsA("RemoteFunction") or obj:IsA("UnreliableRemoteEvent") then
				if sev == "INFO" then sev = "MEDIUM" end
			end

			addResult("REMOTE", sev, path, obj.ClassName)
		end

		progress(i, #list, "REMOTES")
	end
end

local function scanCode(append)
	if not append then
		resetResults("Client Code")
	end

	local list = allDescendants()
	setStatus("SCANNING CLIENT CODE...", C.warn)

	for i, obj in ipairs(list) do
		if obj:IsA("LocalScript") then
			addResult("LOCAL", "MEDIUM", safeFullName(obj), "LocalScript is replicated to client")
		elseif obj:IsA("ModuleScript") then
			addResult("MODULE", riskLevel(safeFullName(obj)), safeFullName(obj), "ModuleScript is visible to client")
		elseif obj:IsA("Script") then
			local rc = safeRead(obj, "RunContext")
			if rc and tostring(rc):find("Client") then
				addResult("CLIENT SCRIPT", "MEDIUM", safeFullName(obj), tostring(rc))
			end
		end

		progress(i, #list, "CODE")
	end
end

local function scanState(append)
	if not append then
		resetResults("State / Values")
	end

	local list = allDescendants()
	setStatus("SCANNING STATE...", C.warn)

	for i, obj in ipairs(list) do
		local path = safeFullName(obj)

		local ok, attrs = pcall(function()
			return obj:GetAttributes()
		end)

		if ok then
			for name, value in pairs(attrs) do
				local text = path .. "." .. tostring(name)
				addResult("ATTRIBUTE", riskLevel(text), text, valueToString(value))
			end
		end

		local okTags, tags = pcall(function()
			return CollectionService:GetTags(obj)
		end)

		if okTags then
			for _, tag in ipairs(tags) do
				addResult("TAG", riskLevel(path .. "." .. tag), path, tag)
			end
		end

		if obj:IsA("ValueBase") then
			local value = safeRead(obj, "Value")
			addResult("VALUE", riskLevel(path), path, valueToString(value))
		end

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

		progress(i, #list, "STATE")
	end
end

local function scanInteractions(append)
	if not append then
		resetResults("Interactions")
	end

	local list = allDescendants()
	setStatus("SCANNING INTERACTIONS...", C.warn)

	for i, obj in ipairs(list) do
		local path = safeFullName(obj)

		if obj:IsA("ProximityPrompt") then
			local detail = string.format(
				"Action=%s | Distance=%s | Hold=%s",
				valueToString(safeRead(obj, "ActionText")),
				valueToString(safeRead(obj, "MaxActivationDistance")),
				valueToString(safeRead(obj, "HoldDuration"))
			)
			addResult("PROMPT", "MEDIUM", path, detail)

		elseif obj:IsA("ClickDetector") then
			addResult(
				"CLICK",
				"MEDIUM",
				path,
				"Distance=" .. valueToString(safeRead(obj, "MaxActivationDistance"))
			)

		elseif obj:IsA("DragDetector") then
			addResult("DRAG", "MEDIUM", path, "Client interaction surface")
		end

		progress(i, #list, "INTERACTIONS")
	end
end

local function scanPhysics(append)
	if not append then
		resetResults("Physics")
	end

	local list = allDescendants()
	setStatus("SCANNING PHYSICS...", C.warn)

	for i, obj in ipairs(list) do
		if obj:IsA("BasePart") then
			local anchored = safeRead(obj, "Anchored")
			if anchored == false then
				local detail = string.format(
					"Massless=%s | CanCollide=%s | CanTouch=%s | CanQuery=%s",
					valueToString(safeRead(obj, "Massless")),
					valueToString(safeRead(obj, "CanCollide")),
					valueToString(safeRead(obj, "CanTouch")),
					valueToString(safeRead(obj, "CanQuery"))
				)
				addResult("UNANCHORED", "MEDIUM", safeFullName(obj), detail)
			end

		elseif 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
			addResult("PHYSICS CTRL", "MEDIUM", safeFullName(obj), obj.ClassName)
		end

		progress(i, #list, "PHYSICS")
	end
end

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 scanAssets(append)
	if not append then
		resetResults("Assets")
	end

	local list = allDescendants()
	setStatus("SCANNING ASSETS...", C.warn)

	for i, obj in ipairs(list) do
		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",
							riskLevel(safeFullName(obj)),
							safeFullName(obj) .. "." .. prop,
							valueToString(value)
						)
					end
				end
			end
		end

		progress(i, #list, "ASSETS")
	end
end

local function showRisks()
	resetResults("Risk Review")
	setStatus("BUILDING RISK VIEW...", C.warn)

	local list = allDescendants()

	for i, obj in ipairs(list) do
		local path = safeFullName(obj)
		local sev = riskLevel(path)

		if sev ~= "INFO" then
			addResult("NAME RISK", sev, path, obj.ClassName)
		end

		if obj:IsA("RemoteEvent") or obj:IsA("RemoteFunction") or obj:IsA("UnreliableRemoteEvent") then
			addResult("REMOTE", "MEDIUM", path, obj.ClassName)
		end

		if obj:IsA("ModuleScript") or obj:IsA("LocalScript") then
			local codeSev = riskLevel(path)
			if codeSev == "INFO" then codeSev = "MEDIUM" end
			addResult("CLIENT CODE", codeSev, path, obj.ClassName)
		end

		if obj:IsA("ValueBase") and sev ~= "INFO" then
			addResult("STATE", sev, path, valueToString(safeRead(obj, "Value")))
		end

		progress(i, #list, "RISKS")
	end

	setStatus("RISK REVIEW COMPLETE", C.ok)
	renderPage()
end

local function runSingle(name, fn)
	if scanning then
		setStatus("SCAN ALREADY RUNNING", C.danger)
		return
	end

	scanning = true

	local ok, err = pcall(function()
		fn(false)
	end)

	if ok then
		setStatus(name .. " COMPLETE", C.ok)
	else
		setStatus("ERROR: " .. tostring(err), C.danger)
		warn("[AUDIT] " .. tostring(err))
	end

	scanning = false
	renderPage()
end

local function runFullScan()
	if scanning then
		setStatus("SCAN ALREADY RUNNING", C.danger)
		return
	end

	scanning = true
	resetResults("Full Scan")

	local ok, err = pcall(function()
		scanRemotes(true)
		scanCode(true)
		scanState(true)
		scanInteractions(true)
		scanPhysics(true)
		scanAssets(true)
	end)

	if ok then
		setStatus("FULL SCAN COMPLETE", C.ok)
	else
		setStatus("ERROR: " .. tostring(err), C.danger)
		warn("[AUDIT] " .. tostring(err))
	end

	scanning = false
	renderPage()
end

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

local function showOverview()
	resetResults("Overview")

	local streaming = safeRead(workspace, "StreamingEnabled")

	addResult("SYSTEM", "INFO", "PlaceId", tostring(game.PlaceId))
	addResult("SYSTEM", "INFO", "GameId", tostring(game.GameId))
	addResult("SYSTEM", "INFO", "JobId", tostring(game.JobId))
	addResult("SYSTEM", "INFO", "StreamingEnabled", tostring(streaming))
	addResult("SYSTEM", "INFO", "Loaded descendants", tostring(#game:GetDescendants()))
	addResult("INFO", "INFO", "Usage", "Select a scan from the left. Nothing runs automatically.")

	setStatus("READY", C.ok)
	renderPage()
end

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

launcher.Activated:Connect(function()
	main.Visible = true
	launcher.Visible = false
end)

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

-- X and minimize both intentionally hide, never destroy.
close.Activated:Connect(hideMain)
minimize.Activated:Connect(hideMain)

btnOverview.Activated:Connect(showOverview)
btnRemotes.Activated:Connect(function()
	runSingle("REMOTE SCAN", scanRemotes)
end)
btnCode.Activated:Connect(function()
	runSingle("CLIENT CODE SCAN", scanCode)
end)
btnState.Activated:Connect(function()
	runSingle("STATE SCAN", scanState)
end)
btnInteractions.Activated:Connect(function()
	runSingle("INTERACTION SCAN", scanInteractions)
end)
btnPhysics.Activated:Connect(function()
	runSingle("PHYSICS SCAN", scanPhysics)
end)
btnAssets.Activated:Connect(function()
	runSingle("ASSET SCAN", scanAssets)
end)
btnRisks.Activated:Connect(showRisks)
btnFull.Activated:Connect(runFullScan)

btnClear.Activated:Connect(function()
	resetResults("Overview")
	setStatus("RESULTS CLEARED", C.ok)
end)

prevPage.Activated:Connect(function()
	currentPage -= 1
	renderPage()
end)

nextPage.Activated:Connect(function()
	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
			main.Visible = true
			launcher.Visible = false
		end
	end
end)

----------------------------------------------------------------
-- SIMPLE DRAG FOR LAUNCHER
----------------------------------------------------------------

do
	local dragging = false
	local dragStart
	local startPos

	launcher.InputBegan:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1
			or input.UserInputType == Enum.UserInputType.Touch
		then
			dragging = true
			dragStart = input.Position
			startPos = launcher.Position
		end
	end)

	UserInputService.InputChanged:Connect(function(input)
		if dragging and (
			input.UserInputType == Enum.UserInputType.MouseMovement
			or input.UserInputType == Enum.UserInputType.Touch
		) then
			local delta = input.Position - dragStart

			launcher.Position = UDim2.new(
				startPos.X.Scale,
				startPos.X.Offset + delta.X,
				startPos.Y.Scale,
				startPos.Y.Offset + delta.Y
			)
		end
	end)

	UserInputService.InputEnded:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1
			or input.UserInputType == Enum.UserInputType.Touch
		then
			dragging = false
		end
	end)
end

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

showOverview()
main.Visible = false
launcher.Visible = true

print("[AUDIT] v5 loaded. Click the AUDIT button to open the menu.")