@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.
@@ -10,6 +10,229 @@
10
10
  <int64 name="SourceAssetId">-1</int64>
11
11
  <BinaryString name="Tags"></BinaryString>
12
12
  </Properties>
13
+ <Item class="ModuleScript" referent="RBX_batch_create_instances">
14
+ <Properties>
15
+ <BinaryString name="AttributesSerialize"></BinaryString>
16
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
17
+ <bool name="DefinesCapabilities">false</bool>
18
+ <bool name="Disabled">false</bool>
19
+ <Content name="LinkedSource"><null></null></Content>
20
+ <string name="Name">batch_create_instances</string>
21
+ <int64 name="SourceAssetId">-1</int64>
22
+ <BinaryString name="Tags"></BinaryString>
23
+ <ProtectedString name="Source"><![CDATA[--!strict
24
+ -- ============================================================================
25
+ -- Skill Module: batch_create_instances
26
+ -- Deskripsi: Pembangun Hierarki Instance Massal & Deklaratif (Tree / Batch List)
27
+ -- ============================================================================
28
+
29
+ local HttpService = game:GetService("HttpService")
30
+
31
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
32
+ local success, config = pcall(function()
33
+ if type(data) == "table" then return data end
34
+ return HttpService:JSONDecode(tostring(data))
35
+ end)
36
+
37
+ if not success or type(config) ~= "table" then
38
+ return { status = "error", error = "Format data JSON tidak valid untuk batch_create_instances." }
39
+ end
40
+
41
+ local setPropsFunc = require(script.Parent:WaitForChild("set_properties"))
42
+ local defaultParent = targetInstance or game.Workspace
43
+
44
+ if config.target and ctx and ctx.getInstanceFromPath then
45
+ local resolved = ctx.getInstanceFromPath(config.target)
46
+ if resolved then defaultParent = resolved end
47
+ end
48
+
49
+ local createdList = {}
50
+ local errors = {}
51
+ local createdCount = 0
52
+
53
+ local function applyProps(inst: Instance, props: any)
54
+ if not props or type(props) ~= "table" or not next(props) then return end
55
+ local propJson = HttpService:JSONEncode(props)
56
+ local res = setPropsFunc(inst, propJson, ctx, inst:GetFullName())
57
+ if res and res.status == "error" then
58
+ table.insert(errors, string.format("[%s] Error properti: %s", inst.Name, tostring(res.result or res.error)))
59
+ end
60
+ end
61
+
62
+ -- 1. MODE HIERARCHICAL TREE (Pohon Bersarang)
63
+ local function buildTreeNode(parentInst: Instance, node: any): Instance?
64
+ local className = node.className or node.ClassName
65
+ if not className or type(className) ~= "string" then
66
+ table.insert(errors, "Node pohon tidak memiliki ClassName yang valid.")
67
+ return nil
68
+ end
69
+
70
+ local ok, inst = pcall(function()
71
+ local newObj = Instance.new(className)
72
+ if node.name or node.Name then
73
+ newObj.Name = tostring(node.name or node.Name)
74
+ end
75
+ newObj.Parent = parentInst
76
+ return newObj
77
+ end)
78
+
79
+ if not ok or not inst then
80
+ table.insert(errors, string.format("Gagal membuat instance '%s': %s", className, tostring(inst)))
81
+ return nil
82
+ end
83
+
84
+ createdCount = createdCount + 1
85
+ table.insert(createdList, inst:GetFullName())
86
+
87
+ -- Terapkan properti
88
+ applyProps(inst, node.properties or node.Properties)
89
+
90
+ -- Rekursif untuk seluruh child
91
+ local children = node.children or node.Children
92
+ if children and type(children) == "table" then
93
+ for _, childNode in ipairs(children) do
94
+ buildTreeNode(inst, childNode)
95
+ end
96
+ end
97
+
98
+ return inst
99
+ end
100
+
101
+ -- Eksekusi Mode Tree jika disediakan
102
+ if config.tree and type(config.tree) == "table" then
103
+ buildTreeNode(defaultParent, config.tree)
104
+ end
105
+
106
+ -- 2. MODE FLAT ARRAY (Daftar Instance Terpisah)
107
+ local instancesArray = config.instances or config.items
108
+ if instancesArray and type(instancesArray) == "table" then
109
+ for idx, item in ipairs(instancesArray) do
110
+ local parentInst = defaultParent
111
+ if item.parent and ctx and ctx.getInstanceFromPath then
112
+ local resParent = ctx.getInstanceFromPath(item.parent)
113
+ if resParent then parentInst = resParent end
114
+ end
115
+
116
+ local className = item.className or item.ClassName
117
+ if className then
118
+ local ok, inst = pcall(function()
119
+ local newObj = Instance.new(className)
120
+ if item.name or item.Name then
121
+ newObj.Name = tostring(item.name or item.Name)
122
+ end
123
+ newObj.Parent = parentInst
124
+ return newObj
125
+ end)
126
+
127
+ if ok and inst then
128
+ createdCount = createdCount + 1
129
+ table.insert(createdList, inst:GetFullName())
130
+ applyProps(inst, item.properties or item.Properties)
131
+ else
132
+ table.insert(errors, string.format("Item #%d (%s) gagal dibuat: %s", idx, tostring(className), tostring(inst)))
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ local status = (createdCount > 0 or #errors == 0) and "success" or "error"
139
+ return {
140
+ status = status,
141
+ result = {
142
+ totalCreated = createdCount,
143
+ created = createdList,
144
+ hasErrors = (#errors > 0),
145
+ errors = errors,
146
+ message = string.format("Berhasil membuat %d instance secara batch.", createdCount)
147
+ }
148
+ }
149
+ end
150
+
151
+ ]]></ProtectedString>
152
+ </Properties>
153
+ </Item>
154
+ <Item class="ModuleScript" referent="RBX_batch_set_properties">
155
+ <Properties>
156
+ <BinaryString name="AttributesSerialize"></BinaryString>
157
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
158
+ <bool name="DefinesCapabilities">false</bool>
159
+ <bool name="Disabled">false</bool>
160
+ <Content name="LinkedSource"><null></null></Content>
161
+ <string name="Name">batch_set_properties</string>
162
+ <int64 name="SourceAssetId">-1</int64>
163
+ <BinaryString name="Tags"></BinaryString>
164
+ <ProtectedString name="Source"><![CDATA[--!strict
165
+ -- ============================================================================
166
+ -- Skill Module: batch_set_properties
167
+ -- Deskripsi: Modifikasi Properti Massal untuk Beberapa Instance Sekaligus
168
+ -- ============================================================================
169
+
170
+ local HttpService = game:GetService("HttpService")
171
+
172
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
173
+ local success, config = pcall(function()
174
+ if type(data) == "table" then return data end
175
+ return HttpService:JSONDecode(tostring(data))
176
+ end)
177
+
178
+ if not success or type(config) ~= "table" then
179
+ return { status = "error", error = "Format data JSON tidak valid untuk batch_set_properties." }
180
+ end
181
+
182
+ local setPropsFunc = require(script.Parent:WaitForChild("set_properties"))
183
+ local updates = config.updates or config
184
+ if type(updates) ~= "table" then
185
+ return { status = "error", error = "Parameter 'updates' harus berupa array pembaruan properti." }
186
+ end
187
+
188
+ local updatedCount = 0
189
+ local successfulTargets = {}
190
+ local errors = {}
191
+
192
+ for idx, updateItem in ipairs(updates) do
193
+ local inst = targetInstance
194
+ local instPath = updateItem.target or updateItem.path
195
+ if instPath and ctx and ctx.getInstanceFromPath then
196
+ local resolved = ctx.getInstanceFromPath(instPath)
197
+ if resolved then
198
+ inst = resolved
199
+ else
200
+ table.insert(errors, string.format("Item #%d: Target path '%s' tidak ditemukan.", idx, tostring(instPath)))
201
+ continue
202
+ end
203
+ end
204
+
205
+ local props = updateItem.properties or updateItem.props
206
+ if inst and props and type(props) == "table" then
207
+ local propJson = HttpService:JSONEncode(props)
208
+ local res = setPropsFunc(inst, propJson, ctx, inst:GetFullName())
209
+ if res and res.status == "error" then
210
+ table.insert(errors, string.format("[%s]: %s", inst.Name, tostring(res.result or res.error)))
211
+ else
212
+ updatedCount = updatedCount + 1
213
+ table.insert(successfulTargets, inst:GetFullName())
214
+ end
215
+ else
216
+ table.insert(errors, string.format("Item #%d: Instance atau properties tidak valid.", idx))
217
+ end
218
+ end
219
+
220
+ local status = (updatedCount > 0 or #errors == 0) and "success" or "error"
221
+ return {
222
+ status = status,
223
+ result = {
224
+ totalUpdated = updatedCount,
225
+ updatedInstances = successfulTargets,
226
+ hasErrors = (#errors > 0),
227
+ errors = errors,
228
+ message = string.format("Berhasil memperbarui properti untuk %d instance.", updatedCount)
229
+ }
230
+ }
231
+ end
232
+
233
+ ]]></ProtectedString>
234
+ </Properties>
235
+ </Item>
13
236
  <Item class="ModuleScript" referent="RBX_create_instance">
14
237
  <Properties>
15
238
  <BinaryString name="AttributesSerialize"></BinaryString>
@@ -473,28 +696,101 @@ end
473
696
  <string name="Name">insert_asset</string>
474
697
  <int64 name="SourceAssetId">-1</int64>
475
698
  <BinaryString name="Tags"></BinaryString>
476
- <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
477
- local InsertService = game:GetService("InsertService")
478
- local rawId = tostring(data or ""):gsub("rbxassetid://", ""):gsub("%s+", "")
699
+ <ProtectedString name="Source"><![CDATA[--!strict
700
+ -- ============================================================================
701
+ -- Skill Module: insert_asset (Upgraded with Asset Security Scanner & Script Stripper)
702
+ -- ============================================================================
703
+
704
+ local HttpService = game:GetService("HttpService")
705
+ local InsertService = game:GetService("InsertService")
706
+
707
+ local SecurityScanner = require(script.Parent:WaitForChild("security_scanner"))
708
+
709
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
710
+ local assetIdStr = ""
711
+ local stripScripts = true -- Default TRUE demi keamanan mutlak
712
+ local scanSecurity = true
713
+
714
+ if typeof(data) == "string" then
715
+ local success, parsed = pcall(function()
716
+ return HttpService:JSONDecode(data)
717
+ end)
718
+ if success and typeof(parsed) == "table" then
719
+ assetIdStr = tostring(parsed.asset_id or parsed.assetId or "")
720
+ if parsed.strip_scripts ~= nil then stripScripts = parsed.strip_scripts end
721
+ if parsed.scan_security ~= nil then scanSecurity = parsed.scan_security end
722
+ else
723
+ assetIdStr = data
724
+ end
725
+ elseif typeof(data) == "table" then
726
+ assetIdStr = tostring(data.asset_id or data.assetId or "")
727
+ if data.strip_scripts ~= nil then stripScripts = data.strip_scripts end
728
+ if data.scan_security ~= nil then scanSecurity = data.scan_security end
729
+ end
730
+
731
+ local rawId = string.gsub(assetIdStr, "rbxassetid://", "")
732
+ rawId = string.gsub(rawId, "%s+", "")
479
733
  local assetId = tonumber(rawId)
480
- if not assetId then
734
+
735
+ if not assetId or assetId <= 0 then
481
736
  return { status = "error", error = "Asset ID harus berupa angka yang valid." }
482
737
  end
483
-
738
+
484
739
  local success, model = pcall(function()
485
740
  return InsertService:LoadAsset(assetId)
486
741
  end)
487
-
488
- if success and model then
489
- if targetInstance == game or targetInstance.ClassName == "DataModel" then
490
- model.Parent = game.Workspace
742
+
743
+ if not success or not model then
744
+ return {
745
+ status = "error",
746
+ error = "Gagal memuat aset. Pastikan Asset ID benar dan model berstatus publik di Creator Store."
747
+ }
748
+ end
749
+
750
+ local securityNotes = {}
751
+ local removedScripts = {}
752
+
753
+ -- 1. Scan keamanan jika diaktifkan
754
+ if scanSecurity then
755
+ local scanResult = SecurityScanner.scanInstance(model, true)
756
+ if scanResult.riskLevel == "DANGEROUS" then
757
+ table.insert(securityNotes, string.format("⚠️ PERINGATAN: Aset ini memiliki %d ancaman BERBAHAYA (Backdoor/Obfuscation/Webhook)!", scanResult.highThreats))
758
+ end
759
+ end
760
+
761
+ -- 2. Sanitasi: Hapus skrip jika strip_scripts = true
762
+ if stripScripts then
763
+ removedScripts = SecurityScanner.stripScripts(model)
764
+ if #removedScripts > 0 then
765
+ table.insert(securityNotes, string.format("🛡️ Keamanan: %d skrip bawaan telah dihapus secara otomatis demi keamanan game Anda.", #removedScripts))
491
766
  else
492
- model.Parent = targetInstance
767
+ table.insert(securityNotes, "🛡️ Keamanan: Aset bersih, tidak ditemukan skrip berbahaya.")
493
768
  end
494
- return { status = "success", result = "Aset berhasil dimasukkan ke " .. model.Parent.Name .. "." }
495
769
  else
496
- return { status = "error", error = "Gagal memuat aset. Pastikan Asset ID benar dan akun/plugin Anda memiliki izin (Ownership/Public)." }
770
+ table.insert(securityNotes, "ℹ️ Catatan: strip_scripts = false, skrip bawaan model dipertahankan.")
497
771
  end
772
+
773
+ -- 3. Masukkan model ke target
774
+ local parentTarget = targetInstance
775
+ if not parentTarget or parentTarget == game or parentTarget.ClassName == "DataModel" then
776
+ parentTarget = game.Workspace
777
+ end
778
+
779
+ model.Parent = parentTarget
780
+
781
+ return {
782
+ status = "success",
783
+ result = {
784
+ message = string.format("Aset '%s' (ID: %d) berhasil dimasukkan ke %s.", model.Name, assetId, parentTarget.Name),
785
+ assetName = model.Name,
786
+ assetId = assetId,
787
+ parent = parentTarget:GetFullName(),
788
+ stripScriptsApplied = stripScripts,
789
+ removedScriptsCount = #removedScripts,
790
+ removedScripts = removedScripts,
791
+ securityNotes = table.concat(securityNotes, "\n"),
792
+ }
793
+ }
498
794
  end
499
795
  ]]></ProtectedString>
500
796
  </Properties>
@@ -511,7 +807,7 @@ end
511
807
  <BinaryString name="Tags"></BinaryString>
512
808
  <token name="RunContext">0</token>
513
809
  <ProtectedString name="Source"><![CDATA[-- ══════════════════════════════════════════════════════════════════════
514
- -- NVStudio MCP Plugin v2.2.2
810
+ -- NVStudio MCP Plugin v2.3.0
515
811
  -- Arsitektur: Dual-Mode WebSocket Duplex (Port 3055/ws) + Fallback Long-Polling
516
812
  -- Plugin auto-connect saat dimuat via WebSocket native, dengan fallback otomatis
517
813
  -- ke HTTP stream jika WebSocket tidak tersedia di environment Roblox Studio ini.
@@ -1447,6 +1743,118 @@ return function(targetInstance, data, ctx, targetPath)
1447
1743
  affected_lines = affectedLines
1448
1744
  }
1449
1745
  end
1746
+ ]]></ProtectedString>
1747
+ </Properties>
1748
+ </Item>
1749
+ <Item class="ModuleScript" referent="RBX_preview_asset">
1750
+ <Properties>
1751
+ <BinaryString name="AttributesSerialize"></BinaryString>
1752
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
1753
+ <bool name="DefinesCapabilities">false</bool>
1754
+ <bool name="Disabled">false</bool>
1755
+ <Content name="LinkedSource"><null></null></Content>
1756
+ <string name="Name">preview_asset</string>
1757
+ <int64 name="SourceAssetId">-1</int64>
1758
+ <BinaryString name="Tags"></BinaryString>
1759
+ <ProtectedString name="Source"><![CDATA[--!strict
1760
+ -- ============================================================================
1761
+ -- Skill Module: preview_asset
1762
+ -- Deskripsi: Pratinjau & Pemindaian Keamanan Aset Creator Store Secara Terisolasi
1763
+ -- ============================================================================
1764
+
1765
+ local HttpService = game:GetService("HttpService")
1766
+ local InsertService = game:GetService("InsertService")
1767
+
1768
+ local SecurityScanner = require(script.Parent:WaitForChild("security_scanner"))
1769
+
1770
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
1771
+ local assetIdStr = ""
1772
+ local deepScan = true
1773
+
1774
+ if typeof(data) == "string" then
1775
+ local success, parsed = pcall(function()
1776
+ return HttpService:JSONDecode(data)
1777
+ end)
1778
+ if success and typeof(parsed) == "table" then
1779
+ assetIdStr = tostring(parsed.asset_id or parsed.assetId or "")
1780
+ if parsed.deep_scan ~= nil then deepScan = parsed.deep_scan end
1781
+ else
1782
+ assetIdStr = data
1783
+ end
1784
+ elseif typeof(data) == "table" then
1785
+ assetIdStr = tostring(data.asset_id or data.assetId or "")
1786
+ if data.deep_scan ~= nil then deepScan = data.deep_scan end
1787
+ end
1788
+
1789
+ local cleanIdStr = string.gsub(assetIdStr, "rbxassetid://", "")
1790
+ cleanIdStr = string.gsub(cleanIdStr, "%s+", "")
1791
+ local assetId = tonumber(cleanIdStr)
1792
+
1793
+ if not assetId or assetId <= 0 then
1794
+ return { status = "error", error = "Asset ID tidak valid. Berikan Asset ID angka (contoh: 123456789)." }
1795
+ end
1796
+
1797
+ -- Muat Aset secara terisolasi (unparented / memory only)
1798
+ local ok, model = pcall(function()
1799
+ return InsertService:LoadAsset(assetId)
1800
+ end)
1801
+
1802
+ if not ok or not model then
1803
+ return {
1804
+ status = "error",
1805
+ error = string.format("Gagal memuat aset ID %d. Pastikan Asset ID publik dan terdaftar di Creator Store.", assetId)
1806
+ }
1807
+ end
1808
+
1809
+ local scanResult
1810
+ local scanOk, err = pcall(function()
1811
+ scanResult = SecurityScanner.scanInstance(model, deepScan)
1812
+ end)
1813
+
1814
+ local rootName = model.Name
1815
+ local childrenOverview = {}
1816
+ for _, child in ipairs(model:GetChildren()) do
1817
+ table.insert(childrenOverview, {
1818
+ name = child.Name,
1819
+ className = child.ClassName,
1820
+ })
1821
+ end
1822
+
1823
+ -- Hancurkan model sementara dari memori (Cleanup Mutlak)
1824
+ pcall(function()
1825
+ model:Destroy()
1826
+ end)
1827
+
1828
+ if not scanOk or not scanResult then
1829
+ return {
1830
+ status = "error",
1831
+ error = "Gagal memindai aset: " .. tostring(err)
1832
+ }
1833
+ end
1834
+
1835
+ local recommendation = "Aman untuk dimasukkan langsung menggunakan insert_asset."
1836
+ if scanResult.riskLevel == "DANGEROUS" then
1837
+ recommendation = "PERINGATAN: Aset ini memiliki ancaman BERBAHAYA! Wajib gunakan 'insert_asset' dengan strip_scripts = true, atau hindari aset ini."
1838
+ elseif scanResult.riskLevel == "WARNING" then
1839
+ recommendation = "Perhatian: Aset mengandung skrip atau peringatan moderat. Periksa kembali atau gunakan strip_scripts = true jika skrip tidak diperlukan."
1840
+ end
1841
+
1842
+ return {
1843
+ status = "success",
1844
+ result = {
1845
+ assetId = assetId,
1846
+ assetName = rootName,
1847
+ riskLevel = scanResult.riskLevel,
1848
+ recommendation = recommendation,
1849
+ stats = scanResult.stats,
1850
+ highThreatsCount = scanResult.highThreats,
1851
+ mediumThreatsCount = scanResult.mediumThreats,
1852
+ threats = scanResult.threats,
1853
+ topLevelChildren = childrenOverview,
1854
+ }
1855
+ }
1856
+ end
1857
+
1450
1858
  ]]></ProtectedString>
1451
1859
  </Properties>
1452
1860
  </Item>
@@ -1479,6 +1887,100 @@ end
1479
1887
  return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. ")." }
1480
1888
  end
1481
1889
  end
1890
+ ]]></ProtectedString>
1891
+ </Properties>
1892
+ </Item>
1893
+ <Item class="ModuleScript" referent="RBX_scan_security">
1894
+ <Properties>
1895
+ <BinaryString name="AttributesSerialize"></BinaryString>
1896
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
1897
+ <bool name="DefinesCapabilities">false</bool>
1898
+ <bool name="Disabled">false</bool>
1899
+ <Content name="LinkedSource"><null></null></Content>
1900
+ <string name="Name">scan_security</string>
1901
+ <int64 name="SourceAssetId">-1</int64>
1902
+ <BinaryString name="Tags"></BinaryString>
1903
+ <ProtectedString name="Source"><![CDATA[--!strict
1904
+ -- ============================================================================
1905
+ -- Skill Module: scan_security
1906
+ -- Deskripsi: Audit Keamanan Instance/Model yang Sudah Ada di Tempat Kerja Studio
1907
+ -- ============================================================================
1908
+
1909
+ local HttpService = game:GetService("HttpService")
1910
+ local SecurityScanner = require(script.Parent:WaitForChild("security_scanner"))
1911
+
1912
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
1913
+ local target = targetInstance or game.Workspace
1914
+ local deepScan = true
1915
+ local cleanThreats = false
1916
+
1917
+ if typeof(data) == "string" and #data > 0 then
1918
+ local success, parsed = pcall(function()
1919
+ return HttpService:JSONDecode(data)
1920
+ end)
1921
+ if success and typeof(parsed) == "table" then
1922
+ if parsed.deep_scan ~= nil then deepScan = parsed.deep_scan end
1923
+ if parsed.clean_threats ~= nil then cleanThreats = parsed.clean_threats end
1924
+ end
1925
+ elseif typeof(data) == "table" then
1926
+ if data.deep_scan ~= nil then deepScan = data.deep_scan end
1927
+ if data.clean_threats ~= nil then cleanThreats = data.clean_threats end
1928
+ end
1929
+
1930
+ local scanResult
1931
+ local ok, err = pcall(function()
1932
+ scanResult = SecurityScanner.scanInstance(target, deepScan)
1933
+ end)
1934
+
1935
+ if not ok or not scanResult then
1936
+ return {
1937
+ status = "error",
1938
+ error = "Gagal memindai target: " .. tostring(err)
1939
+ }
1940
+ end
1941
+
1942
+ local cleanedScripts = {}
1943
+ if cleanThreats and #scanResult.threats > 0 then
1944
+ -- Hapus skrip yang terdeteksi bahaya (HIGH severity)
1945
+ local scriptsToDestroy = {}
1946
+ for _, threat in ipairs(scanResult.threats) do
1947
+ if threat.severity == "HIGH" and threat.scriptPath then
1948
+ scriptsToDestroy[threat.scriptPath] = true
1949
+ end
1950
+ end
1951
+
1952
+ local function purge(instance: Instance)
1953
+ for _, desc in ipairs(instance:GetDescendants()) do
1954
+ if desc:IsA("LuaSourceContainer") and scriptsToDestroy[desc:GetFullName()] then
1955
+ table.insert(cleanedScripts, {
1956
+ name = desc.Name,
1957
+ className = desc.ClassName,
1958
+ path = desc:GetFullName(),
1959
+ })
1960
+ pcall(function() desc:Destroy() end)
1961
+ end
1962
+ end
1963
+ end
1964
+
1965
+ purge(target)
1966
+ end
1967
+
1968
+ return {
1969
+ status = "success",
1970
+ result = {
1971
+ target = target:GetFullName(),
1972
+ riskLevel = scanResult.riskLevel,
1973
+ stats = scanResult.stats,
1974
+ highThreatsCount = scanResult.highThreats,
1975
+ mediumThreatsCount = scanResult.mediumThreats,
1976
+ threats = scanResult.threats,
1977
+ cleanedThreatsCount = #cleanedScripts,
1978
+ cleanedScripts = cleanedScripts,
1979
+ isCleaned = cleanThreats,
1980
+ }
1981
+ }
1982
+ end
1983
+
1482
1984
  ]]></ProtectedString>
1483
1985
  </Properties>
1484
1986
  </Item>
@@ -1624,6 +2126,365 @@ end
1624
2126
  return { status = "success", result = "Tidak ditemukan instance dengan nama: " .. searchName }
1625
2127
  end
1626
2128
  end
2129
+ ]]></ProtectedString>
2130
+ </Properties>
2131
+ </Item>
2132
+ <Item class="ModuleScript" referent="RBX_security_scanner">
2133
+ <Properties>
2134
+ <BinaryString name="AttributesSerialize"></BinaryString>
2135
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
2136
+ <bool name="DefinesCapabilities">false</bool>
2137
+ <bool name="Disabled">false</bool>
2138
+ <Content name="LinkedSource"><null></null></Content>
2139
+ <string name="Name">security_scanner</string>
2140
+ <int64 name="SourceAssetId">-1</int64>
2141
+ <BinaryString name="Tags"></BinaryString>
2142
+ <ProtectedString name="Source"><![CDATA[--!strict
2143
+ -- ============================================================================
2144
+ -- Module: security_scanner
2145
+ -- Deskripsi: Engine Heuristik Pemindaian Keamanan & Backdoor Detection
2146
+ -- Standar: Business-Grade, Zero-Execution Isolation, Comprehensive Threat Rules
2147
+ -- ============================================================================
2148
+
2149
+ local SecurityScanner = {}
2150
+
2151
+ -- Daftar nama skrip yang sering digunakan oleh virus/backdoor klasik Roblox
2152
+ local SUSPICIOUS_SCRIPT_NAMES = {
2153
+ "[Vv]accine",
2154
+ "[Aa]nti[Ll]ag",
2155
+ "[Ff]ix[Ll]ag",
2156
+ "[Ii]nfection",
2157
+ "[Ff]ire",
2158
+ "[Cc]lean",
2159
+ "[Vv]irus",
2160
+ "4[Dd]",
2161
+ "4[Dd]_[Pp]lugin",
2162
+ "[Ss]pread",
2163
+ "[Ll]ag[Ff]ixer",
2164
+ "[Dd]up",
2165
+ }
2166
+
2167
+ -- Aturan Pola Heuristik Kode Berbahaya
2168
+ local THREAT_PATTERNS = {
2169
+ {
2170
+ id = "BACKDOOR_REQUIRE",
2171
+ pattern = "require%s*%(%s*%d+%s*%)",
2172
+ severity = "HIGH",
2173
+ name = "Backdoor Numeric Require",
2174
+ description = "Memanggil modul eksternal pihak ketiga via Asset ID numerik (indikasi backdoor/remote loader).",
2175
+ },
2176
+ {
2177
+ id = "LOADSTRING_EXEC",
2178
+ pattern = "loadstring%s*%(",
2179
+ severity = "HIGH",
2180
+ name = "Dynamic Code Execution (loadstring)",
2181
+ description = "Mengeksekusi string kode dinamis yang tidak dapat diaudit.",
2182
+ },
2183
+ {
2184
+ id = "GETFENV_SETFENV",
2185
+ pattern = "[gs]etfenv%s*%(",
2186
+ severity = "HIGH",
2187
+ name = "Environment Manipulation (getfenv/setfenv)",
2188
+ description = "Memanipulasi environment global Luau untuk mengelabui analisis statis.",
2189
+ },
2190
+ {
2191
+ id = "DISCORD_WEBHOOK",
2192
+ pattern = "discord[a-z]*%.com/api/webhooks",
2193
+ severity = "HIGH",
2194
+ name = "Discord Webhook Exfiltration",
2195
+ description = "Mencoba mengirimkan data game atau informasi pemain ke webhook eksternal Discord.",
2196
+ },
2197
+ {
2198
+ id = "IP_LOGGER",
2199
+ pattern = "webhook%.site|grabify%.link|iplogger|blasze",
2200
+ severity = "HIGH",
2201
+ name = "External IP/Data Logger",
2202
+ description = "Menghubungi domain pencatat IP atau data capture pihak ketiga.",
2203
+ },
2204
+ {
2205
+ id = "FORCED_PURCHASE",
2206
+ pattern = "MarketplaceService%s*:%s*Prompt[A-Za-z]*Purchase",
2207
+ severity = "MEDIUM",
2208
+ name = "Forced Robux/Product Purchase",
2209
+ description = "Memicu dialog pembelian paksa Gamepass atau Developer Product.",
2210
+ },
2211
+ {
2212
+ id = "FORCED_TELEPORT",
2213
+ pattern = "TeleportService%s*:%s*Teleport",
2214
+ severity = "MEDIUM",
2215
+ name = "Forced Game Teleportation",
2216
+ description = "Memindahkan pemain secara paksa ke game/place lain.",
2217
+ },
2218
+ {
2219
+ id = "CHAR_TABLE_OBFUSCATION",
2220
+ pattern = "string%.char%s*%(%s*%d+%s*,%s*%d+%s*,%s*%d+",
2221
+ severity = "HIGH",
2222
+ name = "Obfuscated string.char Array",
2223
+ description = "Menyusun kode rahasia tersembunyi menggunakan deretan kode karakter ASCII.",
2224
+ },
2225
+ }
2226
+
2227
+ -- Helper: Pengecekan nama script mencurigakan
2228
+ local function checkSuspiciousName(name: string): boolean
2229
+ for _, pattern in ipairs(SUSPICIOUS_SCRIPT_NAMES) do
2230
+ if string.find(name, pattern) then
2231
+ return true
2232
+ end
2233
+ end
2234
+ return false
2235
+ end
2236
+
2237
+ -- Helper: Deteksi obfuscation byte escape (\108\111\97\100) atau hex (\x6c\x6f)
2238
+ local function checkByteObfuscation(source: string): (boolean, string?)
2239
+ local byteCount = 0
2240
+ for _ in string.gmatch(source, "\\[0-9][0-9][0-9]") do
2241
+ byteCount = byteCount + 1
2242
+ if byteCount >= 4 then
2243
+ return true, "Terdeteksi deretan escape byte ASCII (\\ddd) berulang."
2244
+ end
2245
+ end
2246
+
2247
+ local hexCount = 0
2248
+ for _ in string.gmatch(source, "\\x[0-9a-fA-F][0-9a-fA-F]") do
2249
+ hexCount = hexCount + 1
2250
+ if hexCount >= 4 then
2251
+ return true, "Terdeteksi deretan escape hex (\\xHH) berulang."
2252
+ end
2253
+ end
2254
+
2255
+ return false, nil
2256
+ end
2257
+
2258
+ -- Helper: Deteksi kode yang sengaja disembunyikan dengan spasi/baris kosong berlebih
2259
+ local function checkOffscreenHiding(source: string): (boolean, string?)
2260
+ -- Cek spasi horizontal masif (> 80 spasi berturut-turut)
2261
+ if string.find(source, " " .. string.rep(" ", 80)) then
2262
+ return true, "Kode sengaja didorong ke kanan dengan lebih dari 80 spasi agar tersembunyi dari layar Studio."
2263
+ end
2264
+
2265
+ -- Cek baris kosong masif di awal skrip (> 15 baris kosong)
2266
+ local leadingNewlines = 0
2267
+ for line in string.gmatch(source, "([^\r\n]*)\r?\n") do
2268
+ if string.gsub(line, "%s+", "") == "" then
2269
+ leadingNewlines = leadingNewlines + 1
2270
+ if leadingNewlines >= 15 then
2271
+ return true, "Terdapat lebih dari 15 baris kosong di awal skrip untuk menyembunyikan payload di baris bawah."
2272
+ end
2273
+ else
2274
+ break
2275
+ end
2276
+ end
2277
+
2278
+ return false, nil
2279
+ end
2280
+
2281
+ -- Analisis Source Code dari sebuah Script
2282
+ local function scanScriptSource(scriptInstance: Instance, source: string): { any }
2283
+ local threats = {}
2284
+ local lines = string.split(source, "\n")
2285
+
2286
+ -- 1. Scan pola heuristik baris per baris
2287
+ for lineIdx, line in ipairs(lines) do
2288
+ -- Skip baris komentar murni
2289
+ local trimmed = string.match(line, "^%s*(.-)%s*$") or ""
2290
+ if not string.sub(trimmed, 1, 2) == "--" then
2291
+ for _, rule in ipairs(THREAT_PATTERNS) do
2292
+ local s, e = string.find(line, rule.pattern)
2293
+ if s then
2294
+ local snippet = string.sub(line, math.max(1, s - 30), math.min(#line, e + 30))
2295
+ table.insert(threats, {
2296
+ severity = rule.severity,
2297
+ ruleId = rule.id,
2298
+ name = rule.name,
2299
+ scriptName = scriptInstance.Name,
2300
+ scriptPath = scriptInstance:GetFullName(),
2301
+ scriptClass = scriptInstance.ClassName,
2302
+ line = lineIdx,
2303
+ snippet = string.match(snippet, "^%s*(.-)%s*$") or snippet,
2304
+ description = rule.description,
2305
+ })
2306
+ end
2307
+ end
2308
+ end
2309
+ end
2310
+
2311
+ -- 2. Cek Obfuscation
2312
+ local isObf, obfReason = checkByteObfuscation(source)
2313
+ if isObf then
2314
+ table.insert(threats, {
2315
+ severity = "HIGH",
2316
+ ruleId = "BYTE_OBFUSCATION",
2317
+ name = "Byte / Hex Obfuscation Payload",
2318
+ scriptName = scriptInstance.Name,
2319
+ scriptPath = scriptInstance:GetFullName(),
2320
+ scriptClass = scriptInstance.ClassName,
2321
+ line = 1,
2322
+ snippet = string.sub(source, 1, 60) .. "...",
2323
+ description = obfReason or "Payload kode disamarkan menggunakan escape bytes.",
2324
+ })
2325
+ end
2326
+
2327
+ -- 3. Cek Off-screen Code Hiding
2328
+ local isHidden, hideReason = checkOffscreenHiding(source)
2329
+ if isHidden then
2330
+ table.insert(threats, {
2331
+ severity = "HIGH",
2332
+ ruleId = "OFFSCREEN_HIDING",
2333
+ name = "Off-Screen Hidden Code",
2334
+ scriptName = scriptInstance.Name,
2335
+ scriptPath = scriptInstance:GetFullName(),
2336
+ scriptClass = scriptInstance.ClassName,
2337
+ line = 1,
2338
+ snippet = "[Whitespace Padding Terdeteksi]",
2339
+ description = hideReason or "Kode disembunyikan menggunakan whitespace ekstrem.",
2340
+ })
2341
+ end
2342
+
2343
+ return threats
2344
+ end
2345
+
2346
+ -- ============================================================================
2347
+ -- PUBLIC API
2348
+ -- ============================================================================
2349
+
2350
+ -- Memindai Instance secara rekursif
2351
+ function SecurityScanner.scanInstance(rootInstance: Instance, deepScan: boolean?)
2352
+ local shouldDeepScan = (deepScan ~= false)
2353
+ local allThreats = {}
2354
+ local stats = {
2355
+ totalInstances = 1,
2356
+ models = 0,
2357
+ parts = 0,
2358
+ meshParts = 0,
2359
+ textures = 0,
2360
+ scripts = {
2361
+ total = 0,
2362
+ server = 0,
2363
+ localScript = 0,
2364
+ module = 0,
2365
+ },
2366
+ }
2367
+
2368
+ local function inspectItem(item: Instance)
2369
+ stats.totalInstances = stats.totalInstances + 1
2370
+
2371
+ if item:IsA("Model") then
2372
+ stats.models = stats.models + 1
2373
+ elseif item:IsA("MeshPart") then
2374
+ stats.meshParts = stats.meshParts + 1
2375
+ stats.parts = stats.parts + 1
2376
+ elseif item:IsA("BasePart") then
2377
+ stats.parts = stats.parts + 1
2378
+ elseif item:IsA("Texture") or item:IsA("Decal") or item:IsA("SurfaceAppearance") then
2379
+ stats.textures = stats.textures + 1
2380
+ end
2381
+
2382
+ if item:IsA("LuaSourceContainer") then
2383
+ stats.scripts.total = stats.scripts.total + 1
2384
+ if item:IsA("Script") then
2385
+ stats.scripts.server = stats.scripts.server + 1
2386
+ elseif item:IsA("LocalScript") then
2387
+ stats.scripts.localScript = stats.scripts.localScript + 1
2388
+ elseif item:IsA("ModuleScript") then
2389
+ stats.scripts.module = stats.scripts.module + 1
2390
+ end
2391
+
2392
+ -- Cek nama mencurigakan
2393
+ if checkSuspiciousName(item.Name) then
2394
+ table.insert(allThreats, {
2395
+ severity = "HIGH",
2396
+ ruleId = "SUSPICIOUS_SCRIPT_NAME",
2397
+ name = "Known Malicious Script Name Pattern",
2398
+ scriptName = item.Name,
2399
+ scriptPath = item:GetFullName(),
2400
+ scriptClass = item.ClassName,
2401
+ line = 0,
2402
+ snippet = item.Name,
2403
+ description = string.format("Nama skrip '%s' cocok dengan pola nama virus/backdoor klasik.", item.Name),
2404
+ })
2405
+ end
2406
+
2407
+ -- Deep scan isi source code
2408
+ if shouldDeepScan then
2409
+ local success, source = pcall(function()
2410
+ return (item :: any).Source
2411
+ end)
2412
+ if success and typeof(source) == "string" and #source > 0 then
2413
+ local scriptThreats = scanScriptSource(item, source)
2414
+ for _, t in ipairs(scriptThreats) do
2415
+ table.insert(allThreats, t)
2416
+ end
2417
+ end
2418
+ end
2419
+ end
2420
+ end
2421
+
2422
+ inspectItem(rootInstance)
2423
+ for _, desc in ipairs(rootInstance:GetDescendants()) do
2424
+ inspectItem(desc)
2425
+ end
2426
+
2427
+ -- Hitung Level Risiko
2428
+ local riskLevel = "SAFE"
2429
+ local highCount = 0
2430
+ local medCount = 0
2431
+
2432
+ for _, t in ipairs(allThreats) do
2433
+ if t.severity == "HIGH" then
2434
+ highCount = highCount + 1
2435
+ elseif t.severity == "MEDIUM" then
2436
+ medCount = medCount + 1
2437
+ end
2438
+ end
2439
+
2440
+ if highCount > 0 then
2441
+ riskLevel = "DANGEROUS"
2442
+ elseif medCount > 0 or stats.scripts.total > 0 then
2443
+ riskLevel = "WARNING"
2444
+ end
2445
+
2446
+ return {
2447
+ riskLevel = riskLevel,
2448
+ threats = allThreats,
2449
+ stats = stats,
2450
+ highThreats = highCount,
2451
+ mediumThreats = medCount,
2452
+ }
2453
+ end
2454
+
2455
+ -- Menghapus seluruh skrip dari sebuah instance (Sanitasi Total)
2456
+ function SecurityScanner.stripScripts(rootInstance: Instance): { any }
2457
+ local removed = {}
2458
+
2459
+ for _, desc in ipairs(rootInstance:GetDescendants()) do
2460
+ if desc:IsA("LuaSourceContainer") then
2461
+ table.insert(removed, {
2462
+ name = desc.Name,
2463
+ className = desc.ClassName,
2464
+ path = desc:GetFullName(),
2465
+ })
2466
+ pcall(function()
2467
+ desc:Destroy()
2468
+ end)
2469
+ end
2470
+ end
2471
+
2472
+ if rootInstance:IsA("LuaSourceContainer") then
2473
+ table.insert(removed, {
2474
+ name = rootInstance.Name,
2475
+ className = rootInstance.ClassName,
2476
+ path = rootInstance:GetFullName(),
2477
+ })
2478
+ pcall(function()
2479
+ rootInstance:Destroy()
2480
+ end)
2481
+ end
2482
+
2483
+ return removed
2484
+ end
2485
+
2486
+ return SecurityScanner
2487
+
1627
2488
  ]]></ProtectedString>
1628
2489
  </Properties>
1629
2490
  </Item>
@@ -1651,6 +2512,10 @@ end
1651
2512
  local currentVal = targetInstance[prop]
1652
2513
  if typeof(currentVal) == "Vector3" and type(val) == "table" and #val == 3 then
1653
2514
  targetInstance[prop] = Vector3.new(val[1], val[2], val[3])
2515
+ elseif typeof(currentVal) == "Vector2" and type(val) == "table" and #val >= 2 then
2516
+ targetInstance[prop] = Vector2.new(val[1], val[2])
2517
+ elseif typeof(currentVal) == "UDim" and type(val) == "table" and #val >= 2 then
2518
+ targetInstance[prop] = UDim.new(val[1], val[2])
1654
2519
  elseif typeof(currentVal) == "Color3" and type(val) == "table" and #val == 3 then
1655
2520
  local r, g, b = val[1], val[2], val[3]
1656
2521
  if r > 1 or g > 1 or b > 1 then
@@ -1660,6 +2525,12 @@ end
1660
2525
  end
1661
2526
  elseif typeof(currentVal) == "UDim2" and type(val) == "table" and #val == 4 then
1662
2527
  targetInstance[prop] = UDim2.new(val[1], val[2], val[3], val[4])
2528
+ elseif typeof(currentVal) == "NumberRange" and type(val) == "table" then
2529
+ targetInstance[prop] = NumberRange.new(val[1], val[2] or val[1])
2530
+ elseif typeof(currentVal) == "Rect" and type(val) == "table" and #val >= 4 then
2531
+ targetInstance[prop] = Rect.new(val[1], val[2], val[3], val[4])
2532
+ elseif typeof(currentVal) == "BrickColor" and type(val) == "string" then
2533
+ targetInstance[prop] = BrickColor.new(val)
1663
2534
  elseif typeof(currentVal) == "CFrame" and type(val) == "table" then
1664
2535
  targetInstance[prop] = CFrame.new(unpack(val))
1665
2536
  elseif typeof(currentVal) == "EnumItem" and type(val) == "string" then
@@ -1820,7 +2691,7 @@ function UIManager.init(pluginInstance, studioId)
1820
2691
 
1821
2692
  -- Version
1822
2693
  local versionLabel = Instance.new("TextLabel")
1823
- versionLabel.Text = "v2.2.0"
2694
+ versionLabel.Text = "v2.3.0"
1824
2695
  versionLabel.Font = Enum.Font.Gotham
1825
2696
  versionLabel.TextSize = 11
1826
2697
  versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)