diff --git a/build.gradle.kts b/build.gradle.kts index 47d4886c..a336880e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile val sharedGroup = "com.projectcitybuild.pcbridge" -val sharedVersion = "6.9.0" +val sharedVersion = "6.10.0" group = sharedGroup version = sharedVersion diff --git a/pcbridge-http/src/main/kotlin/com/projectcitybuild/pcbridge/http/pcb/requests/PCBRequest.kt b/pcbridge-http/src/main/kotlin/com/projectcitybuild/pcbridge/http/pcb/requests/PCBRequest.kt index 4f413966..11b9b58c 100644 --- a/pcbridge-http/src/main/kotlin/com/projectcitybuild/pcbridge/http/pcb/requests/PCBRequest.kt +++ b/pcbridge-http/src/main/kotlin/com/projectcitybuild/pcbridge/http/pcb/requests/PCBRequest.kt @@ -61,6 +61,15 @@ internal interface PCBRequest { @Field(value = "uuid") uuid: String, ): HttpOpElevation + @POST("v3/server/audit/command/op") + @FormUrlEncoded + suspend fun auditCommandOp( + @Field(value = "command") command: String, + @Field(value = "actor") actor: String, + @Field(value = "ip") ip: String, + @Field(value = "meta") meta: String?, + ) + /** * Begins registration of a PCB account linked to the current * Minecraft player diff --git a/pcbridge-http/src/main/kotlin/com/projectcitybuild/pcbridge/http/pcb/services/OpElevateHttpService.kt b/pcbridge-http/src/main/kotlin/com/projectcitybuild/pcbridge/http/pcb/services/OpElevateHttpService.kt index 97694ddc..9a38535a 100644 --- a/pcbridge-http/src/main/kotlin/com/projectcitybuild/pcbridge/http/pcb/services/OpElevateHttpService.kt +++ b/pcbridge-http/src/main/kotlin/com/projectcitybuild/pcbridge/http/pcb/services/OpElevateHttpService.kt @@ -22,4 +22,20 @@ class OpElevateHttpService( retrofit.pcb().opRevoke(playerUUID.toString()) } } + + suspend fun audit( + command: String, + actor: String, + ip: String, + meta: String?, + ) = withContext(Dispatchers.IO) { + responseParser.parse { + retrofit.pcb().auditCommandOp( + command = command, + actor = actor, + ip = ip, + meta = meta, + ) + } + } } diff --git a/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/PluginLifecycle.kt b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/PluginLifecycle.kt index 4b7619f1..e7b508d0 100644 --- a/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/PluginLifecycle.kt +++ b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/PluginLifecycle.kt @@ -63,6 +63,7 @@ import com.projectcitybuild.pcbridge.paper.features.maintenance.hooks.listener.M import com.projectcitybuild.pcbridge.paper.features.maintenance.hooks.middleware.MaintenanceConnectionMiddleware import com.projectcitybuild.pcbridge.paper.features.moderate.hooks.commands.KickCommand import com.projectcitybuild.pcbridge.paper.features.pim.hooks.commands.PimCommand +import com.projectcitybuild.pcbridge.paper.features.pim.hooks.listener.OpAuditingListener import com.projectcitybuild.pcbridge.paper.features.pim.hooks.listener.OpClearListener import com.projectcitybuild.pcbridge.paper.features.pim.hooks.listener.OpDialogListener import com.projectcitybuild.pcbridge.paper.features.pim.hooks.listener.OpRestoreListener @@ -213,6 +214,7 @@ class PluginLifecycle : KoinComponent { get(), get(), get(), + get(), get(), get(), get(), diff --git a/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/core/support/spigot/SerializableLocation.kt b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/core/support/spigot/SerializableLocation.kt new file mode 100644 index 00000000..c298f589 --- /dev/null +++ b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/core/support/spigot/SerializableLocation.kt @@ -0,0 +1,28 @@ +package com.projectcitybuild.pcbridge.paper.core.support.spigot + +import kotlinx.serialization.Serializable +import org.bukkit.Location +import org.bukkit.World + +@Serializable +data class SerializableLocation( + val worldId: String, + val worldName: String, + val x: Double, + val y: Double, + val z: Double, + val yaw: Float, + val pitch: Float, +) { + companion object { + fun fromLocation(location: Location, world: World) = SerializableLocation( + worldId = world.uid.toString(), + worldName = world.name, + x = location.x, + y = location.y, + z = location.z, + yaw = location.yaw, + pitch = location.pitch, + ) + } +} \ No newline at end of file diff --git a/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/PimModule.kt b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/PimModule.kt index 70eb3897..631a7f52 100644 --- a/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/PimModule.kt +++ b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/PimModule.kt @@ -1,6 +1,7 @@ package com.projectcitybuild.pcbridge.paper.features.pim import com.projectcitybuild.pcbridge.http.pcb.PCBHttp +import com.projectcitybuild.pcbridge.paper.features.pim.domain.repositories.OpAuditRepository import com.projectcitybuild.pcbridge.paper.features.pim.domain.repositories.OpElevationRepository import com.projectcitybuild.pcbridge.paper.features.pim.domain.services.OpElevationScheduler import com.projectcitybuild.pcbridge.paper.features.pim.domain.services.OpElevationService @@ -9,6 +10,7 @@ import com.projectcitybuild.pcbridge.paper.features.pim.hooks.commands.op.OpRevo import com.projectcitybuild.pcbridge.paper.features.pim.hooks.commands.op.OpGrantCommand import com.projectcitybuild.pcbridge.paper.features.pim.hooks.commands.op.OpStatusCommand import com.projectcitybuild.pcbridge.paper.features.pim.hooks.commands.roles.RolesDebugCommand +import com.projectcitybuild.pcbridge.paper.features.pim.hooks.listener.OpAuditingListener import com.projectcitybuild.pcbridge.paper.features.pim.hooks.listener.OpClearListener import com.projectcitybuild.pcbridge.paper.features.pim.hooks.listener.OpDialogListener import com.projectcitybuild.pcbridge.paper.features.pim.hooks.listener.OpRestoreListener @@ -40,6 +42,12 @@ val pimModule = module { VanillaOpInterceptListener() } + factory { + OpAuditingListener( + opAuditRepository = get(), + ) + } + factory { PimCommand( opGrantCommand = get(), @@ -96,6 +104,13 @@ val pimModule = module { ) } + factory { + OpAuditRepository( + server = get(), + opElevateHttpService = get().opElevate, + ) + } + single { OpElevationScheduler( timer = get(), diff --git a/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/domain/repositories/OpAuditRepository.kt b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/domain/repositories/OpAuditRepository.kt new file mode 100644 index 00000000..56923708 --- /dev/null +++ b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/domain/repositories/OpAuditRepository.kt @@ -0,0 +1,40 @@ +package com.projectcitybuild.pcbridge.paper.features.pim.domain.repositories + +import com.projectcitybuild.pcbridge.http.pcb.services.OpElevateHttpService +import net.kyori.adventure.sound.SoundStop.source +import org.bukkit.Server + +class OpAuditRepository( + private val server: Server, + private val opElevateHttpService: OpElevateHttpService, +) { + suspend fun auditCommand(command: String, actor: Actor) { + opElevateHttpService.audit( + command = command, + actor = actor.name, + ip = server.ip(), + meta = actor.meta, + ) + } + + sealed class Actor(val name: String, val meta: String? = null) { + object Rcon: Actor( + name = "rcon", + ) + object Console: Actor( + name = "console", + ) + data class CommandBlock(val blockMeta: String?): Actor( + name = "command_block", + meta = blockMeta, + ) + object Unknown: Actor( + name = "unknown", + ) + } +} + +private fun Server.ip(): String { + if (ip.isEmpty()) return "127.0.0.1" + return ip +} \ No newline at end of file diff --git a/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/hooks/listener/OpAuditingListener.kt b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/hooks/listener/OpAuditingListener.kt new file mode 100644 index 00000000..6cfe5b66 --- /dev/null +++ b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/hooks/listener/OpAuditingListener.kt @@ -0,0 +1,52 @@ +package com.projectcitybuild.pcbridge.paper.features.pim.hooks.listener + +import com.projectcitybuild.pcbridge.paper.architecture.listeners.scoped +import com.projectcitybuild.pcbridge.paper.core.support.spigot.SerializableLocation +import com.projectcitybuild.pcbridge.paper.features.pim.domain.repositories.OpAuditRepository +import com.projectcitybuild.pcbridge.paper.features.pim.pimTracer +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson +import org.bukkit.command.BlockCommandSender +import org.bukkit.command.CommandSender +import org.bukkit.command.ConsoleCommandSender +import org.bukkit.command.RemoteConsoleCommandSender +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.server.ServerCommandEvent + +class OpAuditingListener( + private val opAuditRepository: OpAuditRepository, +): Listener { + /** + * Monitors /op /deop usage by console, command blocks or rcon, + * and logs their usage for auditing purposes + */ + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + suspend fun onServerCommandEvent( + event: ServerCommandEvent, + ) = event.scoped(pimTracer, this::class.java) { + val command = event.command.lowercase() // No leading "/" + + if (!command.startsWith("op ") && !command.startsWith("deop ")) { + return@scoped + } + val args = command.split(" ") + if (args.size < 2) return@scoped + + opAuditRepository.auditCommand( + command = command, + actor = event.sender.actor() + ) + } +} + +private fun CommandSender.actor() = when (this) { + is RemoteConsoleCommandSender -> OpAuditRepository.Actor.Rcon + is ConsoleCommandSender -> OpAuditRepository.Actor.Console + is BlockCommandSender -> OpAuditRepository.Actor.CommandBlock( + blockMeta = gson().serializer().toJson( + SerializableLocation.fromLocation(block.location, block.world) + ) + ) + else -> OpAuditRepository.Actor.Unknown +} diff --git a/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/hooks/listener/VanillaOpInterceptListener.kt b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/hooks/listener/VanillaOpInterceptListener.kt index 98061f14..ea3a2aee 100644 --- a/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/hooks/listener/VanillaOpInterceptListener.kt +++ b/pcbridge-paper/src/main/kotlin/com/projectcitybuild/pcbridge/paper/features/pim/hooks/listener/VanillaOpInterceptListener.kt @@ -8,7 +8,10 @@ import org.bukkit.event.Listener import org.bukkit.event.player.PlayerCommandPreprocessEvent class VanillaOpInterceptListener: Listener { - @EventHandler(priority = EventPriority.HIGHEST) + /** + * Intercepts /op /deop usage by players and cancels them + */ + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) fun onPlayerCommandPreprocess( event: PlayerCommandPreprocessEvent, ) = event.scopedSync(pimTracer, this::class.java) { diff --git a/pcbridge-paper/src/main/resources/paper-plugin.yml b/pcbridge-paper/src/main/resources/paper-plugin.yml index bdc93063..2ad3d7a9 100644 --- a/pcbridge-paper/src/main/resources/paper-plugin.yml +++ b/pcbridge-paper/src/main/resources/paper-plugin.yml @@ -1,6 +1,6 @@ name: PCBridge website: https://github.com/projectcitybuild/PCBridge -version: 6.9.0 +version: 6.10.0 api-version: 1.21.10 main: com.projectcitybuild.pcbridge.paper.Plugin bootstrapper: com.projectcitybuild.pcbridge.paper.PaperBootstrap