diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/bootstrap/tasks/DatasenteretTaskSeeds.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/bootstrap/tasks/DatasenteretTaskSeeds.kt new file mode 100644 index 00000000..5ceb6ce8 --- /dev/null +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/bootstrap/tasks/DatasenteretTaskSeeds.kt @@ -0,0 +1,493 @@ +package edu.ntnu.idi.idatt.backend.bootstrap.tasks + +import edu.ntnu.idi.idatt.backend.game.tasks.domain.TaskType + +internal fun datasenteretTaskSeeds(): List = + listOf( + // Task 1 NEWS_COMPARISON + SeedTask( + difficultyLevel = 1, + taskType = TaskType.NEWS_COMPARISON, + title = + localized( + "Datasenteret 1: Datatyven sprer nyheter", + "Data Center 1: The data thief spreads news", + ), + introText = + localizedOptional( + "Datatyven har publisert to versjoner av en sak om et dataangrep mot skolen. Velg artikkelen som virker mest troverdig.", + "The data thief has published two versions of a story about a school data attack. Select the article that appears most trustworthy.", + ), + items = + listOf( + SeedItem( + itemOrder = 1, + promptText = + localized( + "Hvilken artikkel virker mest troverdig?", + "Which article appears most trustworthy?", + ), + metadataJson = + """ + {"articles":[ + {"optionOrder":1,"domain":"skolenytt-alarm.net","headline":{"nb":"SKOLENS DATABASER HACKET! Alle elevpassord på avveie — myndigheter tier","en":"SCHOOL DATABASES HACKED! All student passwords leaked — authorities silent"},"body":{"nb":"Sjokkerende angrep avslørte i går tusenvis av elevers data. En anonym kilde sier myndighetene skjuler omfanget. Del dette FØR artikkelen slettes — Big Tech vil ikke at du skal vite dette!","en":"A shocking attack yesterday exposed thousands of students' data. An anonymous source says authorities are hiding the scale. Share this BEFORE the article is deleted — Big Tech does not want you to know this!"},"mediaUrl":null}, + {"optionOrder":2,"domain":"udir.no","headline":{"nb":"Veiledning etter datasikkerhetshendelse ved én videregående skole","en":"Guidance following a data security incident at one upper secondary school"},"body":{"nb":"Utdanningsdirektoratet bekrefter at én skole opplevde uautorisert tilgang til et administrativt system. Ingen elevdata ble eksponert. Berørte elever er varslet direkte. Saken er under etterforskning.","en":"The Norwegian Directorate for Education and Training confirms that one school experienced unauthorised access to an administrative system. No student data was exposed. Affected students have been notified directly. The matter is under investigation."},"mediaUrl":null} + ]} + """.trimIndent(), + options = + listOf( + explainedOption( + 1, + "SKOLENS DATABASER HACKET! Alle elevpassord på avveie: myndigheter tier", + "SCHOOL DATABASES HACKED! All student passwords leaked: authorities silent", + false, + "Denne saken bruker store bokstaver, anonyme kilder og «del dette nå»-oppfordringer, alle klassiske tegn på desinformasjon.", + "This story uses capital letters, anonymous sources, and share-now appeals, all classic signs of disinformation.", + ), + explainedOption( + 2, + "Veiledning etter datasikkerhetshendelse ved én videregående skole", + "Guidance following a data security incident at one upper secondary school", + true, + "Riktig! udir.no er et offentlig domene, språket er nøkternt, kilden er navngitt og omfanget er konkret avgrenset.", + "Correct! udir.no is a public domain, the language is measured, the source is named, and the scope is clearly defined.", + ), + ), + ), + ), + ), + // Task 2 IMAGE_ASSESSMENT + imageAssessmentTask( + difficultyLevel = 2, + title = + localized( + "Datasenteret 2: Falskt bevis", + "Data Center 2: Fake evidence", + ), + introText = + localizedOptional( + "Datatyven sender et bilde som bevis. Se etter detaljer som ikke stemmer.", + "The data thief sends an image as evidence. Look for details that do not add up.", + ), + promptText = + localized( + "Hva slags bilde er dette?", + "What kind of image is this?", + ), + mediaUrl = "/assets/stops/datasenteret/task2-ai-dog.png", + correctImageOptionOrder = 1, + reasonOptions = + listOf( + explainedOption( + 1, + "Proporsjonene mellom hunden og vognen ser unaturlige ut, og bakbeinas feste mot selen er umulig.", + "The proportions between the dog and the cart look unnatural, and the attachment of the hind legs to the harness is anatomically impossible.", + true, + "Riktig! KI-genererte bilder feiler ofte på fysikk og anatomi, spesielt der to gjenstander møtes.", + "Correct! AI-generated images often fail on physics and anatomy, especially where two objects meet.", + ), + explainedOption( + 2, + "Bildet er manipulert fordi bakgrunnen ser uskarp ut.", + "The image is manipulated because the background looks blurred.", + false, + "Uskarp bakgrunn er en vanlig fotografiteknikk, det er ikke i seg selv et tegn på manipulering.", + "Blurred backgrounds are a common photography technique, that alone is not a sign of manipulation.", + ), + explainedOption( + 3, + "Bildet er ekte fordi hunden ser naturlig ut og bildekvaliteten er høy.", + "The image is real because the dog looks natural and the image quality is high.", + false, + "Høy bildekvalitet er ikke nok. KI-modeller produserer svært realistiske bilder, detaljene i anatomien og fysikken avslører dem.", + "High image quality is not enough. AI models produce very realistic images, anatomical and physical details are what give them away.", + ), + ), + ), + // Task 3 IMAGE_SELECT + imageSelectTask( + difficultyLevel = 3, + title = + localized( + "Datasenteret 3: Phishing-e-post fra «skolen»", + "Data Center 3: Phishing email from 'the school'", + ), + introText = + localizedOptional( + "Denne e-posten ser nesten ekte ut. Trykk på delene som virker mistenkelige.", + "This email looks almost genuine. Tap the parts that seem suspicious.", + ), + imageUrlNb = "/assets/stops/datasenteret/task3-nb.png", + imageUrlEn = "/assets/stops/datasenteret/task3-en.png", + items = + listOf( + hotspot( + itemOrder = 2, + x = 7.96, + y = 7.80, + width = 82.30, + height = 5.80, + feedbackNb = "Avsenderadressen er itsuppОrt@din-skole.kommune.no men О-en i «itsupport» er ikke latinsk. Det er et kyrillisk tegn (U+041E) som ser identisk ut. Dette kalles et homografangrep og er et av de vanskeligste triksene å oppdage.", + feedbackEn = "The sender address is itsuppОrt@din-skole.kommune.no but the О in 'itsupport' is not Latin. It is a Cyrillic character (U+041E) that looks identical. This is called a homograph attack and is one of the hardest tricks to spot.", + ), + hotspot( + itemOrder = 3, + x = 7.96, + y = 12.40, + width = 82.30, + height = 5.40, + feedbackNb = "«Innen 2 timer» er et klassisk hastverkstriks. Ekte IT-systemer gir deg alltid god tid og presser aldri med korte frister.", + feedbackEn = "'Within 2 hours' is a classic urgency trick. Real IT systems always give you plenty of time and never pressure you with short deadlines.", + ), + hotspot( + itemOrder = 4, + x = 7.96, + y = 62.00, + width = 82.30, + height = 9.50, + feedbackNb = "Lenken peker til skole-login-verify.net som er ikke et kjent norsk domene, og det slutter på .net. Avsenderdomenet og lenkedomenet stemmer ikke overens. Hva gjør du?", + feedbackEn = "The link points to skole-login-verify.net which is not a known Norwegian domain, and it ends in .net. The sender domain and link domain do not match. What do you do?", + hasActionQuestion = true, + options = + listOf( + explainedOption( + 1, + "Ikke klikke. Kontakte IT-avdelingen direkte for å sjekke om e-posten er ekte.", + "Do not click. Contact the IT department directly to check whether the email is genuine.", + true, + "Riktig! Selv om du ikke oppdager det kyrilliske tegnet, er den mistenkelige lenken mer enn nok grunn til å stoppe opp.", + "Correct! Even if you do not spot the Cyrillic character, the suspicious link is more than enough reason to stop.", + ), + explainedOption( + 2, + "Klikke på lenken — e-posten ser jo ut som den er fra skolen.", + "Click the link — the email looks like it is from the school.", + false, + "Det er akkurat det angriperen håper. Det kyrilliske tegnet og den falske lenken avslører at dette er et svært sofistikert angrep.", + "That is exactly what the attacker is hoping for. The Cyrillic character and the fake link reveal this is a highly sophisticated attack.", + ), + explainedOption( + 3, + "Svare på e-posten og spørre om den er ekte.", + "Reply to the email and ask whether it is genuine.", + false, + "Du svarer direkte til angriperen. Ta alltid kontakt via en kjent kanal, ikke ved å svare på den mistenkelige e-posten.", + "You would be replying directly to the attacker. Always make contact through a known channel, not by replying to the suspicious email.", + ), + ), + ), + ), + ), + // Task 4 IMAGE_SELECT + imageSelectTask( + difficultyLevel = 4, + title = + localized( + "Datasenteret 4: Falsk betalingsside", + "Data Center 4: Fake payment page", + ), + introText = + localizedOptional( + "Datatyven har laget en falsk betalingsside for klasseturen. Trykk på delene som avslører at siden ikke er ekte.", + "The data thief has created a fake payment page for the class trip. Tap the parts that reveal the page is not genuine.", + ), + imageUrlNb = "/assets/stops/datasenteret/task4-nb.png", + imageUrlEn = "/assets/stops/datasenteret/task4-en.png", + items = + listOf( + hotspot( + itemOrder = 2, + x = 7.50, + y = 1.20, + width = 85.00, + height = 5.20, + feedbackNb = "Adressen starter med http://, ikke https://. Ekte betalingssider bruker alltid https for å kryptere informasjonen din. Domenet betaling-skoletur.com er heller ikke et kjent norsk domene.", + feedbackEn = "The address starts with http://, not https://. Real payment pages always use https to encrypt your information. The domain betaling-skoletur.com is also not a known Norwegian domain.", + ), + hotspot( + itemOrder = 3, + x = 26.00, + y = 24.00, + width = 20.00, + height = 15.00, + feedbackNb = "Kun direkte overføring til et kontonummer, ingen kortbetaling eller faktura. Pengene er nesten umulige å få tilbake hvis noe går galt.", + feedbackEn = "Only direct transfer to an account number, no card payment or invoice option. Money transferred this way is nearly impossible to recover if something goes wrong.", + ), + hotspot( + itemOrder = 4, + x = 7.50, + y = 42.00, + width = 85.00, + height = 8.00, + feedbackNb = "«Veldig viktig ellers får du ikke bli med» er et presstriks, akkurat som hastverksfrister i phishing-e-poster. Ekte skoleinformasjon fra lærere bruker rolig og saklig språk.", + feedbackEn = "'Very important or you cannot come' is a pressure tactic, just like urgency deadlines in phishing emails. Genuine school information from teachers uses calm and factual language.", + ), + hotspot( + itemOrder = 5, + x = 7.50, + y = 75.00, + width = 85.00, + height = 5.50, + feedbackNb = "«100% trygg betaling, ingen kvittering nødvendig» er et sterkt faresignal. Ekte betalinger gir alltid kvittering som dokumentasjon. Hva gjør du?", + feedbackEn = "'100% safe payment, no receipt necessary' is a strong warning sign. Genuine payments always provide a receipt as documentation. What do you do?", + hasActionQuestion = true, + options = + listOf( + explainedOption( + 1, + "Ikke betale. Kontakte kontaktlæreren direkte for å sjekke om siden er ekte.", + "Do not pay. Contact the class teacher directly to check whether the page is genuine.", + true, + "Riktig! Ukjent domene, kun bankoverføring, pressspråk og ingen kvittering er fire grunner til å stoppe opp og dobbeltsjekke.", + "Correct! Unknown domain, bank transfer only, pressure language, and no receipt are four reasons to stop and double-check.", + ), + explainedOption( + 2, + "Betale raskt — det står jo at det er veldig viktig.", + "Pay quickly — it says it is very important.", + false, + "Det er akkurat hva angriperen vil at du skal tenke. Hastverk er et verktøy for å hindre deg i å tenke deg om.", + "That is exactly what the attacker wants you to think. Urgency is a tool to stop you from thinking it through.", + ), + explainedOption( + 3, + "Betale, men ta skjermbilde som dokumentasjon.", + "Pay, but take a screenshot as documentation.", + false, + "Et skjermbilde dokumenterer at du ble svindlet, det gir deg ikke pengene tilbake. Stopp opp før du betaler.", + "A screenshot documents that you were scammed, it does not get your money back. Stop before you pay.", + ), + ), + ), + ), + ), + // Task 5 PASSWORD_EVALUATION + binaryChoiceTask( + difficultyLevel = 5, + taskType = TaskType.PASSWORD_EVALUATION, + title = + localized( + "Datasenteret 5: Datatyven gjetter passordet ditt", + "Data Center 5: The data thief guesses your password", + ), + introText = + localizedOptional( + "Datatyven vet brukernavnet ditt er «ada2015» og prøver «Ada2015!» først. Hvorfor er dette et problem?", + "The data thief knows your username is 'ada2015' and tries 'Ada2015!' first. Why is this a problem?", + ), + promptText = + localized( + "Hvorfor er «Ada2015!» et dårlig passord?", + "Why is 'Ada2015!' a bad password?", + ), + correctOption = + explainedOption( + 1, + "Fordi det er basert på personlig informasjon som finnes offentlig: brukernavn, navn og fødselsår.", + "Because it is based on personal information that is publicly available: username, name, and birth year.", + true, + "Riktig! Passord basert på brukernavn, navn eller årstall er de første tingene angripere prøver.", + "Correct! Passwords based on usernames, names, or years are the first things attackers try.", + ), + incorrectOption = + explainedOption( + 2, + "Fordi passordet er for langt og vanskelig å huske.", + "Because the password is too long and hard to remember.", + false, + "Lengde er ikke problemet her, det er at passordet bruker forutsigbar personlig informasjon.", + "Length is not the problem here, it is that the password uses predictable personal information.", + ), + ), + // Task 6 PASSWORD_EVALUATION + binaryChoiceTask( + difficultyLevel = 6, + taskType = TaskType.PASSWORD_EVALUATION, + title = + localized( + "Datasenteret 6: Samme passord overalt", + "Data Center 6: Same password everywhere", + ), + introText = + localizedOptional( + "Du har brukt samme passord på skolesystemet og et spillnettsted som ble hacket. Hva er konsekvensen?", + "You used the same password on the school system and a gaming site that was hacked. What is the consequence?", + ), + promptText = + localized( + "Hva kan datatyven gjøre med det lekkede passordet?", + "What can the data thief do with the leaked password?", + ), + correctOption = + explainedOption( + 1, + "Bruke det lekkede passordet fra spillet til å logge inn på skolekontoen din.", + "Use the leaked password from the game to log into your school account.", + true, + "Riktig! Dette kalles credential stuffing: hackere prøver automatisk lekkede passord på mange tjenester.", + "Correct! This is called credential stuffing: hackers automatically try leaked passwords across many services.", + ), + incorrectOption = + explainedOption( + 2, + "Ingenting, skolesystemet og spillnettstedet er separate og påvirker ikke hverandre.", + "Nothing, the school system and the gaming site are separate and do not affect each other.", + false, + "Feil. Gjenbruk av passord betyr at ett lekket passord kan åpne mange kontoer på tvers av tjenester.", + "Wrong. Reusing passwords means one leaked password can unlock many accounts across services.", + ), + ), + // Task 7 PASSWORD_EVALUATION + binaryChoiceTask( + difficultyLevel = 7, + taskType = TaskType.PASSWORD_EVALUATION, + title = + localized( + "Datasenteret 7: Stopp datatyven", + "Data Center 7: Stop the data thief", + ), + introText = + localizedOptional( + "Datatyven har ingen personlig informasjon om deg lenger. Hvilket passord er vanskeligst å knekke?", + "The data thief no longer has any personal information about you. Which password is hardest to crack?", + ), + promptText = + localized( + "Hvilket passord er sterkest?", + "Which password is the strongest?", + ), + correctOption = + explainedOption( + 1, + "Blåbær-Togstasjon9-Paraply", + "Blueberry-TrainStation9-Umbrella", + true, + "Riktig! En tilfeldig passordfrase er lang, lett å huske og nesten umulig å gjette selv uten personlig informasjon.", + "Correct! A random passphrase is long, easy to remember, and nearly impossible to guess even without personal information.", + ), + incorrectOption = + explainedOption( + 2, + "Tr0ndhe1m!", + "Tr0ndhe1m!", + false, + "Bokstavbytter som o→0 og i→1 er godt kjent for angripere og gir lite ekstra sikkerhet. Lengde og tilfeldighet er viktigere.", + "Letter substitutions like o→0 and i→1 are well known to attackers and add little extra security. Length and randomness matter more.", + ), + ), + ) + +private fun imageAssessmentTask( + difficultyLevel: Int, + title: LocalizedText, + introText: LocalizedOptionalText?, + promptText: LocalizedText, + mediaUrl: String, + correctImageOptionOrder: Int, + reasonOptions: List, +): SeedTask = + SeedTask( + difficultyLevel = difficultyLevel, + taskType = TaskType.IMAGE_ASSESSMENT, + title = title, + introText = introText, + items = + listOf( + SeedItem( + itemOrder = 1, + promptText = promptText, + mediaUrl = mediaUrl, + metadataJson = """{"role":"imageChoice"}""", + options = imageTypeOptions(correctOptionOrder = correctImageOptionOrder), + ), + SeedItem( + itemOrder = 2, + promptText = + localized( + "Hvorfor er dette den beste vurderingen?", + "Why is this the best assessment?", + ), + metadataJson = """{"role":"reasonChoice"}""", + options = reasonOptions, + ), + ), + ) + +private fun imageTypeOptions(correctOptionOrder: Int): List = + listOf( + explainedOption( + 1, + "KI", + "AI", + correctOptionOrder == 1, + "Ekstra kroppsdeler, merkelige skygger og feil i detaljer tyder på at bildet er KI-generert.", + "Extra body parts, strange shadows, and detail errors suggest the image was AI-generated.", + ), + explainedOption( + 2, + "Manipulert", + "Manipulated", + correctOptionOrder == 2, + "Ujevne kanter eller endrede deler kan tyde på at bildet er manipulert.", + "Uneven edges or altered parts can suggest the image was manipulated.", + ), + explainedOption( + 3, + "Ekte", + "Real", + correctOptionOrder == 3, + "Et ekte bilde bør ha naturlig sammenheng mellom lys, skygger, tekst og detaljer.", + "A real image should have natural consistency between light, shadows, text, and details.", + ), + ) + +private fun imageSelectTask( + difficultyLevel: Int, + title: LocalizedText, + introText: LocalizedOptionalText?, + imageUrlNb: String, + imageUrlEn: String, + items: List, +): SeedTask = + SeedTask( + difficultyLevel = difficultyLevel, + taskType = TaskType.IMAGE_SELECT, + title = title, + introText = introText, + items = + listOf( + SeedItem( + itemOrder = 1, + promptText = localized("", ""), + metadataJson = """{"mediaUrl":{"nb":"$imageUrlNb","en":"$imageUrlEn"}}""", + options = emptyList(), + ), + ) + items, + ) + +private fun hotspot( + itemOrder: Int, + x: Double, + y: Double, + width: Double, + height: Double, + feedbackNb: String, + feedbackEn: String, + isTrustworthy: Boolean = false, + hasActionQuestion: Boolean = false, + options: List = emptyList(), +): SeedItem { + val meta = + buildString { + append("""{"x":$x,"y":$y,"width":$width,"height":$height,"isHotspot":true""") + if (isTrustworthy) append(""","isTrustworthy":true""") + if (hasActionQuestion) append(""","hasActionQuestion":true""") + append("}") + } + return SeedItem( + itemOrder = itemOrder, + promptText = localized(feedbackNb, feedbackEn), + metadataJson = meta, + options = options, + ) +} diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/bootstrap/tasks/TaskBootstrap.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/bootstrap/tasks/TaskBootstrap.kt index c2729082..0e2a6f87 100644 --- a/src/main/kotlin/edu/ntnu/idi/idatt/backend/bootstrap/tasks/TaskBootstrap.kt +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/bootstrap/tasks/TaskBootstrap.kt @@ -66,18 +66,17 @@ class TaskBootstrap( return stops } - private fun taskSeedsForStop(stop: MapStop): List { - if (stop.isFinalBoss == true) return listOf(finalBossTaskSeed()) - return when (requireNotNull(stop.slug) { "Seeded map stop must have a slug" }) { + private fun taskSeedsForStop(stop: MapStop): List = + when (requireNotNull(stop.slug) { "Seeded map stop must have a slug" }) { "nyhetskvartalet" -> nyhetskvartaleTaskSeeds() "fotografen" -> fotografenTaskSeeds() "postkontoret" -> postkontoretTaskSeeds() "markedsplassen" -> markedsplassenTaskSeeds() "den-sosiale-markedsplassen" -> denSosialeMarkedsplassenTaskSeeds() "passordbanken" -> passordbankenTaskSeeds() + "datasenteret" -> datasenteretTaskSeeds() else -> emptyList() } - } private fun applyTaskSeed( task: Task, diff --git a/src/main/resources/static/assets/stops/datasenteret/task2-ai-dog.png b/src/main/resources/static/assets/stops/datasenteret/task2-ai-dog.png new file mode 100644 index 00000000..d9f3bf5c Binary files /dev/null and b/src/main/resources/static/assets/stops/datasenteret/task2-ai-dog.png differ diff --git a/src/main/resources/static/assets/stops/datasenteret/task3-en.png b/src/main/resources/static/assets/stops/datasenteret/task3-en.png new file mode 100644 index 00000000..6f01a011 Binary files /dev/null and b/src/main/resources/static/assets/stops/datasenteret/task3-en.png differ diff --git a/src/main/resources/static/assets/stops/datasenteret/task3-nb.png b/src/main/resources/static/assets/stops/datasenteret/task3-nb.png new file mode 100644 index 00000000..d74f471e Binary files /dev/null and b/src/main/resources/static/assets/stops/datasenteret/task3-nb.png differ diff --git a/src/main/resources/static/assets/stops/datasenteret/task4-en.png b/src/main/resources/static/assets/stops/datasenteret/task4-en.png new file mode 100644 index 00000000..4b4dd715 Binary files /dev/null and b/src/main/resources/static/assets/stops/datasenteret/task4-en.png differ diff --git a/src/main/resources/static/assets/stops/datasenteret/task4-nb.png b/src/main/resources/static/assets/stops/datasenteret/task4-nb.png new file mode 100644 index 00000000..b701da45 Binary files /dev/null and b/src/main/resources/static/assets/stops/datasenteret/task4-nb.png differ