diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6ba20ee1c..0010a8596 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -69,7 +69,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" - name: run mvn clean package run: ./mvnw clean package -Ddependency-check.skip=true -Dmaven.test.skip=true - name: Perform CodeQL Analysis diff --git a/.github/workflows/container_test.yml b/.github/workflows/container_test.yml index c2cd20e93..69b4dca6a 100644 --- a/.github/workflows/container_test.yml +++ b/.github/workflows/container_test.yml @@ -23,7 +23,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: Navigate to test script and run run: cd .github/scripts && bash docker-create.sh -t diff --git a/.github/workflows/dast-zap-test.yml b/.github/workflows/dast-zap-test.yml index 25294ff14..10e5021cb 100644 --- a/.github/workflows/dast-zap-test.yml +++ b/.github/workflows/dast-zap-test.yml @@ -18,7 +18,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" - name: Clean install run: ./mvnw --no-transfer-progress clean install -DskipTests -Ddependency-check.skip -Dcyclonedx.skip=true -Dexec.skip - name: Start wrongsecrets diff --git a/.github/workflows/github-pages-preview.yml b/.github/workflows/github-pages-preview.yml index 64133c431..43a438139 100644 --- a/.github/workflows/github-pages-preview.yml +++ b/.github/workflows/github-pages-preview.yml @@ -37,7 +37,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: Build application (JAR only) diff --git a/.github/workflows/java_swagger_doc.yml b/.github/workflows/java_swagger_doc.yml index 72161f6b2..96f9e8542 100644 --- a/.github/workflows/java_swagger_doc.yml +++ b/.github/workflows/java_swagger_doc.yml @@ -18,7 +18,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" - name: Clean install run: ./mvnw --no-transfer-progress clean install -DskipTests -Ddependency-check.skip -Dcyclonedx.skip=true -Dexec.skip - name: Compile javadoc diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 91f09caf5..d51ab547e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: checkstyle with Maven run: ./mvnw --no-transfer-progress checkstyle:check @@ -43,7 +43,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: spotbugs with Maven run: ./mvnw --no-transfer-progress package -DskipTests spotbugs:check @@ -59,7 +59,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: Test with Maven run: ./mvnw --no-transfer-progress test diff --git a/.github/workflows/master-container-publish.yml b/.github/workflows/master-container-publish.yml index 4267e7798..ed6d3bc68 100644 --- a/.github/workflows/master-container-publish.yml +++ b/.github/workflows/master-container-publish.yml @@ -21,7 +21,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: Extract version from pom.xml diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index aba2629ad..b9e4bfa92 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -29,7 +29,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: Extract version from pom.xml @@ -249,7 +249,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: Extract PR version @@ -278,7 +278,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: Extract main version diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index ac703f48e..f5aa8936f 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -31,7 +31,7 @@ jobs: cache: "npm" - uses: actions/setup-java@v5 with: - distribution: "oracle" + distribution: "temurin" java-version: "23" - name: Install npm dependencies run: npm install diff --git a/.github/workflows/version-sync-check.yml b/.github/workflows/version-sync-check.yml index aa8e140aa..6c1f70e65 100644 --- a/.github/workflows/version-sync-check.yml +++ b/.github/workflows/version-sync-check.yml @@ -20,7 +20,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: Validate version consistency diff --git a/.github/workflows/visual-diff.yml b/.github/workflows/visual-diff.yml index a5c8b1d6d..a2be05f82 100644 --- a/.github/workflows/visual-diff.yml +++ b/.github/workflows/visual-diff.yml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: Extract PR version @@ -53,7 +53,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: "23" - distribution: "oracle" + distribution: "temurin" cache: "maven" - name: Extract main version diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/ChallengesController.java b/src/main/java/org/owasp/wrongsecrets/challenges/ChallengesController.java index 7516c2653..e3bd7f49e 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/ChallengesController.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/ChallengesController.java @@ -5,6 +5,7 @@ import com.google.common.base.Strings; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -203,7 +204,10 @@ public String reset( @PostMapping(value = "/challenge/{name}", params = "action=submit") @Operation(description = "Post your answer to the challenge for a given challenge") public String postController( - @ModelAttribute ChallengeForm challengeForm, Model model, @PathVariable String name) { + @ModelAttribute ChallengeForm challengeForm, + Model model, + @PathVariable String name, + HttpServletRequest request) { var challengeDefinition = findByShortName(name); if (!isChallengeEnabled(challengeDefinition)) { @@ -223,8 +227,9 @@ public String postController( scoreCard.completeChallenge(challengeDefinition); // Send Slack notification for challenge completion if (slackNotificationService != null) { + String userAgent = request.getHeader("User-Agent"); slackNotificationService.notifyChallengeCompletion( - challengeDefinition.name().shortName(), null); + challengeDefinition.name().shortName(), null, userAgent); } // TODO extract this to a separate method probably have separate handler classes in the // configuration otherwise this is not maintainable, probably give the challenge a CTF diff --git a/src/main/java/org/owasp/wrongsecrets/challenges/docker/SlackNotificationService.java b/src/main/java/org/owasp/wrongsecrets/challenges/docker/SlackNotificationService.java index 333e5db09..024166fbe 100644 --- a/src/main/java/org/owasp/wrongsecrets/challenges/docker/SlackNotificationService.java +++ b/src/main/java/org/owasp/wrongsecrets/challenges/docker/SlackNotificationService.java @@ -36,15 +36,16 @@ public SlackNotificationService( * * @param challengeName The name of the completed challenge * @param userName Optional username of the person who completed the challenge + * @param userAgent Optional user agent string from the HTTP request */ - public void notifyChallengeCompletion(String challengeName, String userName) { + public void notifyChallengeCompletion(String challengeName, String userName, String userAgent) { if (!isSlackConfigured()) { logger.debug("Slack not configured, skipping notification for challenge: {}", challengeName); return; } try { - String message = buildCompletionMessage(challengeName, userName); + String message = buildCompletionMessage(challengeName, userName, userAgent); SlackMessage slackMessage = new SlackMessage(message); HttpHeaders headers = new HttpHeaders(); @@ -62,6 +63,16 @@ public void notifyChallengeCompletion(String challengeName, String userName) { } } + /** + * Sends a Slack notification when a challenge is completed (backward compatibility method). + * + * @param challengeName The name of the completed challenge + * @param userName Optional username of the person who completed the challenge + */ + public void notifyChallengeCompletion(String challengeName, String userName) { + notifyChallengeCompletion(challengeName, userName, null); + } + private boolean isSlackConfigured() { return challenge59.isPresent() && challenge59.get().getSlackWebhookUrl() != null @@ -70,12 +81,16 @@ private boolean isSlackConfigured() { && challenge59.get().getSlackWebhookUrl().startsWith("https://hooks.slack.com"); } - private String buildCompletionMessage(String challengeName, String userName) { + private String buildCompletionMessage(String challengeName, String userName, String userAgent) { String userPart = (userName != null && !userName.trim().isEmpty()) ? " by " + userName : ""; + String userAgentPart = + (userAgent != null && !userAgent.trim().isEmpty()) + ? " (User-Agent: " + userAgent + ")" + : ""; return String.format( - "🎉 Challenge %s completed%s! Another secret vulnerability discovered in WrongSecrets.", - challengeName, userPart); + "🎉 Challenge %s completed%s%s! Another secret vulnerability discovered in WrongSecrets.", + challengeName, userPart, userAgentPart); } /** Simple record for Slack message payload. */ diff --git a/src/test/e2e/cypress.config.arcane.js b/src/test/e2e/cypress.config.arcane.js index dd13852c9..4a9042768 100644 --- a/src/test/e2e/cypress.config.arcane.js +++ b/src/test/e2e/cypress.config.arcane.js @@ -10,6 +10,7 @@ module.exports = defineConfig({ reporterOptions: { configFile: 'reporter-config.json' }, + userAgent: 'Cypress WrongSecrets E2E Tests (Arcane)', setupNodeEvents (on, config) { // implement node event listeners here } diff --git a/src/test/e2e/cypress.config.heroku.js b/src/test/e2e/cypress.config.heroku.js index a6793c330..2ddceeb5c 100644 --- a/src/test/e2e/cypress.config.heroku.js +++ b/src/test/e2e/cypress.config.heroku.js @@ -10,6 +10,7 @@ module.exports = defineConfig({ reporterOptions: { configFile: 'reporter-config.json' }, + userAgent: 'Cypress WrongSecrets E2E Tests (Heroku)', setupNodeEvents (on, config) { // implement node event listeners here } diff --git a/src/test/e2e/cypress.config.js b/src/test/e2e/cypress.config.js index cd4942209..4adfae8b2 100644 --- a/src/test/e2e/cypress.config.js +++ b/src/test/e2e/cypress.config.js @@ -10,6 +10,7 @@ module.exports = defineConfig({ reporterOptions: { configFile: 'reporter-config.json' }, + userAgent: 'Cypress WrongSecrets E2E Tests', setupNodeEvents (on, config) { // implement node event listeners here } diff --git a/src/test/java/org/owasp/wrongsecrets/challenges/docker/SlackNotificationServiceTest.java b/src/test/java/org/owasp/wrongsecrets/challenges/docker/SlackNotificationServiceTest.java index 3402d3aa8..3ea174fc6 100644 --- a/src/test/java/org/owasp/wrongsecrets/challenges/docker/SlackNotificationServiceTest.java +++ b/src/test/java/org/owasp/wrongsecrets/challenges/docker/SlackNotificationServiceTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpEntity; @@ -28,6 +29,77 @@ void setUp() { objectMapper = new ObjectMapper(); } + @Test + void shouldSendNotificationWithUserAgentWhenSlackIsConfigured() { + // Given + String webhookUrl = "https://hooks.slack.com/services/T123456789/B123456789/abcdef123456"; + String userAgent = "Mozilla/5.0 (Test Browser)"; + when(challenge59.getSlackWebhookUrl()).thenReturn(webhookUrl); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(ResponseEntity.ok("ok")); + + slackNotificationService = + new SlackNotificationService(restTemplate, objectMapper, challenge59); + + // When + slackNotificationService.notifyChallengeCompletion("challenge-1", "testuser", userAgent); + + // Then + verify(restTemplate, times(1)) + .postForEntity(eq(webhookUrl), any(HttpEntity.class), eq(String.class)); + } + + @Test + void shouldIncludeUserAgentInMessageWhenProvided() { + // Given + String webhookUrl = "https://hooks.slack.com/services/T123456789/B123456789/abcdef123456"; + String userAgent = "Cypress WrongSecrets E2E Tests"; + when(challenge59.getSlackWebhookUrl()).thenReturn(webhookUrl); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(ResponseEntity.ok("ok")); + + slackNotificationService = + new SlackNotificationService(restTemplate, objectMapper, challenge59); + + // When + slackNotificationService.notifyChallengeCompletion("challenge-1", "testuser", userAgent); + + // Then + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(HttpEntity.class); + verify(restTemplate, times(1)) + .postForEntity(eq(webhookUrl), entityCaptor.capture(), eq(String.class)); + + HttpEntity capturedEntity = entityCaptor.getValue(); + SlackNotificationService.SlackMessage slackMessage = + (SlackNotificationService.SlackMessage) capturedEntity.getBody(); + assertTrue(slackMessage.getText().contains("(User-Agent: " + userAgent + ")")); + } + + @Test + void shouldNotIncludeUserAgentInMessageWhenNotProvided() { + // Given + String webhookUrl = "https://hooks.slack.com/services/T123456789/B123456789/abcdef123456"; + when(challenge59.getSlackWebhookUrl()).thenReturn(webhookUrl); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(ResponseEntity.ok("ok")); + + slackNotificationService = + new SlackNotificationService(restTemplate, objectMapper, challenge59); + + // When + slackNotificationService.notifyChallengeCompletion("challenge-1", "testuser", null); + + // Then + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(HttpEntity.class); + verify(restTemplate, times(1)) + .postForEntity(eq(webhookUrl), entityCaptor.capture(), eq(String.class)); + + HttpEntity capturedEntity = entityCaptor.getValue(); + SlackNotificationService.SlackMessage slackMessage = + (SlackNotificationService.SlackMessage) capturedEntity.getBody(); + assertFalse(slackMessage.getText().contains("User-Agent")); + } + @Test void shouldSendNotificationWhenSlackIsConfigured() { // Given