Files
oslstats/pkg/slapshotapi/tampering.go

181 lines
5.1 KiB
Go

package slapshotapi
import (
"fmt"
"strings"
"github.com/pkg/errors"
)
// DetectTampering analyzes 3 period logs for modification signs.
// Returns (isTampering bool, reason string, error).
func DetectTampering(logs []*MatchLog) (bool, string, error) {
if len(logs) != 3 {
return false, "", errors.New("exactly 3 period logs are required")
}
reasons := []string{}
// Check metadata consistency
tampered, reason, err := ValidateMetadataConsistency(logs)
if err != nil {
return false, "", errors.Wrap(err, "ValidateMetadataConsistency")
}
if tampered {
reasons = append(reasons, reason)
}
// Check score progression
tampered, reason, err = ValidateScoreProgression(logs)
if err != nil {
return false, "", errors.Wrap(err, "ValidateScoreProgression")
}
if tampered {
reasons = append(reasons, reason)
}
// Check player consistency
tampered, reason, err = ValidatePlayerConsistency(logs)
if err != nil {
return false, "", errors.Wrap(err, "ValidatePlayerConsistency")
}
if tampered {
reasons = append(reasons, reason)
}
if len(reasons) > 0 {
return true, strings.Join(reasons, "; "), nil
}
return false, "", nil
}
// ValidateMetadataConsistency checks that arena, match_length, and
// custom_mercy_rule are consistent across all periods, and warns if any
// period has periods_enabled set to "False" (periods disabled).
func ValidateMetadataConsistency(logs []*MatchLog) (bool, string, error) {
if len(logs) != 3 {
return false, "", errors.New("exactly 3 period logs are required")
}
ref := logs[0]
inconsistencies := []string{}
for i := 1; i < len(logs); i++ {
log := logs[i]
if log.Arena != ref.Arena {
inconsistencies = append(inconsistencies,
fmt.Sprintf("arena differs in period %d (%q vs %q)", i+1, log.Arena, ref.Arena))
}
if log.MatchLength != ref.MatchLength {
inconsistencies = append(inconsistencies,
fmt.Sprintf("match_length differs in period %d (%s vs %s)", i+1, log.MatchLength, ref.MatchLength))
}
if log.CustomMercyRule != ref.CustomMercyRule {
inconsistencies = append(inconsistencies,
fmt.Sprintf("custom_mercy_rule differs in period %d (%s vs %s)", i+1, log.CustomMercyRule, ref.CustomMercyRule))
}
}
// Warn if any period has periods disabled
for i, log := range logs {
if strings.EqualFold(log.PeriodsEnabled, "False") {
inconsistencies = append(inconsistencies,
fmt.Sprintf("periods_enabled is False in period %d", i+1))
}
}
if len(inconsistencies) > 0 {
return true, "metadata inconsistency: " + strings.Join(inconsistencies, ", "), nil
}
return false, "", nil
}
// ValidateScoreProgression checks that scores only increase or stay the same
// across periods (cumulative stats).
func ValidateScoreProgression(logs []*MatchLog) (bool, string, error) {
if len(logs) != 3 {
return false, "", errors.New("exactly 3 period logs are required")
}
issues := []string{}
for i := 1; i < len(logs); i++ {
prev := logs[i-1]
curr := logs[i]
if curr.Score.Home < prev.Score.Home {
issues = append(issues,
fmt.Sprintf("home score decreased from period %d (%d) to period %d (%d)",
i, prev.Score.Home, i+1, curr.Score.Home))
}
if curr.Score.Away < prev.Score.Away {
issues = append(issues,
fmt.Sprintf("away score decreased from period %d (%d) to period %d (%d)",
i, prev.Score.Away, i+1, curr.Score.Away))
}
}
if len(issues) > 0 {
return true, "score regression: " + strings.Join(issues, ", "), nil
}
return false, "", nil
}
// ValidatePlayerConsistency checks that player rosters remain relatively stable
// across periods. A player present in period 1 should ideally still be present
// in later periods. Drastic roster changes are suspicious.
func ValidatePlayerConsistency(logs []*MatchLog) (bool, string, error) {
if len(logs) != 3 {
return false, "", errors.New("exactly 3 period logs are required")
}
// Build player sets per period
periodPlayers := make([]map[string]string, 3) // game_user_id -> team
for i, log := range logs {
periodPlayers[i] = make(map[string]string)
for _, p := range log.Players {
periodPlayers[i][p.GameUserID] = p.Team
}
}
issues := []string{}
// Check for team-switching between periods (same player, different team)
for i := 1; i < len(logs); i++ {
for id, prevTeam := range periodPlayers[i-1] {
if currTeam, exists := periodPlayers[i][id]; exists {
if currTeam != prevTeam {
issues = append(issues,
fmt.Sprintf("player %s switched teams between period %d and %d",
id, i, i+1))
}
}
}
}
// Check for drastic roster changes (more than 50% different players)
for i := 1; i < len(logs); i++ {
prev := periodPlayers[i-1]
curr := periodPlayers[i]
if len(prev) == 0 {
continue
}
missing := 0
for id := range prev {
if _, exists := curr[id]; !exists {
missing++
}
}
ratio := float64(missing) / float64(len(prev))
if ratio > 0.5 {
issues = append(issues,
fmt.Sprintf("more than 50%% of players changed between period %d and %d (%d/%d missing)",
i, i+1, missing, len(prev)))
}
}
if len(issues) > 0 {
return true, "player inconsistency: " + strings.Join(issues, ", "), nil
}
return false, "", nil
}