@adiholick/nvstudio-mcp 2.2.3 → 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.
@@ -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.2.0"
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)