package api import ( "encoding/json" "fmt" "io" "log" "net/http" "os" "path" "reflect" "strconv" "time" "srs.epita.fr/fic-server/admin/generation" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/settings" "github.com/gin-gonic/gin" ) var IsProductionEnv = false func declareSettingsRoutes(router *gin.RouterGroup) { router.GET("/challenge.json", getChallengeInfo) router.PUT("/challenge.json", saveChallengeInfo) router.GET("/settings-ro.json", getROSettings) router.GET("/settings.json", getSettings) router.PUT("/settings.json", saveSettings) router.DELETE("/settings.json", func(c *gin.Context) { err := ResetSettings() if err != nil { log.Println("Unable to ResetSettings:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during setting reset."}) return } c.JSON(http.StatusOK, true) }) router.GET("/settings-next", listNextSettings) apiNextSettingsRoutes := router.Group("/settings-next/:ts") apiNextSettingsRoutes.Use(NextSettingsHandler) apiNextSettingsRoutes.GET("", getNextSettings) apiNextSettingsRoutes.DELETE("", deleteNextSettings) router.POST("/reset", reset) router.POST("/full-generation", fullGeneration) router.GET("/prod", func(c *gin.Context) { c.JSON(http.StatusOK, IsProductionEnv) }) router.PUT("/prod", func(c *gin.Context) { err := c.ShouldBindJSON(&IsProductionEnv) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } c.JSON(http.StatusOK, IsProductionEnv) }) } func NextSettingsHandler(c *gin.Context) { ts, err := strconv.ParseInt(string(c.Params.ByName("ts")), 10, 64) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid next settings identifier"}) return } nsf, err := settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", ts)), ts) if err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Next settings not found"}) return } c.Set("next-settings", nsf) c.Next() } func fullGeneration(c *gin.Context) { resp, err := generation.FullGeneration() if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ "errmsg": err.Error(), }) return } defer resp.Body.Close() v, _ := io.ReadAll(resp.Body) c.JSON(resp.StatusCode, gin.H{ "errmsg": string(v), }) } func getROSettings(c *gin.Context) { syncMtd := "Disabled" if sync.GlobalImporter != nil { syncMtd = sync.GlobalImporter.Kind() } var syncId *string if sync.GlobalImporter != nil { syncId = sync.GlobalImporter.Id() } c.JSON(http.StatusOK, gin.H{ "sync-type": reflect.TypeOf(sync.GlobalImporter).Name(), "sync-id": syncId, "sync": syncMtd, }) } func GetChallengeInfo() (*settings.ChallengeInfo, error) { var challengeinfo string var err error if sync.GlobalImporter == nil { if fd, err := os.Open(path.Join(settings.SettingsDir, settings.ChallengeFile)); err == nil { defer fd.Close() var buf []byte buf, err = io.ReadAll(fd) if err == nil { challengeinfo = string(buf) } } } else { challengeinfo, err = sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile) } if err != nil { log.Println("Unable to retrieve challenge.json:", err.Error()) return nil, fmt.Errorf("Unable to retrive challenge.json: %w", err) } s, err := settings.ReadChallengeInfo(challengeinfo) if err != nil { log.Println("Unable to ReadChallengeInfo:", err.Error()) return nil, fmt.Errorf("Unable to read challenge info: %w", err) } return s, nil } func getChallengeInfo(c *gin.Context) { if s, err := GetChallengeInfo(); err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) } else { c.JSON(http.StatusOK, s) } } func saveChallengeInfo(c *gin.Context) { var info *settings.ChallengeInfo err := c.ShouldBindJSON(&info) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } if sync.GlobalImporter != nil { jenc, err := json.Marshal(info) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } err = sync.WriteFileContent(sync.GlobalImporter, "challenge.json", jenc) if err != nil { log.Println("Unable to SaveChallengeInfo:", err.Error()) // Ignore the error, try to continue } err = sync.ImportChallengeInfo(info, DashboardDir) if err != nil { log.Println("Unable to ImportChallengeInfo:", err.Error()) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Sprintf("Something goes wrong when trying to import related files: %s", err.Error())}) return } } if err := settings.SaveChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile), info); err != nil { log.Println("Unable to SaveChallengeInfo:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save distributed challenge info: %s", err.Error())}) return } c.JSON(http.StatusOK, info) } func getSettings(c *gin.Context) { s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)) if err != nil { log.Println("Unable to ReadSettings:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to read settings: %s", err.Error())}) return } s.WorkInProgress = !IsProductionEnv c.Writer.Header().Add("X-FIC-Time", fmt.Sprintf("%d", time.Now().Unix())) c.JSON(http.StatusOK, s) } func saveSettings(c *gin.Context) { var config *settings.Settings err := c.ShouldBindJSON(&config) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } // Is this a future setting? if c.Request.URL.Query().Has("t") { t, err := time.Parse(time.RFC3339, c.Request.URL.Query().Get("t")) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } // Load current settings to perform diff later init_settings, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)) if err != nil { log.Println("Unable to ReadSettings:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to read settings: %s", err.Error())}) return } current_settings := init_settings // Apply already registered settings nsu, err := settings.MergeNextSettingsUntil(&t) if err == nil { current_settings = settings.MergeSettings(*init_settings, nsu) } else { log.Println("Unable to MergeNextSettingsUntil:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to merge next settings: %s", err.Error())}) return } // Keep only diff diff := settings.DiffSettings(current_settings, config) hasItems := false for _, _ = range diff { hasItems = true break } if !hasItems { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "No difference to apply."}) return } if !c.Request.URL.Query().Has("erase") { // Check if there is already diff to apply at the given time if nsf, err := settings.ReadNextSettingsFile(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", t.Unix())), t.Unix()); err == nil { for k, v := range nsf.Values { if _, ok := diff[k]; !ok { diff[k] = v } } } } // Save the diff settings.SaveSettings(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", t.Unix())), diff) // Return current settings c.JSON(http.StatusOK, current_settings) } else { // Just apply settings right now! if err := settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), config); err != nil { log.Println("Unable to SaveSettings:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to save settings: %s", err.Error())}) return } ApplySettings(config) c.JSON(http.StatusOK, config) } } func listNextSettings(c *gin.Context) { nsf, err := settings.ListNextSettingsFiles() if err != nil { log.Println("Unable to ListNextSettingsFiles:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list next settings files: %s", err.Error())}) return } c.JSON(http.StatusOK, nsf) } func getNextSettings(c *gin.Context) { c.JSON(http.StatusOK, c.MustGet("next-settings").(*settings.NextSettingsFile)) } func deleteNextSettings(c *gin.Context) { nsf := c.MustGet("next-settings").(*settings.NextSettingsFile) err := os.Remove(path.Join(settings.SettingsDir, fmt.Sprintf("%d.json", nsf.Id))) if err != nil { log.Println("Unable to remove the file:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to remove the file: %s", err.Error())}) return } c.JSON(http.StatusOK, true) } func ApplySettings(config *settings.Settings) { fic.PartialValidation = config.PartialValidation fic.UnlockedChallengeDepth = config.UnlockedChallengeDepth fic.UnlockedChallengeUpTo = config.UnlockedChallengeUpTo fic.DisplayAllFlags = config.DisplayAllFlags fic.HideCaseSensitivity = config.HideCaseSensitivity fic.UnlockedStandaloneExercices = config.UnlockedStandaloneExercices fic.UnlockedStandaloneExercicesByThemeStepValidation = config.UnlockedStandaloneExercicesByThemeStepValidation fic.UnlockedStandaloneExercicesByStandaloneExerciceValidation = config.UnlockedStandaloneExercicesByStandaloneExerciceValidation fic.DisplayMCQBadCount = config.DisplayMCQBadCount fic.FirstBlood = config.FirstBlood fic.SubmissionCostBase = config.SubmissionCostBase fic.HintCoefficient = config.HintCurCoefficient fic.WChoiceCoefficient = config.WChoiceCurCoefficient fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient fic.SubmissionCostBase = config.SubmissionCostBase fic.SubmissionUniqueness = config.SubmissionUniqueness fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries if config.DiscountedFactor != fic.DiscountedFactor { fic.DiscountedFactor = config.DiscountedFactor if err := fic.DBRecreateDiscountedView(); err != nil { log.Println("Unable to recreate exercices_discounted view:", err.Error()) } } } func ResetSettings() error { return settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), &settings.Settings{ WorkInProgress: IsProductionEnv, FirstBlood: fic.FirstBlood, SubmissionCostBase: fic.SubmissionCostBase, ExerciceCurCoefficient: 1, HintCurCoefficient: 1, WChoiceCurCoefficient: 1, GlobalScoreCoefficient: 1, DiscountedFactor: 0, UnlockedStandaloneExercices: 10, UnlockedStandaloneExercicesByThemeStepValidation: 1, UnlockedStandaloneExercicesByStandaloneExerciceValidation: 0, AllowRegistration: false, CanJoinTeam: false, DenyTeamCreation: false, DenyNameChange: false, AcceptNewIssue: true, QAenabled: false, EnableResolutionRoute: false, PartialValidation: true, UnlockedChallengeDepth: 0, SubmissionUniqueness: true, CountOnlyNotGoodTries: true, DisplayAllFlags: false, DisplayMCQBadCount: false, EventKindness: false, }) } func ResetChallengeInfo() error { return settings.SaveChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile), &settings.ChallengeInfo{ Title: "Challenge forensic", SubTitle: "sous le patronage du commandement de la cyberdéfense", Authors: "Laboratoire SRS, ÉPITA", VideosLink: "", Description: `
Le challenge forensic vous place dans la peau de spécialistes en investigation numérique. Nous mettons à votre disposition une vingtaine de scénarios différents, dans lesquels vous devrez faire les différentes étapes de la caractérisation d’une réponse à incident proposées.
Chaque scénario met en scène un contexte d’entreprise, ayant découvert récemment qu’elle a été victime d’une cyberattaque. Elle vous demande alors de l’aider à caractériser, afin de mieux comprendre la situation, notamment le mode opératoire de l’adversaire, les impacts de la cyberattaque, le périmètre technique compromis, etc. Il faudra parfois aussi l’éclairer sur les premières étapes de la réaction.
`, Rules: `Pendant toute la durée du challenge, vous aurez accès à tous les scénarios, mais seulement à la première des 5 étapes. Chaque étape supplémentaire est débloquée lorsque vous validez l’intégralité de l’étape précédente. Toutefois, pour dynamiser le challenge toutes les étapes et tous les scénarios seront débloquées pour la dernière heure du challenge.
Nous mettons à votre disposition une plateforme sur laquelle vous pourrez obtenir les informations sur le contexte de l’entreprise et, généralement, une série de fichiers qui semblent appropriés pour avancer dans l’investigation.
La validation d’une étape se fait sur la plateforme, après avoir analysé les informations fournies, en répondant à des questions plus ou moins précises. Il s’agit le plus souvent des mots-clefs que l’on placerait dans un rapport.
Pour vous débloquer ou accélérer votre investigation, vous pouvez accéder à quelques indices, en échange d’une décote sur votre score d’un certain nombre de points préalablement affichés.
Chaque équipe dispose d’un compteur de points dans l’intervalle ]-∞;+∞[ (aux détails techniques près), à partir duquel le classement est établi.
Vous perdez des points en dévoilant des indices, en demandant des propositions de réponses en remplacement de certains champs de texte, ou en essayant un trop grand nombre de fois une réponse.
Le nombre de points que vous fait perdre un indice dépend habituellement de l’aide qu’il vous apportera et est indiqué avant de le dévoiler, car il peut fluctuer en fonction de l’avancement du challenge.
Pour chaque champ de texte, vous disposez de 10 tentatives avant de perdre des points (vous perdez les points même si vous ne validez pas l’étape) pour chaque tentative supplémentaire : -0,25 point entre 11 et 20, -0,5 entre 21 et 30, -0,75 entre 31 et 40, …
La seule manière de gagner des points est de valider une étape d’un scénario dans son intégralité. Le nombre de points gagnés dépend de la difficulté théorique de l’étape ainsi que d’éventuels bonus. Un bonus de 10 % est accordé à la première équipe qui valide une étape. D’autres bonus peuvent ponctuer le challenge, détaillé dans la partie suivante.
Le classement est établi par équipe, selon le nombre de points récoltés et perdus par tous les membres. En cas d’égalité au score, les équipes sont départagées en fonction de leur ordre d’arrivée à ce score.
Le challenge forensic est jalonné de plusieurs temps forts durant lesquels certains calculs détaillés dans la partie précédente peuvent être altérés. L’équipe d’animation du challenge vous avertira environ 15 minutes avant le début de la modification.
Chaque modification se répercute instantanément dans votre interface, attendez simplement qu’elle apparaisse afin d’être certain d’en bénéficier. Un compte à rebours est généralement affiché sur les écrans pour indiquer la fin d’un temps fort. La fin d’application d’un bonus est déterminé par l’heure d’arrivée de votre demande sur nos serveurs.
Sans y être limité ou assuré, sachez que durant les précédentes éditions du challenge forensic, nous avons par exemple : doublé les points de défis peu tentés, doublé les points de tous les défis pendant 30 minutes, réduit le coût des indices pendant 15 minutes, etc.
Tous les étudiants de la majeure Système, Réseaux et Sécurité de l’ÉPITA, son équipe enseignante ainsi que le commandement de la cyberdéfense vous souhaitent bon courage pour cette nouvelle éditions du challenge !
`, YourMission: `Vous voici aujourd'hui dans la peau de spécialistes en investigation numérique. Vous avez à votre disposition une vingtaine de scénarios différents dans lesquels vous devrez faire les différentes étapes de la caractérisation d’une réponse à incident.
Chaque scénario est découpé en 5 grandes étapes de difficulté croissante. Un certain nombre de points est attribué à chaque étape, avec un processus de validation automatique.
Un classement est établi en temps réel, tenant compte des différents bonus, en fonction du nombre de points de chaque équipe.
`, }) } func reset(c *gin.Context) { var m map[string]string err := c.ShouldBindJSON(&m) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } t, ok := m["type"] if !ok { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Field type not found"}) } switch t { case "teams": err = fic.ResetTeams() case "challenges": err = fic.ResetExercices() case "game": err = fic.ResetGame() case "annexes": err = fic.ResetAnnexes() case "settings": err = ResetSettings() case "challengeInfo": err = ResetChallengeInfo() default: c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Unknown reset type"}) return } if err != nil { log.Printf("Unable to reset (type=%q): %s", t, err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to performe the reset: %s", err.Error())}) return } c.JSON(http.StatusOK, true) }