@adiholick/nvstudio-mcp 2.2.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -47
- package/agent-plugin/mcp_config.json +8 -0
- package/agent-plugin/plugin.json +6 -0
- package/agent-plugin/rules/AGENTS.md +87 -0
- package/agent-plugin/skills/nvstudio-assets-terrain-guide/SKILL.md +27 -0
- package/agent-plugin/skills/nvstudio-gui-standards/SKILL.md +419 -0
- package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +69 -0
- package/agent-plugin/skills/nvstudio-roblox-sync-guide/SKILL.md +16 -0
- package/dist/build-plugin.js +55 -0
- package/dist/index.js +988 -146
- package/dist/luau-validator.js +99 -34
- package/dist/roblox-bridge.js +390 -72
- package/dist/roblox-docs.js +264 -0
- package/dist/task-queue.js +57 -6
- package/dist/ws-gateway.js +579 -0
- package/package.json +7 -3
- package/public/index.html +634 -109
- package/studio-plugin/Main.server.lua +484 -53
- package/studio-plugin/batch_create_instances.lua +128 -0
- package/studio-plugin/batch_set_properties.lua +69 -0
- package/studio-plugin/get_properties.lua +25 -2
- package/studio-plugin/insert_asset.lua +85 -12
- package/studio-plugin/nvstudio_mcp.rbxmx +1393 -68
- package/studio-plugin/preview_asset.lua +99 -0
- package/studio-plugin/scan_security.lua +81 -0
- package/studio-plugin/security_scanner.lua +346 -0
- package/studio-plugin/set_properties.lua +10 -0
- package/studio-plugin/ui_manager.lua +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
-- ============================================================================
|
|
3
|
+
-- Skill Module: preview_asset
|
|
4
|
+
-- Deskripsi: Pratinjau & Pemindaian Keamanan Aset Creator Store Secara Terisolasi
|
|
5
|
+
-- ============================================================================
|
|
6
|
+
|
|
7
|
+
local HttpService = game:GetService("HttpService")
|
|
8
|
+
local InsertService = game:GetService("InsertService")
|
|
9
|
+
|
|
10
|
+
local SecurityScanner = require(script.Parent:WaitForChild("security_scanner"))
|
|
11
|
+
|
|
12
|
+
return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
|
|
13
|
+
local assetIdStr = ""
|
|
14
|
+
local deepScan = true
|
|
15
|
+
|
|
16
|
+
if typeof(data) == "string" then
|
|
17
|
+
local success, parsed = pcall(function()
|
|
18
|
+
return HttpService:JSONDecode(data)
|
|
19
|
+
end)
|
|
20
|
+
if success and typeof(parsed) == "table" then
|
|
21
|
+
assetIdStr = tostring(parsed.asset_id or parsed.assetId or "")
|
|
22
|
+
if parsed.deep_scan ~= nil then deepScan = parsed.deep_scan end
|
|
23
|
+
else
|
|
24
|
+
assetIdStr = data
|
|
25
|
+
end
|
|
26
|
+
elseif typeof(data) == "table" then
|
|
27
|
+
assetIdStr = tostring(data.asset_id or data.assetId or "")
|
|
28
|
+
if data.deep_scan ~= nil then deepScan = data.deep_scan end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
local cleanIdStr = string.gsub(assetIdStr, "rbxassetid://", "")
|
|
32
|
+
cleanIdStr = string.gsub(cleanIdStr, "%s+", "")
|
|
33
|
+
local assetId = tonumber(cleanIdStr)
|
|
34
|
+
|
|
35
|
+
if not assetId or assetId <= 0 then
|
|
36
|
+
return { status = "error", error = "Asset ID tidak valid. Berikan Asset ID angka (contoh: 123456789)." }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
-- Muat Aset secara terisolasi (unparented / memory only)
|
|
40
|
+
local ok, model = pcall(function()
|
|
41
|
+
return InsertService:LoadAsset(assetId)
|
|
42
|
+
end)
|
|
43
|
+
|
|
44
|
+
if not ok or not model then
|
|
45
|
+
return {
|
|
46
|
+
status = "error",
|
|
47
|
+
error = string.format("Gagal memuat aset ID %d. Pastikan Asset ID publik dan terdaftar di Creator Store.", assetId)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
local scanResult
|
|
52
|
+
local scanOk, err = pcall(function()
|
|
53
|
+
scanResult = SecurityScanner.scanInstance(model, deepScan)
|
|
54
|
+
end)
|
|
55
|
+
|
|
56
|
+
local rootName = model.Name
|
|
57
|
+
local childrenOverview = {}
|
|
58
|
+
for _, child in ipairs(model:GetChildren()) do
|
|
59
|
+
table.insert(childrenOverview, {
|
|
60
|
+
name = child.Name,
|
|
61
|
+
className = child.ClassName,
|
|
62
|
+
})
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
-- Hancurkan model sementara dari memori (Cleanup Mutlak)
|
|
66
|
+
pcall(function()
|
|
67
|
+
model:Destroy()
|
|
68
|
+
end)
|
|
69
|
+
|
|
70
|
+
if not scanOk or not scanResult then
|
|
71
|
+
return {
|
|
72
|
+
status = "error",
|
|
73
|
+
error = "Gagal memindai aset: " .. tostring(err)
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
local recommendation = "Aman untuk dimasukkan langsung menggunakan insert_asset."
|
|
78
|
+
if scanResult.riskLevel == "DANGEROUS" then
|
|
79
|
+
recommendation = "PERINGATAN: Aset ini memiliki ancaman BERBAHAYA! Wajib gunakan 'insert_asset' dengan strip_scripts = true, atau hindari aset ini."
|
|
80
|
+
elseif scanResult.riskLevel == "WARNING" then
|
|
81
|
+
recommendation = "Perhatian: Aset mengandung skrip atau peringatan moderat. Periksa kembali atau gunakan strip_scripts = true jika skrip tidak diperlukan."
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
status = "success",
|
|
86
|
+
result = {
|
|
87
|
+
assetId = assetId,
|
|
88
|
+
assetName = rootName,
|
|
89
|
+
riskLevel = scanResult.riskLevel,
|
|
90
|
+
recommendation = recommendation,
|
|
91
|
+
stats = scanResult.stats,
|
|
92
|
+
highThreatsCount = scanResult.highThreats,
|
|
93
|
+
mediumThreatsCount = scanResult.mediumThreats,
|
|
94
|
+
threats = scanResult.threats,
|
|
95
|
+
topLevelChildren = childrenOverview,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
-- ============================================================================
|
|
3
|
+
-- Skill Module: scan_security
|
|
4
|
+
-- Deskripsi: Audit Keamanan Instance/Model yang Sudah Ada di Tempat Kerja Studio
|
|
5
|
+
-- ============================================================================
|
|
6
|
+
|
|
7
|
+
local HttpService = game:GetService("HttpService")
|
|
8
|
+
local SecurityScanner = require(script.Parent:WaitForChild("security_scanner"))
|
|
9
|
+
|
|
10
|
+
return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
|
|
11
|
+
local target = targetInstance or game.Workspace
|
|
12
|
+
local deepScan = true
|
|
13
|
+
local cleanThreats = false
|
|
14
|
+
|
|
15
|
+
if typeof(data) == "string" and #data > 0 then
|
|
16
|
+
local success, parsed = pcall(function()
|
|
17
|
+
return HttpService:JSONDecode(data)
|
|
18
|
+
end)
|
|
19
|
+
if success and typeof(parsed) == "table" then
|
|
20
|
+
if parsed.deep_scan ~= nil then deepScan = parsed.deep_scan end
|
|
21
|
+
if parsed.clean_threats ~= nil then cleanThreats = parsed.clean_threats end
|
|
22
|
+
end
|
|
23
|
+
elseif typeof(data) == "table" then
|
|
24
|
+
if data.deep_scan ~= nil then deepScan = data.deep_scan end
|
|
25
|
+
if data.clean_threats ~= nil then cleanThreats = data.clean_threats end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
local scanResult
|
|
29
|
+
local ok, err = pcall(function()
|
|
30
|
+
scanResult = SecurityScanner.scanInstance(target, deepScan)
|
|
31
|
+
end)
|
|
32
|
+
|
|
33
|
+
if not ok or not scanResult then
|
|
34
|
+
return {
|
|
35
|
+
status = "error",
|
|
36
|
+
error = "Gagal memindai target: " .. tostring(err)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
local cleanedScripts = {}
|
|
41
|
+
if cleanThreats and #scanResult.threats > 0 then
|
|
42
|
+
-- Hapus skrip yang terdeteksi bahaya (HIGH severity)
|
|
43
|
+
local scriptsToDestroy = {}
|
|
44
|
+
for _, threat in ipairs(scanResult.threats) do
|
|
45
|
+
if threat.severity == "HIGH" and threat.scriptPath then
|
|
46
|
+
scriptsToDestroy[threat.scriptPath] = true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
local function purge(instance: Instance)
|
|
51
|
+
for _, desc in ipairs(instance:GetDescendants()) do
|
|
52
|
+
if desc:IsA("LuaSourceContainer") and scriptsToDestroy[desc:GetFullName()] then
|
|
53
|
+
table.insert(cleanedScripts, {
|
|
54
|
+
name = desc.Name,
|
|
55
|
+
className = desc.ClassName,
|
|
56
|
+
path = desc:GetFullName(),
|
|
57
|
+
})
|
|
58
|
+
pcall(function() desc:Destroy() end)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
purge(target)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
status = "success",
|
|
68
|
+
result = {
|
|
69
|
+
target = target:GetFullName(),
|
|
70
|
+
riskLevel = scanResult.riskLevel,
|
|
71
|
+
stats = scanResult.stats,
|
|
72
|
+
highThreatsCount = scanResult.highThreats,
|
|
73
|
+
mediumThreatsCount = scanResult.mediumThreats,
|
|
74
|
+
threats = scanResult.threats,
|
|
75
|
+
cleanedThreatsCount = #cleanedScripts,
|
|
76
|
+
cleanedScripts = cleanedScripts,
|
|
77
|
+
isCleaned = cleanThreats,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
-- ============================================================================
|
|
3
|
+
-- Module: security_scanner
|
|
4
|
+
-- Deskripsi: Engine Heuristik Pemindaian Keamanan & Backdoor Detection
|
|
5
|
+
-- Standar: Business-Grade, Zero-Execution Isolation, Comprehensive Threat Rules
|
|
6
|
+
-- ============================================================================
|
|
7
|
+
|
|
8
|
+
local SecurityScanner = {}
|
|
9
|
+
|
|
10
|
+
-- Daftar nama skrip yang sering digunakan oleh virus/backdoor klasik Roblox
|
|
11
|
+
local SUSPICIOUS_SCRIPT_NAMES = {
|
|
12
|
+
"[Vv]accine",
|
|
13
|
+
"[Aa]nti[Ll]ag",
|
|
14
|
+
"[Ff]ix[Ll]ag",
|
|
15
|
+
"[Ii]nfection",
|
|
16
|
+
"[Ff]ire",
|
|
17
|
+
"[Cc]lean",
|
|
18
|
+
"[Vv]irus",
|
|
19
|
+
"4[Dd]",
|
|
20
|
+
"4[Dd]_[Pp]lugin",
|
|
21
|
+
"[Ss]pread",
|
|
22
|
+
"[Ll]ag[Ff]ixer",
|
|
23
|
+
"[Dd]up",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
-- Aturan Pola Heuristik Kode Berbahaya
|
|
27
|
+
local THREAT_PATTERNS = {
|
|
28
|
+
{
|
|
29
|
+
id = "BACKDOOR_REQUIRE",
|
|
30
|
+
pattern = "require%s*%(%s*%d+%s*%)",
|
|
31
|
+
severity = "HIGH",
|
|
32
|
+
name = "Backdoor Numeric Require",
|
|
33
|
+
description = "Memanggil modul eksternal pihak ketiga via Asset ID numerik (indikasi backdoor/remote loader).",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id = "LOADSTRING_EXEC",
|
|
37
|
+
pattern = "loadstring%s*%(",
|
|
38
|
+
severity = "HIGH",
|
|
39
|
+
name = "Dynamic Code Execution (loadstring)",
|
|
40
|
+
description = "Mengeksekusi string kode dinamis yang tidak dapat diaudit.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id = "GETFENV_SETFENV",
|
|
44
|
+
pattern = "[gs]etfenv%s*%(",
|
|
45
|
+
severity = "HIGH",
|
|
46
|
+
name = "Environment Manipulation (getfenv/setfenv)",
|
|
47
|
+
description = "Memanipulasi environment global Luau untuk mengelabui analisis statis.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id = "DISCORD_WEBHOOK",
|
|
51
|
+
pattern = "discord[a-z]*%.com/api/webhooks",
|
|
52
|
+
severity = "HIGH",
|
|
53
|
+
name = "Discord Webhook Exfiltration",
|
|
54
|
+
description = "Mencoba mengirimkan data game atau informasi pemain ke webhook eksternal Discord.",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id = "IP_LOGGER",
|
|
58
|
+
pattern = "webhook%.site|grabify%.link|iplogger|blasze",
|
|
59
|
+
severity = "HIGH",
|
|
60
|
+
name = "External IP/Data Logger",
|
|
61
|
+
description = "Menghubungi domain pencatat IP atau data capture pihak ketiga.",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id = "FORCED_PURCHASE",
|
|
65
|
+
pattern = "MarketplaceService%s*:%s*Prompt[A-Za-z]*Purchase",
|
|
66
|
+
severity = "MEDIUM",
|
|
67
|
+
name = "Forced Robux/Product Purchase",
|
|
68
|
+
description = "Memicu dialog pembelian paksa Gamepass atau Developer Product.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id = "FORCED_TELEPORT",
|
|
72
|
+
pattern = "TeleportService%s*:%s*Teleport",
|
|
73
|
+
severity = "MEDIUM",
|
|
74
|
+
name = "Forced Game Teleportation",
|
|
75
|
+
description = "Memindahkan pemain secara paksa ke game/place lain.",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id = "CHAR_TABLE_OBFUSCATION",
|
|
79
|
+
pattern = "string%.char%s*%(%s*%d+%s*,%s*%d+%s*,%s*%d+",
|
|
80
|
+
severity = "HIGH",
|
|
81
|
+
name = "Obfuscated string.char Array",
|
|
82
|
+
description = "Menyusun kode rahasia tersembunyi menggunakan deretan kode karakter ASCII.",
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
-- Helper: Pengecekan nama script mencurigakan
|
|
87
|
+
local function checkSuspiciousName(name: string): boolean
|
|
88
|
+
for _, pattern in ipairs(SUSPICIOUS_SCRIPT_NAMES) do
|
|
89
|
+
if string.find(name, pattern) then
|
|
90
|
+
return true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
return false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
-- Helper: Deteksi obfuscation byte escape (\108\111\97\100) atau hex (\x6c\x6f)
|
|
97
|
+
local function checkByteObfuscation(source: string): (boolean, string?)
|
|
98
|
+
local byteCount = 0
|
|
99
|
+
for _ in string.gmatch(source, "\\[0-9][0-9][0-9]") do
|
|
100
|
+
byteCount = byteCount + 1
|
|
101
|
+
if byteCount >= 4 then
|
|
102
|
+
return true, "Terdeteksi deretan escape byte ASCII (\\ddd) berulang."
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
local hexCount = 0
|
|
107
|
+
for _ in string.gmatch(source, "\\x[0-9a-fA-F][0-9a-fA-F]") do
|
|
108
|
+
hexCount = hexCount + 1
|
|
109
|
+
if hexCount >= 4 then
|
|
110
|
+
return true, "Terdeteksi deretan escape hex (\\xHH) berulang."
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
return false, nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
-- Helper: Deteksi kode yang sengaja disembunyikan dengan spasi/baris kosong berlebih
|
|
118
|
+
local function checkOffscreenHiding(source: string): (boolean, string?)
|
|
119
|
+
-- Cek spasi horizontal masif (> 80 spasi berturut-turut)
|
|
120
|
+
if string.find(source, " " .. string.rep(" ", 80)) then
|
|
121
|
+
return true, "Kode sengaja didorong ke kanan dengan lebih dari 80 spasi agar tersembunyi dari layar Studio."
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
-- Cek baris kosong masif di awal skrip (> 15 baris kosong)
|
|
125
|
+
local leadingNewlines = 0
|
|
126
|
+
for line in string.gmatch(source, "([^\r\n]*)\r?\n") do
|
|
127
|
+
if string.gsub(line, "%s+", "") == "" then
|
|
128
|
+
leadingNewlines = leadingNewlines + 1
|
|
129
|
+
if leadingNewlines >= 15 then
|
|
130
|
+
return true, "Terdapat lebih dari 15 baris kosong di awal skrip untuk menyembunyikan payload di baris bawah."
|
|
131
|
+
end
|
|
132
|
+
else
|
|
133
|
+
break
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
return false, nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
-- Analisis Source Code dari sebuah Script
|
|
141
|
+
local function scanScriptSource(scriptInstance: Instance, source: string): { any }
|
|
142
|
+
local threats = {}
|
|
143
|
+
local lines = string.split(source, "\n")
|
|
144
|
+
|
|
145
|
+
-- 1. Scan pola heuristik baris per baris
|
|
146
|
+
for lineIdx, line in ipairs(lines) do
|
|
147
|
+
-- Skip baris komentar murni
|
|
148
|
+
local trimmed = string.match(line, "^%s*(.-)%s*$") or ""
|
|
149
|
+
if not string.sub(trimmed, 1, 2) == "--" then
|
|
150
|
+
for _, rule in ipairs(THREAT_PATTERNS) do
|
|
151
|
+
local s, e = string.find(line, rule.pattern)
|
|
152
|
+
if s then
|
|
153
|
+
local snippet = string.sub(line, math.max(1, s - 30), math.min(#line, e + 30))
|
|
154
|
+
table.insert(threats, {
|
|
155
|
+
severity = rule.severity,
|
|
156
|
+
ruleId = rule.id,
|
|
157
|
+
name = rule.name,
|
|
158
|
+
scriptName = scriptInstance.Name,
|
|
159
|
+
scriptPath = scriptInstance:GetFullName(),
|
|
160
|
+
scriptClass = scriptInstance.ClassName,
|
|
161
|
+
line = lineIdx,
|
|
162
|
+
snippet = string.match(snippet, "^%s*(.-)%s*$") or snippet,
|
|
163
|
+
description = rule.description,
|
|
164
|
+
})
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
-- 2. Cek Obfuscation
|
|
171
|
+
local isObf, obfReason = checkByteObfuscation(source)
|
|
172
|
+
if isObf then
|
|
173
|
+
table.insert(threats, {
|
|
174
|
+
severity = "HIGH",
|
|
175
|
+
ruleId = "BYTE_OBFUSCATION",
|
|
176
|
+
name = "Byte / Hex Obfuscation Payload",
|
|
177
|
+
scriptName = scriptInstance.Name,
|
|
178
|
+
scriptPath = scriptInstance:GetFullName(),
|
|
179
|
+
scriptClass = scriptInstance.ClassName,
|
|
180
|
+
line = 1,
|
|
181
|
+
snippet = string.sub(source, 1, 60) .. "...",
|
|
182
|
+
description = obfReason or "Payload kode disamarkan menggunakan escape bytes.",
|
|
183
|
+
})
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
-- 3. Cek Off-screen Code Hiding
|
|
187
|
+
local isHidden, hideReason = checkOffscreenHiding(source)
|
|
188
|
+
if isHidden then
|
|
189
|
+
table.insert(threats, {
|
|
190
|
+
severity = "HIGH",
|
|
191
|
+
ruleId = "OFFSCREEN_HIDING",
|
|
192
|
+
name = "Off-Screen Hidden Code",
|
|
193
|
+
scriptName = scriptInstance.Name,
|
|
194
|
+
scriptPath = scriptInstance:GetFullName(),
|
|
195
|
+
scriptClass = scriptInstance.ClassName,
|
|
196
|
+
line = 1,
|
|
197
|
+
snippet = "[Whitespace Padding Terdeteksi]",
|
|
198
|
+
description = hideReason or "Kode disembunyikan menggunakan whitespace ekstrem.",
|
|
199
|
+
})
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
return threats
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
-- ============================================================================
|
|
206
|
+
-- PUBLIC API
|
|
207
|
+
-- ============================================================================
|
|
208
|
+
|
|
209
|
+
-- Memindai Instance secara rekursif
|
|
210
|
+
function SecurityScanner.scanInstance(rootInstance: Instance, deepScan: boolean?)
|
|
211
|
+
local shouldDeepScan = (deepScan ~= false)
|
|
212
|
+
local allThreats = {}
|
|
213
|
+
local stats = {
|
|
214
|
+
totalInstances = 1,
|
|
215
|
+
models = 0,
|
|
216
|
+
parts = 0,
|
|
217
|
+
meshParts = 0,
|
|
218
|
+
textures = 0,
|
|
219
|
+
scripts = {
|
|
220
|
+
total = 0,
|
|
221
|
+
server = 0,
|
|
222
|
+
localScript = 0,
|
|
223
|
+
module = 0,
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
local function inspectItem(item: Instance)
|
|
228
|
+
stats.totalInstances = stats.totalInstances + 1
|
|
229
|
+
|
|
230
|
+
if item:IsA("Model") then
|
|
231
|
+
stats.models = stats.models + 1
|
|
232
|
+
elseif item:IsA("MeshPart") then
|
|
233
|
+
stats.meshParts = stats.meshParts + 1
|
|
234
|
+
stats.parts = stats.parts + 1
|
|
235
|
+
elseif item:IsA("BasePart") then
|
|
236
|
+
stats.parts = stats.parts + 1
|
|
237
|
+
elseif item:IsA("Texture") or item:IsA("Decal") or item:IsA("SurfaceAppearance") then
|
|
238
|
+
stats.textures = stats.textures + 1
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
if item:IsA("LuaSourceContainer") then
|
|
242
|
+
stats.scripts.total = stats.scripts.total + 1
|
|
243
|
+
if item:IsA("Script") then
|
|
244
|
+
stats.scripts.server = stats.scripts.server + 1
|
|
245
|
+
elseif item:IsA("LocalScript") then
|
|
246
|
+
stats.scripts.localScript = stats.scripts.localScript + 1
|
|
247
|
+
elseif item:IsA("ModuleScript") then
|
|
248
|
+
stats.scripts.module = stats.scripts.module + 1
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
-- Cek nama mencurigakan
|
|
252
|
+
if checkSuspiciousName(item.Name) then
|
|
253
|
+
table.insert(allThreats, {
|
|
254
|
+
severity = "HIGH",
|
|
255
|
+
ruleId = "SUSPICIOUS_SCRIPT_NAME",
|
|
256
|
+
name = "Known Malicious Script Name Pattern",
|
|
257
|
+
scriptName = item.Name,
|
|
258
|
+
scriptPath = item:GetFullName(),
|
|
259
|
+
scriptClass = item.ClassName,
|
|
260
|
+
line = 0,
|
|
261
|
+
snippet = item.Name,
|
|
262
|
+
description = string.format("Nama skrip '%s' cocok dengan pola nama virus/backdoor klasik.", item.Name),
|
|
263
|
+
})
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
-- Deep scan isi source code
|
|
267
|
+
if shouldDeepScan then
|
|
268
|
+
local success, source = pcall(function()
|
|
269
|
+
return (item :: any).Source
|
|
270
|
+
end)
|
|
271
|
+
if success and typeof(source) == "string" and #source > 0 then
|
|
272
|
+
local scriptThreats = scanScriptSource(item, source)
|
|
273
|
+
for _, t in ipairs(scriptThreats) do
|
|
274
|
+
table.insert(allThreats, t)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
inspectItem(rootInstance)
|
|
282
|
+
for _, desc in ipairs(rootInstance:GetDescendants()) do
|
|
283
|
+
inspectItem(desc)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
-- Hitung Level Risiko
|
|
287
|
+
local riskLevel = "SAFE"
|
|
288
|
+
local highCount = 0
|
|
289
|
+
local medCount = 0
|
|
290
|
+
|
|
291
|
+
for _, t in ipairs(allThreats) do
|
|
292
|
+
if t.severity == "HIGH" then
|
|
293
|
+
highCount = highCount + 1
|
|
294
|
+
elseif t.severity == "MEDIUM" then
|
|
295
|
+
medCount = medCount + 1
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
if highCount > 0 then
|
|
300
|
+
riskLevel = "DANGEROUS"
|
|
301
|
+
elseif medCount > 0 or stats.scripts.total > 0 then
|
|
302
|
+
riskLevel = "WARNING"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
riskLevel = riskLevel,
|
|
307
|
+
threats = allThreats,
|
|
308
|
+
stats = stats,
|
|
309
|
+
highThreats = highCount,
|
|
310
|
+
mediumThreats = medCount,
|
|
311
|
+
}
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
-- Menghapus seluruh skrip dari sebuah instance (Sanitasi Total)
|
|
315
|
+
function SecurityScanner.stripScripts(rootInstance: Instance): { any }
|
|
316
|
+
local removed = {}
|
|
317
|
+
|
|
318
|
+
for _, desc in ipairs(rootInstance:GetDescendants()) do
|
|
319
|
+
if desc:IsA("LuaSourceContainer") then
|
|
320
|
+
table.insert(removed, {
|
|
321
|
+
name = desc.Name,
|
|
322
|
+
className = desc.ClassName,
|
|
323
|
+
path = desc:GetFullName(),
|
|
324
|
+
})
|
|
325
|
+
pcall(function()
|
|
326
|
+
desc:Destroy()
|
|
327
|
+
end)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
if rootInstance:IsA("LuaSourceContainer") then
|
|
332
|
+
table.insert(removed, {
|
|
333
|
+
name = rootInstance.Name,
|
|
334
|
+
className = rootInstance.ClassName,
|
|
335
|
+
path = rootInstance:GetFullName(),
|
|
336
|
+
})
|
|
337
|
+
pcall(function()
|
|
338
|
+
rootInstance:Destroy()
|
|
339
|
+
end)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
return removed
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
return SecurityScanner
|
|
346
|
+
|
|
@@ -12,6 +12,10 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
12
12
|
local currentVal = targetInstance[prop]
|
|
13
13
|
if typeof(currentVal) == "Vector3" and type(val) == "table" and #val == 3 then
|
|
14
14
|
targetInstance[prop] = Vector3.new(val[1], val[2], val[3])
|
|
15
|
+
elseif typeof(currentVal) == "Vector2" and type(val) == "table" and #val >= 2 then
|
|
16
|
+
targetInstance[prop] = Vector2.new(val[1], val[2])
|
|
17
|
+
elseif typeof(currentVal) == "UDim" and type(val) == "table" and #val >= 2 then
|
|
18
|
+
targetInstance[prop] = UDim.new(val[1], val[2])
|
|
15
19
|
elseif typeof(currentVal) == "Color3" and type(val) == "table" and #val == 3 then
|
|
16
20
|
local r, g, b = val[1], val[2], val[3]
|
|
17
21
|
if r > 1 or g > 1 or b > 1 then
|
|
@@ -21,6 +25,12 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
21
25
|
end
|
|
22
26
|
elseif typeof(currentVal) == "UDim2" and type(val) == "table" and #val == 4 then
|
|
23
27
|
targetInstance[prop] = UDim2.new(val[1], val[2], val[3], val[4])
|
|
28
|
+
elseif typeof(currentVal) == "NumberRange" and type(val) == "table" then
|
|
29
|
+
targetInstance[prop] = NumberRange.new(val[1], val[2] or val[1])
|
|
30
|
+
elseif typeof(currentVal) == "Rect" and type(val) == "table" and #val >= 4 then
|
|
31
|
+
targetInstance[prop] = Rect.new(val[1], val[2], val[3], val[4])
|
|
32
|
+
elseif typeof(currentVal) == "BrickColor" and type(val) == "string" then
|
|
33
|
+
targetInstance[prop] = BrickColor.new(val)
|
|
24
34
|
elseif typeof(currentVal) == "CFrame" and type(val) == "table" then
|
|
25
35
|
targetInstance[prop] = CFrame.new(unpack(val))
|
|
26
36
|
elseif typeof(currentVal) == "EnumItem" and type(val) == "string" then
|
|
@@ -95,7 +95,7 @@ function UIManager.init(pluginInstance, studioId)
|
|
|
95
95
|
|
|
96
96
|
-- Version
|
|
97
97
|
local versionLabel = Instance.new("TextLabel")
|
|
98
|
-
versionLabel.Text = "v2.
|
|
98
|
+
versionLabel.Text = "v2.3.0"
|
|
99
99
|
versionLabel.Font = Enum.Font.Gotham
|
|
100
100
|
versionLabel.TextSize = 11
|
|
101
101
|
versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)
|