@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.
- 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 +216 -3
- package/dist/luau-validator.js +99 -34
- package/dist/roblox-bridge.js +44 -4
- package/dist/roblox-docs.js +264 -0
- package/package.json +5 -3
- package/public/index.html +619 -108
- package/studio-plugin/Main.server.lua +1 -1
- package/studio-plugin/batch_create_instances.lua +128 -0
- package/studio-plugin/batch_set_properties.lua +69 -0
- package/studio-plugin/insert_asset.lua +85 -12
- package/studio-plugin/nvstudio_mcp.rbxmx +885 -14
- 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
|
@@ -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[
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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)
|