diff --git a/src/test/java/com/scalesec/vulnado/CommentTests.java b/src/test/java/com/scalesec/vulnado/CommentTests.java new file mode 100644 index 000000000..9f4e9d9b5 --- /dev/null +++ b/src/test/java/com/scalesec/vulnado/CommentTests.java @@ -0,0 +1,167 @@ +package com.scalesec.vulnado; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class VulnadoApplicationTests { + + @Test + public void contextLoads() { + throw new UnsupportedOperationException("Method not implemented"); + } + + @Test + public void Comment_Create_ShouldReturnComment() { + // Arrange + String username = "testUser"; + String body = "testBody"; + Comment mockComment = mock(Comment.class); + when(mockComment.commit()).thenReturn(true); + + // Act + Comment result = Comment.create(username, body); + + // Assert + assertNotNull("Comment should not be null", result); + assertEquals("Username should match", username, result.getUsername()); + assertEquals("Body should match", body, result.getBody()); + } + + @Test(expected = BadRequest.class) + public void Comment_Create_ShouldThrowBadRequestWhenCommitFails() { + // Arrange + String username = "testUser"; + String body = "testBody"; + Comment mockComment = mock(Comment.class); + when(mockComment.commit()).thenReturn(false); + + // Act + Comment.create(username, body); + } + + @Test(expected = ServerError.class) + public void Comment_Create_ShouldThrowServerErrorOnException() { + // Arrange + String username = "testUser"; + String body = "testBody"; + Comment mockComment = mock(Comment.class); + when(mockComment.commit()).thenThrow(new SQLException("Database error")); + + // Act + Comment.create(username, body); + } + + @Test + public void Comment_FetchAll_ShouldReturnListOfComments() throws SQLException { + // Arrange + Connection mockConnection = mock(Connection.class); + Statement mockStatement = mock(Statement.class); + ResultSet mockResultSet = mock(ResultSet.class); + + when(mockConnection.createStatement()).thenReturn(mockStatement); + when(mockStatement.executeQuery(anyString())).thenReturn(mockResultSet); + when(mockResultSet.next()).thenReturn(true, false); + when(mockResultSet.getString("id")).thenReturn(UUID.randomUUID().toString()); + when(mockResultSet.getString("username")).thenReturn("testUser"); + when(mockResultSet.getString("body")).thenReturn("testBody"); + when(mockResultSet.getTimestamp("created_on")).thenReturn(new Timestamp(System.currentTimeMillis())); + + Postgres.setConnection(mockConnection); + + // Act + List comments = Comment.fetchAll(); + + // Assert + assertNotNull("Comments list should not be null", comments); + assertEquals("Comments list should contain one comment", 1, comments.size()); + } + + @Test + public void Comment_Delete_ShouldReturnTrueWhenSuccessful() throws SQLException { + // Arrange + String commentId = UUID.randomUUID().toString(); + Connection mockConnection = mock(Connection.class); + PreparedStatement mockPreparedStatement = mock(PreparedStatement.class); + + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockPreparedStatement.executeUpdate()).thenReturn(1); + + Postgres.setConnection(mockConnection); + + // Act + boolean result = Comment.delete(commentId); + + // Assert + assertTrue("Delete should return true when successful", result); + } + + @Test + public void Comment_Delete_ShouldReturnFalseWhenUnsuccessful() throws SQLException { + // Arrange + String commentId = UUID.randomUUID().toString(); + Connection mockConnection = mock(Connection.class); + PreparedStatement mockPreparedStatement = mock(PreparedStatement.class); + + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockPreparedStatement.executeUpdate()).thenReturn(0); + + Postgres.setConnection(mockConnection); + + // Act + boolean result = Comment.delete(commentId); + + // Assert + assertFalse("Delete should return false when unsuccessful", result); + } + + @Test + public void Comment_Commit_ShouldReturnTrueWhenSuccessful() throws SQLException { + // Arrange + Comment comment = new Comment(UUID.randomUUID().toString(), "testUser", "testBody", new Timestamp(System.currentTimeMillis())); + Connection mockConnection = mock(Connection.class); + PreparedStatement mockPreparedStatement = mock(PreparedStatement.class); + + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockPreparedStatement.executeUpdate()).thenReturn(1); + + Postgres.setConnection(mockConnection); + + // Act + boolean result = comment.commit(); + + // Assert + assertTrue("Commit should return true when successful", result); + } + + @Test + public void Comment_Commit_ShouldReturnFalseWhenUnsuccessful() throws SQLException { + // Arrange + Comment comment = new Comment(UUID.randomUUID().toString(), "testUser", "testBody", new Timestamp(System.currentTimeMillis())); + Connection mockConnection = mock(Connection.class); + PreparedStatement mockPreparedStatement = mock(PreparedStatement.class); + + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + when(mockPreparedStatement.executeUpdate()).thenReturn(0); + + Postgres.setConnection(mockConnection); + + // Act + boolean result = comment.commit(); + + // Assert + assertFalse("Commit should return false when unsuccessful", result); + } +} diff --git a/src/test/java/com/scalesec/vulnado/CommentsControllerTests.java b/src/test/java/com/scalesec/vulnado/CommentsControllerTests.java new file mode 100644 index 000000000..5bab11174 --- /dev/null +++ b/src/test/java/com/scalesec/vulnado/CommentsControllerTests.java @@ -0,0 +1,125 @@ +package com.scalesec.vulnado; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class CommentsControllerTests { + + @Value("${app.secret}") + private String secret; + + @Test + public void comments_ShouldReturnAllComments() { + // Arrange + CommentsController controller = new CommentsController(); + String token = "valid-token"; + List mockComments = new ArrayList<>(); + mockComments.add(new Comment("user1", "This is a comment.")); + mockComments.add(new Comment("user2", "Another comment.")); + mockStatic(Comment.class); + when(Comment.fetch_all()).thenReturn(mockComments); + + // Act + List result = controller.comments(token); + + // Assert + assertNotNull("Result should not be null", result); + assertEquals("Result size should match", 2, result.size()); + assertEquals("First comment username should match", "user1", result.get(0).getUsername()); + assertEquals("Second comment body should match", "Another comment.", result.get(1).getBody()); + } + + @Test + public void createComment_ShouldCreateAndReturnComment() { + // Arrange + CommentsController controller = new CommentsController(); + String token = "valid-token"; + CommentRequest input = new CommentRequest(); + input.setUsername("user1"); + input.setBody("This is a new comment."); + Comment mockComment = new Comment("user1", "This is a new comment."); + mockStatic(Comment.class); + when(Comment.create(input.getUsername(), input.getBody())).thenReturn(mockComment); + + // Act + Comment result = controller.createComment(token, input); + + // Assert + assertNotNull("Result should not be null", result); + assertEquals("Username should match", "user1", result.getUsername()); + assertEquals("Body should match", "This is a new comment.", result.getBody()); + } + + @Test + public void deleteComment_ShouldDeleteAndReturnTrue() { + // Arrange + CommentsController controller = new CommentsController(); + String token = "valid-token"; + String commentId = "123"; + mockStatic(Comment.class); + when(Comment.delete(commentId)).thenReturn(true); + + // Act + Boolean result = controller.deleteComment(token, commentId); + + // Assert + assertTrue("Result should be true", result); + } + + @Test(expected = ResponseStatusException.class) + public void comments_ShouldThrowExceptionForInvalidToken() { + // Arrange + CommentsController controller = new CommentsController(); + String invalidToken = "invalid-token"; + mockStatic(User.class); + doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED)).when(User.class); + User.assertAuth(secret, invalidToken); + + // Act + controller.comments(invalidToken); + } + + @Test(expected = ResponseStatusException.class) + public void createComment_ShouldThrowExceptionForInvalidToken() { + // Arrange + CommentsController controller = new CommentsController(); + String invalidToken = "invalid-token"; + CommentRequest input = new CommentRequest(); + input.setUsername("user1"); + input.setBody("This is a new comment."); + mockStatic(User.class); + doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED)).when(User.class); + User.assertAuth(secret, invalidToken); + + // Act + controller.createComment(invalidToken, input); + } + + @Test(expected = ResponseStatusException.class) + public void deleteComment_ShouldThrowExceptionForInvalidToken() { + // Arrange + CommentsController controller = new CommentsController(); + String invalidToken = "invalid-token"; + String commentId = "123"; + mockStatic(User.class); + doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED)).when(User.class); + User.assertAuth(secret, invalidToken); + + // Act + controller.deleteComment(invalidToken, commentId); + } +} diff --git a/src/test/java/com/scalesec/vulnado/CowControllerTests.java b/src/test/java/com/scalesec/vulnado/CowControllerTests.java new file mode 100644 index 000000000..272422b99 --- /dev/null +++ b/src/test/java/com/scalesec/vulnado/CowControllerTests.java @@ -0,0 +1,64 @@ +package com.scalesec.vulnado; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.ResponseEntity; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class VulnadoApplicationTests { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void contextLoads() { + // Ensures the application context loads successfully + assertNotNull("Application context should load", restTemplate); + } + + @Test + public void cowsay_GetRequest_ShouldReturnDefaultMessage() { + // Test the GET request with default input + ResponseEntity response = restTemplate.getForEntity("/cowsay", String.class); + assertEquals("Response status should be 200", 200, response.getStatusCodeValue()); + assertNotNull("Response body should not be null", response.getBody()); + assertEquals("Response should match default message", Cowsay.run("I love Linux!"), response.getBody()); + } + + @Test + public void cowsay_GetRequest_ShouldReturnCustomMessage() { + // Test the GET request with custom input + String customMessage = "Hello, World!"; + ResponseEntity response = restTemplate.getForEntity("/cowsay?input=" + customMessage, String.class); + assertEquals("Response status should be 200", 200, response.getStatusCodeValue()); + assertNotNull("Response body should not be null", response.getBody()); + assertEquals("Response should match custom message", Cowsay.run(customMessage), response.getBody()); + } + + @Test + public void cowsay_PostRequest_ShouldReturnDefaultMessage() { + // Test the POST request with default input + ResponseEntity response = restTemplate.postForEntity("/cowsay", null, String.class); + assertEquals("Response status should be 200", 200, response.getStatusCodeValue()); + assertNotNull("Response body should not be null", response.getBody()); + assertEquals("Response should match default message", Cowsay.run("I love Linux!"), response.getBody()); + } + + @Test + public void cowsay_PostRequest_ShouldReturnCustomMessage() { + // Test the POST request with custom input + String customMessage = "Spring Boot is awesome!"; + ResponseEntity response = restTemplate.postForEntity("/cowsay?input=" + customMessage, null, String.class); + assertEquals("Response status should be 200", 200, response.getStatusCodeValue()); + assertNotNull("Response body should not be null", response.getBody()); + assertEquals("Response should match custom message", Cowsay.run(customMessage), response.getBody()); + } +} diff --git a/src/test/java/com/scalesec/vulnado/CowsayTests.java b/src/test/java/com/scalesec/vulnado/CowsayTests.java new file mode 100644 index 000000000..51ea20bb7 --- /dev/null +++ b/src/test/java/com/scalesec/vulnado/CowsayTests.java @@ -0,0 +1,103 @@ +package com.scalesec.vulnado; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import static org.junit.Assert.*; +import org.mockito.Mockito; +import java.util.logging.Logger; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class VulnadoApplicationTests { + + @Test + public void contextLoads() { + throw new UnsupportedOperationException("Method not implemented"); + } + + @Test + public void Cowsay_Run_ShouldReturnExpectedOutput() throws IOException { + // Mocking the ProcessBuilder and its behavior + ProcessBuilder mockProcessBuilder = Mockito.mock(ProcessBuilder.class); + Process mockProcess = Mockito.mock(Process.class); + BufferedReader mockReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream("Mocked Cowsay Output\n".getBytes()))); + + Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); + Mockito.when(mockProcess.getInputStream()).thenReturn(new ByteArrayInputStream("Mocked Cowsay Output\n".getBytes())); + + // Mocking Logger + Logger mockLogger = Mockito.mock(Logger.class); + + // Test the run method + String input = "Hello, World!"; + String expectedOutput = "Mocked Cowsay Output\n"; + String actualOutput = Cowsay.run(input); + + assertEquals("The output should match the mocked cowsay output", expectedOutput, actualOutput); + } + + @Test + public void Cowsay_Run_ShouldHandleEmptyInput() throws IOException { + // Mocking the ProcessBuilder and its behavior + ProcessBuilder mockProcessBuilder = Mockito.mock(ProcessBuilder.class); + Process mockProcess = Mockito.mock(Process.class); + BufferedReader mockReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream("Mocked Cowsay Output\n".getBytes()))); + + Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); + Mockito.when(mockProcess.getInputStream()).thenReturn(new ByteArrayInputStream("Mocked Cowsay Output\n".getBytes())); + + // Mocking Logger + Logger mockLogger = Mockito.mock(Logger.class); + + // Test the run method with empty input + String input = ""; + String expectedOutput = "Mocked Cowsay Output\n"; + String actualOutput = Cowsay.run(input); + + assertEquals("The output should match the mocked cowsay output for empty input", expectedOutput, actualOutput); + } + + @Test + public void Cowsay_Run_ShouldHandleSpecialCharacters() throws IOException { + // Mocking the ProcessBuilder and its behavior + ProcessBuilder mockProcessBuilder = Mockito.mock(ProcessBuilder.class); + Process mockProcess = Mockito.mock(Process.class); + BufferedReader mockReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream("Mocked Cowsay Output\n".getBytes()))); + + Mockito.when(mockProcessBuilder.start()).thenReturn(mockProcess); + Mockito.when(mockProcess.getInputStream()).thenReturn(new ByteArrayInputStream("Mocked Cowsay Output\n".getBytes())); + + // Mocking Logger + Logger mockLogger = Mockito.mock(Logger.class); + + // Test the run method with special characters + String input = "Hello, 'World'!"; + String expectedOutput = "Mocked Cowsay Output\n"; + String actualOutput = Cowsay.run(input); + + assertEquals("The output should match the mocked cowsay output for special characters", expectedOutput, actualOutput); + } + + @Test + public void Cowsay_Run_ShouldHandleException() throws IOException { + // Mocking the ProcessBuilder and its behavior + ProcessBuilder mockProcessBuilder = Mockito.mock(ProcessBuilder.class); + Mockito.when(mockProcessBuilder.start()).thenThrow(new IOException("Mocked Exception")); + + // Mocking Logger + Logger mockLogger = Mockito.mock(Logger.class); + + // Test the run method with exception + String input = "Hello, World!"; + String expectedOutput = ""; + String actualOutput = Cowsay.run(input); + + assertEquals("The output should be empty when an exception occurs", expectedOutput, actualOutput); + } +} diff --git a/src/test/java/com/scalesec/vulnado/LinkListerTests.java b/src/test/java/com/scalesec/vulnado/LinkListerTests.java new file mode 100644 index 000000000..b1e235af3 --- /dev/null +++ b/src/test/java/com/scalesec/vulnado/LinkListerTests.java @@ -0,0 +1,80 @@ +package com.scalesec.vulnado; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class VulnadoApplicationTests { + + @Test + public void contextLoads() { + throw new UnsupportedOperationException("Method not implemented"); + } + + @Test + public void getLinks_ValidUrl_ShouldReturnLinks() throws IOException { + // Arrange + String testUrl = "http://example.com"; + LinkLister mockLinkLister = Mockito.mock(LinkLister.class); + when(mockLinkLister.getLinks(testUrl)).thenReturn(List.of("http://example.com/link1", "http://example.com/link2")); + + // Act + List links = mockLinkLister.getLinks(testUrl); + + // Assert + assertNotNull("Links should not be null", links); + assertEquals("Links size should match", 2, links.size()); + assertTrue("Links should contain expected URL", links.contains("http://example.com/link1")); + assertTrue("Links should contain expected URL", links.contains("http://example.com/link2")); + } + + @Test(expected = IOException.class) + public void getLinks_InvalidUrl_ShouldThrowIOException() throws IOException { + // Arrange + String invalidUrl = "http://invalid-url"; + LinkLister mockLinkLister = Mockito.mock(LinkLister.class); + when(mockLinkLister.getLinks(invalidUrl)).thenThrow(new IOException("Invalid URL")); + + // Act + mockLinkLister.getLinks(invalidUrl); + } + + @Test + public void getLinksV2_ValidUrl_ShouldLogHost() throws Exception { + // Arrange + String testUrl = "http://example.com"; + LinkLister mockLinkLister = Mockito.mock(LinkLister.class); + when(mockLinkLister.getLinksV2(testUrl)).thenReturn(List.of("http://example.com/link1", "http://example.com/link2")); + + // Act + List links = mockLinkLister.getLinksV2(testUrl); + + // Assert + assertNotNull("Links should not be null", links); + assertEquals("Links size should match", 2, links.size()); + assertTrue("Links should contain expected URL", links.contains("http://example.com/link1")); + assertTrue("Links should contain expected URL", links.contains("http://example.com/link2")); + } + + @Test(expected = MalformedURLException.class) + public void getLinksV2_InvalidUrl_ShouldThrowMalformedURLException() throws Exception { + // Arrange + String invalidUrl = "invalid-url"; + LinkLister mockLinkLister = Mockito.mock(LinkLister.class); + when(mockLinkLister.getLinksV2(invalidUrl)).thenThrow(new MalformedURLException("Invalid URL")); + + // Act + mockLinkLister.getLinksV2(invalidUrl); + } +} diff --git a/src/test/java/com/scalesec/vulnado/LinksControllerTests.java b/src/test/java/com/scalesec/vulnado/LinksControllerTests.java new file mode 100644 index 000000000..6aecb4232 --- /dev/null +++ b/src/test/java/com/scalesec/vulnado/LinksControllerTests.java @@ -0,0 +1,96 @@ +package com.scalesec.vulnado; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.ResponseEntity; +import org.springframework.http.HttpStatus; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class VulnadoApplicationTests { + + @Autowired + private TestRestTemplate restTemplate; + + @MockBean + private LinkLister linkLister; + + @Test + public void contextLoads() { + // Ensures the application context loads successfully + assertNotNull("Application context should load successfully", restTemplate); + } + + @Test + public void links_ShouldReturnLinks() throws Exception { + // Mocking the LinkLister.getLinks method + String testUrl = "http://example.com"; + List mockLinks = Arrays.asList("http://example.com/link1", "http://example.com/link2"); + Mockito.when(linkLister.getLinks(testUrl)).thenReturn(mockLinks); + + // Sending a GET request to the /links endpoint + ResponseEntity response = restTemplate.getForEntity("/links?url=" + testUrl, List.class); + + // Asserting the response + assertEquals("Response status should be OK", HttpStatus.OK, response.getStatusCode()); + assertNotNull("Response body should not be null", response.getBody()); + assertEquals("Response body should match the mocked links", mockLinks, response.getBody()); + } + + @Test + public void linksV2_ShouldReturnLinksV2() throws Exception { + // Mocking the LinkLister.getLinksV2 method + String testUrl = "http://example.com"; + List mockLinks = Arrays.asList("http://example.com/link1", "http://example.com/link2"); + Mockito.when(linkLister.getLinksV2(testUrl)).thenReturn(mockLinks); + + // Sending a GET request to the /links-v2 endpoint + ResponseEntity response = restTemplate.getForEntity("/links-v2?url=" + testUrl, List.class); + + // Asserting the response + assertEquals("Response status should be OK", HttpStatus.OK, response.getStatusCode()); + assertNotNull("Response body should not be null", response.getBody()); + assertEquals("Response body should match the mocked links", mockLinks, response.getBody()); + } + + @Test + public void links_ShouldHandleIOException() throws Exception { + // Mocking the LinkLister.getLinks method to throw IOException + String testUrl = "http://example.com"; + Mockito.when(linkLister.getLinks(testUrl)).thenThrow(new IOException("Mocked IOException")); + + // Sending a GET request to the /links endpoint + ResponseEntity response = restTemplate.getForEntity("/links?url=" + testUrl, String.class); + + // Asserting the response + assertEquals("Response status should be INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertNotNull("Response body should not be null", response.getBody()); + } + + @Test + public void linksV2_ShouldHandleBadRequest() throws Exception { + // Mocking the LinkLister.getLinksV2 method to throw BadRequest + String testUrl = "http://example.com"; + Mockito.when(linkLister.getLinksV2(testUrl)).thenThrow(new BadRequest("Mocked BadRequest")); + + // Sending a GET request to the /links-v2 endpoint + ResponseEntity response = restTemplate.getForEntity("/links-v2?url=" + testUrl, String.class); + + // Asserting the response + assertEquals("Response status should be BAD_REQUEST", HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertNotNull("Response body should not be null", response.getBody()); + } +} diff --git a/src/test/java/com/scalesec/vulnado/LoginControllerTests.java b/src/test/java/com/scalesec/vulnado/LoginControllerTests.java new file mode 100644 index 000000000..aa50846ce --- /dev/null +++ b/src/test/java/com/scalesec/vulnado/LoginControllerTests.java @@ -0,0 +1,114 @@ +package com.scalesec.vulnado; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class LoginControllerTests { + + @InjectMocks + private LoginController loginController; + + @Mock + private User mockUser; + + @Mock + private Postgres mockPostgres; + + @Value("${app.secret}") + private String secret; + + // Helper method to create a LoginRequest object + private LoginRequest createLoginRequest(String username, String password) { + LoginRequest request = new LoginRequest(); + request.username = username; + request.password = password; + return request; + } + + @Test + public void login_ValidCredentials_ShouldReturnToken() { + // Arrange + String username = "testUser"; + String password = "testPassword"; + String hashedPassword = "hashedPassword"; + String token = "validToken"; + + LoginRequest request = createLoginRequest(username, password); + + when(User.fetch(username)).thenReturn(mockUser); + when(mockUser.hashedPassword).thenReturn(hashedPassword); + when(Postgres.md5(password)).thenReturn(hashedPassword); + when(mockUser.token(secret)).thenReturn(token); + + // Act + LoginResponse response = loginController.login(request); + + // Assert + assertNotNull("Response should not be null", response); + assertEquals("Token should match expected value", token, response.token); + } + + @Test + public void login_InvalidCredentials_ShouldThrowUnauthorizedException() { + // Arrange + String username = "testUser"; + String password = "wrongPassword"; + String hashedPassword = "hashedPassword"; + + LoginRequest request = createLoginRequest(username, password); + + when(User.fetch(username)).thenReturn(mockUser); + when(mockUser.hashedPassword).thenReturn(hashedPassword); + when(Postgres.md5(password)).thenReturn("wrongHashedPassword"); + + // Act & Assert + try { + loginController.login(request); + fail("Expected Unauthorized exception to be thrown"); + } catch (Unauthorized ex) { + assertEquals("Exception message should match", "Access Denied", ex.getMessage()); + } + } + + @Test + public void login_NonExistentUser_ShouldThrowUnauthorizedException() { + // Arrange + String username = "nonExistentUser"; + String password = "testPassword"; + + LoginRequest request = createLoginRequest(username, password); + + when(User.fetch(username)).thenReturn(null); + + // Act & Assert + try { + loginController.login(request); + fail("Expected Unauthorized exception to be thrown"); + } catch (Unauthorized ex) { + assertEquals("Exception message should match", "Access Denied", ex.getMessage()); + } + } + + @Test + public void login_NullRequest_ShouldThrowException() { + // Act & Assert + try { + loginController.login(null); + fail("Expected exception to be thrown"); + } catch (Exception ex) { + assertTrue("Exception should be of type ResponseStatusException", ex instanceof ResponseStatusException); + assertEquals("Exception status should be BAD_REQUEST", HttpStatus.BAD_REQUEST, ((ResponseStatusException) ex).getStatus()); + } + } +} diff --git a/src/test/java/com/scalesec/vulnado/PostgresTests.java b/src/test/java/com/scalesec/vulnado/PostgresTests.java new file mode 100644 index 000000000..20a302859 --- /dev/null +++ b/src/test/java/com/scalesec/vulnado/PostgresTests.java @@ -0,0 +1,109 @@ +package com.scalesec.vulnado; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.UUID; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class PostgresTests { + + @Test + public void connection_ShouldEstablishConnection() { + try { + Connection connection = Postgres.connection(); + assertNotNull("Connection should not be null", connection); + connection.close(); + } catch (Exception e) { + fail("Exception occurred while establishing connection: " + e.getMessage()); + } + } + + @Test + public void setup_ShouldCreateTablesAndInsertSeedData() { + try { + Postgres.setup(); + Connection connection = Postgres.connection(); + + // Verify users table + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) AS count FROM users"); + rs.next(); + int userCount = rs.getInt("count"); + assertEquals("Users table should have 5 seed entries", 5, userCount); + + // Verify comments table + rs = stmt.executeQuery("SELECT COUNT(*) AS count FROM comments"); + rs.next(); + int commentCount = rs.getInt("count"); + assertEquals("Comments table should have 2 seed entries", 2, commentCount); + + connection.close(); + } catch (Exception e) { + fail("Exception occurred during setup: " + e.getMessage()); + } + } + + @Test + public void md5_ShouldReturnCorrectHash() { + String input = "test"; + String expectedHash = "098f6bcd4621d373cade4e832627b4f6"; // Precomputed MD5 hash for "test" + String actualHash = Postgres.md5(input); + assertEquals("MD5 hash should match expected value", expectedHash, actualHash); + } + + @Test + public void insertUser_ShouldInsertUserIntoDatabase() { + try { + String username = "testUser"; + String password = "testPassword"; + Postgres.insertUser(username, password); + + Connection connection = Postgres.connection(); + PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE username = ?"); + stmt.setString(1, username); + ResultSet rs = stmt.executeQuery(); + + assertTrue("User should exist in the database", rs.next()); + assertEquals("Username should match", username, rs.getString("username")); + assertEquals("Password hash should match", Postgres.md5(password), rs.getString("password")); + + connection.close(); + } catch (Exception e) { + fail("Exception occurred while inserting user: " + e.getMessage()); + } + } + + @Test + public void insertComment_ShouldInsertCommentIntoDatabase() { + try { + String username = "testUser"; + String body = "This is a test comment"; + Postgres.insertComment(username, body); + + Connection connection = Postgres.connection(); + PreparedStatement stmt = connection.prepareStatement("SELECT * FROM comments WHERE username = ? AND body = ?"); + stmt.setString(1, username); + stmt.setString(2, body); + ResultSet rs = stmt.executeQuery(); + + assertTrue("Comment should exist in the database", rs.next()); + assertEquals("Username should match", username, rs.getString("username")); + assertEquals("Comment body should match", body, rs.getString("body")); + + connection.close(); + } catch (Exception e) { + fail("Exception occurred while inserting comment: " + e.getMessage()); + } + } +} diff --git a/src/test/java/com/scalesec/vulnado/UserTests.java b/src/test/java/com/scalesec/vulnado/UserTests.java new file mode 100644 index 000000000..baedb8fce --- /dev/null +++ b/src/test/java/com/scalesec/vulnado/UserTests.java @@ -0,0 +1,109 @@ +package com.scalesec.vulnado; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.mockito.Mockito; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.logging.Logger; +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class VulnadoApplicationTests { + + @Test + public void contextLoads() { + throw new UnsupportedOperationException("Method not implemented"); + } + + @Test + public void user_Token_ShouldGenerateValidToken() { + // Arrange + String secret = "mysecretkey12345678901234567890"; // 32-byte key + User user = new User("1", "testuser", "hashedpassword"); + + // Act + String token = user.token(secret); + + // Assert + assertNotNull("Token should not be null", token); + assertFalse("Token should not be empty", token.isEmpty()); + } + + @Test + public void user_AssertAuth_ShouldValidateTokenSuccessfully() { + // Arrange + String secret = "mysecretkey12345678901234567890"; // 32-byte key + User user = new User("1", "testuser", "hashedpassword"); + String token = user.token(secret); + + // Act & Assert + try { + User.assertAuth(secret, token); + } catch (Exception e) { + fail("Token validation should not throw an exception"); + } + } + + @Test(expected = Unauthorized.class) + public void user_AssertAuth_ShouldThrowUnauthorizedForInvalidToken() { + // Arrange + String secret = "mysecretkey12345678901234567890"; // 32-byte key + String invalidToken = "invalidtoken"; + + // Act + User.assertAuth(secret, invalidToken); + } + + @Test + public void user_Fetch_ShouldReturnUserForValidUsername() throws Exception { + // Arrange + String username = "testuser"; + Connection mockConnection = Mockito.mock(Connection.class); + PreparedStatement mockPreparedStatement = Mockito.mock(PreparedStatement.class); + ResultSet mockResultSet = Mockito.mock(ResultSet.class); + + Mockito.when(mockConnection.prepareStatement(Mockito.anyString())).thenReturn(mockPreparedStatement); + Mockito.when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + Mockito.when(mockResultSet.next()).thenReturn(true); + Mockito.when(mockResultSet.getString("user_id")).thenReturn("1"); + Mockito.when(mockResultSet.getString("username")).thenReturn(username); + Mockito.when(mockResultSet.getString("password")).thenReturn("hashedpassword"); + + Postgres.setMockConnection(mockConnection); // Assuming Postgres.connection() can be mocked + + // Act + User user = User.fetch(username); + + // Assert + assertNotNull("User should not be null", user); + assertEquals("User ID should match", "1", user.getId()); + assertEquals("Username should match", username, user.getUsername()); + assertEquals("Password should match", "hashedpassword", user.getHashedPassword()); + } + + @Test + public void user_Fetch_ShouldReturnNullForInvalidUsername() throws Exception { + // Arrange + String username = "invaliduser"; + Connection mockConnection = Mockito.mock(Connection.class); + PreparedStatement mockPreparedStatement = Mockito.mock(PreparedStatement.class); + ResultSet mockResultSet = Mockito.mock(ResultSet.class); + + Mockito.when(mockConnection.prepareStatement(Mockito.anyString())).thenReturn(mockPreparedStatement); + Mockito.when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + Mockito.when(mockResultSet.next()).thenReturn(false); + + Postgres.setMockConnection(mockConnection); // Assuming Postgres.connection() can be mocked + + // Act + User user = User.fetch(username); + + // Assert + assertNull("User should be null for invalid username", user); + } +} diff --git a/src/test/java/com/scalesec/vulnado/VulnadoApplicationTests.java b/src/test/java/com/scalesec/vulnado/VulnadoApplicationTests.java index 0db5324e1..e6a51ff79 100644 --- a/src/test/java/com/scalesec/vulnado/VulnadoApplicationTests.java +++ b/src/test/java/com/scalesec/vulnado/VulnadoApplicationTests.java @@ -4,15 +4,36 @@ import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; +import org.mockito.Mockito; +import org.springframework.boot.SpringApplication; @RunWith(SpringRunner.class) @SpringBootTest public class VulnadoApplicationTests { - @Test - public void contextLoads() { + @Test + public void contextLoads() { + // Test to ensure the Spring context loads successfully throw new UnsupportedOperationException("Method not implemented"); - } + } -} + @Test + public void main_ShouldInitializeApplication() { + // Mocking Postgres.setup() to ensure it is called + Postgres mockPostgres = Mockito.mock(Postgres.class); + Mockito.doNothing().when(mockPostgres).setup(); + + // Mocking SpringApplication.run() to ensure it is called + SpringApplication mockSpringApplication = Mockito.mock(SpringApplication.class); + Mockito.doNothing().when(mockSpringApplication).run(Mockito.any(Class.class), Mockito.any(String[].class)); + + // Call the main method + VulnadoApplication.main(new String[]{}); + // Verify that Postgres.setup() was called + Mockito.verify(mockPostgres, Mockito.times(1)).setup(); + + // Verify that SpringApplication.run() was called + Mockito.verify(mockSpringApplication, Mockito.times(1)).run(Mockito.eq(VulnadoApplication.class), Mockito.any(String[].class)); + } +} diff --git a/wiki/src/main/java/com/scalesec/vulnado/CommentDocs.md b/wiki/src/main/java/com/scalesec/vulnado/CommentDocs.md new file mode 100644 index 000000000..cb0db361e --- /dev/null +++ b/wiki/src/main/java/com/scalesec/vulnado/CommentDocs.md @@ -0,0 +1,87 @@ +# Comment.java: Comment Management Class + +## Overview +The `Comment` class is responsible for managing comments in a system. It provides functionality to create, fetch, and delete comments, as well as commit them to a database. The class interacts with a PostgreSQL database to perform these operations. + +## Process Flow +```mermaid +flowchart TD + Start("Start") --> CreateComment["Create a Comment"] + CreateComment --> |"Generate UUID and Timestamp"| GenerateData["Generate UUID and Timestamp"] + GenerateData --> |"Commit to Database"| CommitToDB["Commit Comment to Database"] + CommitToDB --> |"Return Created Comment"| End("End") + + FetchAllComments["Fetch All Comments"] --> |"Connect to Database"| ConnectDB["Connect to Database"] + ConnectDB --> |"Execute SELECT Query"| ExecuteQuery["Execute SELECT Query"] + ExecuteQuery --> |"Iterate ResultSet"| IterateResultSet["Iterate ResultSet"] + IterateResultSet --> |"Return List of Comments"| End + + DeleteComment["Delete a Comment"] --> |"Connect to Database"| ConnectDB_Delete["Connect to Database"] + ConnectDB_Delete --> |"Execute DELETE Query"| ExecuteDeleteQuery["Execute DELETE Query"] + ExecuteDeleteQuery --> |"Return Success Status"| End +``` + +## Insights +- **Comment Creation**: Generates a unique ID and timestamp for each comment and commits it to the database. +- **Fetching Comments**: Retrieves all comments from the database using a `SELECT` query and maps the result set to `Comment` objects. +- **Deleting Comments**: Deletes a specific comment from the database using its ID. +- **Error Handling**: Uses custom exceptions (`BadRequest` and `ServerError`) for error handling during comment creation. +- **Database Connection**: Relies on a `Postgres.connection()` method to establish connections to the database. + +## Dependencies +```mermaid +flowchart LR + Comment --- |"Depends"| Postgres + Comment --- |"Uses"| Logger + Comment --- |"Uses"| BadRequest + Comment --- |"Uses"| ServerError +``` + +- `Postgres`: Provides the `connection()` method to interact with the PostgreSQL database. +- `Logger`: Used for logging errors during database operations. +- `BadRequest`: Custom exception thrown when a comment cannot be saved. +- `ServerError`: Custom exception thrown for server-side errors. + +## Data Manipulation (SQL) +### Table Structure: `comments` +| Attribute | Data Type | Description | +|-------------|-------------|--------------------------------------| +| `id` | `VARCHAR` | Unique identifier for the comment. | +| `username` | `VARCHAR` | Username of the comment author. | +| `body` | `TEXT` | Content of the comment. | +| `created_on`| `TIMESTAMP` | Timestamp when the comment was created. | + +### SQL Operations +- **INSERT**: Adds a new comment to the `comments` table. + ```sql + INSERT INTO comments (id, username, body, created_on) VALUES (?,?,?,?) + ``` +- **SELECT**: Fetches all comments from the `comments` table. + ```sql + SELECT id, username, body, created_on FROM comments; + ``` +- **DELETE**: Deletes a comment from the `comments` table by ID. + ```sql + DELETE FROM comments WHERE id = ?; + ``` + +## Vulnerabilities +1. **SQL Injection**: + - The `fetchAll` method uses a raw SQL query without parameterized statements, making it vulnerable to SQL injection attacks. + - Recommendation: Use `PreparedStatement` for the `SELECT` query to prevent SQL injection. + +2. **Resource Management**: + - The `Connection` object in `fetchAll` and `delete` methods is not properly closed in a `finally` block, which can lead to resource leaks. + - Recommendation: Ensure all database connections are closed in a `finally` block or use try-with-resources. + +3. **Error Logging**: + - The `fetchAll` method logs errors using `Logger` but does not propagate them, potentially hiding critical issues. + - Recommendation: Propagate exceptions or return meaningful error responses. + +4. **Exception Handling**: + - The `create` method catches all exceptions and wraps them in a `ServerError`, which may obscure the root cause of the error. + - Recommendation: Handle specific exceptions and provide detailed error messages. + +5. **Timestamp Usage**: + - The `createdOn` attribute is directly set using `new Timestamp(new Date().getTime())`, which may lead to timezone-related issues. + - Recommendation: Use a standardized timezone or UTC for timestamps. diff --git a/wiki/src/main/java/com/scalesec/vulnado/CommentsControllerDocs.md b/wiki/src/main/java/com/scalesec/vulnado/CommentsControllerDocs.md new file mode 100644 index 000000000..5dbc2874e --- /dev/null +++ b/wiki/src/main/java/com/scalesec/vulnado/CommentsControllerDocs.md @@ -0,0 +1,74 @@ +# CommentsController.java: REST API for Comment Management + +## Overview +The `CommentsController` class provides a REST API for managing comments. It includes endpoints for fetching, creating, and deleting comments. The class also handles authentication using a secret value and token-based validation. Additionally, it defines custom exceptions for handling bad requests and server errors. + +## Process Flow +```mermaid +flowchart TD + Start("Start") --> |"Initialize CommentsController"| Initialize_CommentsController["CommentsController"] + Initialize_CommentsController --> |"Fetch all comments"| Fetch_Comments["GET /comments"] + Fetch_Comments --> |"Authenticate user token"| Authenticate_Token["User.assertAuth(secret, token)"] + Authenticate_Token --> |"Return all comments"| Return_Comments["Comment.fetch_all()"] + + Initialize_CommentsController --> |"Create a new comment"| Create_Comment["POST /comments"] + Create_Comment --> |"Process input"| Process_Input["CommentRequest"] + Process_Input --> |"Create comment"| Create_Comment_Action["Comment.create(input.username, input.body)"] + + Initialize_CommentsController --> |"Delete a comment"| Delete_Comment["DELETE /comments/{id}"] + Delete_Comment --> |"Authenticate user token"| Authenticate_Token_Delete["User.assertAuth(secret, token)"] + Authenticate_Token_Delete --> |"Delete comment"| Delete_Comment_Action["Comment.delete(id)"] + + Initialize_CommentsController --> |"Handle exceptions"| Handle_Exceptions["BadRequest & ServerError"] + Handle_Exceptions --> End("End") +``` + +## Insights +- **Authentication**: The API uses token-based authentication (`x-auth-token`) and validates it against a secret value. +- **Cross-Origin Resource Sharing (CORS)**: The endpoints are restricted to requests originating from `http://trusted-domain.com`. +- **Custom Exceptions**: `BadRequest` and `ServerError` classes are defined to handle specific error scenarios. +- **Logging**: Extensive logging is present throughout the code, but it is incorrectly placed outside of methods and may not execute as intended. +- **Comment Management**: The API provides methods to fetch all comments, create a new comment, and delete an existing comment. + +## Dependencies +```mermaid +flowchart LR + CommentsController --- |"Depends"| Comment + CommentsController --- |"Depends"| User + CommentsController --- |"Depends"| CommentRequest + CommentsController --- |"Depends"| BadRequest + CommentsController --- |"Depends"| ServerError +``` + +- `Comment`: Used for fetching, creating, and deleting comments. +- `User`: Used for authenticating user tokens. +- `CommentRequest`: Represents the input structure for creating a comment. +- `BadRequest`: Custom exception for handling bad requests. +- `ServerError`: Custom exception for handling server errors. + +## Vulnerabilities +1. **Improper Logging Placement**: + - Logging statements are placed outside of methods and may not execute as intended. This could lead to missing logs and debugging challenges. + +2. **Hardcoded CORS Origin**: + - The `@CrossOrigin` annotation restricts requests to `http://trusted-domain.com`. If this domain changes, the code must be updated manually, which is not scalable. + +3. **Authentication Logic**: + - The `User.assertAuth(secret, token)` method is called but its implementation is not provided. If this method does not properly validate tokens, it could lead to unauthorized access. + +4. **Error Handling**: + - The custom exceptions (`BadRequest` and `ServerError`) do not log the exception details or stack trace, which could make debugging more difficult. + +5. **Potential Injection Vulnerability**: + - The `Comment.create(input.username, input.body)` and `Comment.delete(id)` methods are called without sanitizing inputs. If these methods interact with a database, they could be vulnerable to SQL injection. + +6. **Lack of Input Validation**: + - The `CommentRequest` class does not validate `username` or `body`. Malformed or malicious input could lead to unexpected behavior or security issues. + +## Data Manipulation (SQL) +If the `Comment` class interacts with a database, the following operations are implied: +- `Comment.fetch_all()`: Likely performs a `SELECT` operation to retrieve all comments. +- `Comment.create(input.username, input.body)`: Likely performs an `INSERT` operation to add a new comment. +- `Comment.delete(id)`: Likely performs a `DELETE` operation to remove a comment by its ID. + +However, the exact SQL structure and attributes are not provided in the code. diff --git a/wiki/src/main/java/com/scalesec/vulnado/CowControllerDocs.md b/wiki/src/main/java/com/scalesec/vulnado/CowControllerDocs.md new file mode 100644 index 000000000..b0339d9a0 --- /dev/null +++ b/wiki/src/main/java/com/scalesec/vulnado/CowControllerDocs.md @@ -0,0 +1,55 @@ +# CowController.java: Cow Controller for Handling Cowsay Requests + +## Overview +The `CowController` class is a Spring Boot REST controller designed to handle HTTP requests for the `/cowsay` endpoint. It allows users to send a message, which is processed by the `Cowsay.run` method to generate a "cowsay" output. + +## Process Flow +```mermaid +flowchart TD + Start("HTTP Request to /cowsay") + Decision{"Request Method"} + GET["GET Request"] + POST["POST Request"] + ProcessInput["Extract 'input' parameter"] + DefaultInput["Default: 'I love Linux!'"] + RunCowsay["Call Cowsay.run(input)"] + Response["Return Cowsay Output"] + + Start --> Decision + Decision --> |"GET"| GET + Decision --> |"POST"| POST + GET --> ProcessInput + POST --> ProcessInput + ProcessInput --> |"If 'input' not provided"| DefaultInput + DefaultInput --> RunCowsay + ProcessInput --> RunCowsay + RunCowsay --> Response +``` + +## Insights +- The `/cowsay` endpoint supports both `GET` and `POST` HTTP methods. +- The `input` parameter is optional; if not provided, it defaults to `"I love Linux!"`. +- The `Cowsay.run` method is responsible for generating the cowsay output based on the provided input. +- The controller uses Spring Boot annotations such as `@RestController` and `@EnableAutoConfiguration` for configuration and REST API functionality. + +## Dependencies +```mermaid +flowchart LR + CowController --- |"Calls"| Cowsay + CowController --- |"Uses"| org_springframework_web_bind_annotation + CowController --- |"Uses"| org_springframework_boot_autoconfigure +``` + +- `Cowsay`: Processes the `input` parameter and generates the cowsay output. +- `org.springframework.web.bind.annotation`: Provides annotations for mapping HTTP requests to controller methods. +- `org.springframework.boot.autoconfigure`: Enables auto-configuration for Spring Boot applications. + +## Vulnerabilities +- **Command Injection Risk**: If the `Cowsay.run` method executes system commands based on the `input` parameter, it may be vulnerable to command injection attacks. Proper sanitization and validation of the `input` parameter are necessary to mitigate this risk. +- **Denial of Service (DoS)**: If the `Cowsay.run` method performs resource-intensive operations, an attacker could exploit this by sending large or malicious inputs repeatedly, potentially causing a denial of service. +- **Unvalidated Input**: The `input` parameter is directly passed to `Cowsay.run` without validation, which could lead to unexpected behavior or security issues depending on the implementation of `Cowsay.run`. + +## Recommendations +- Validate and sanitize the `input` parameter to prevent command injection and other input-related vulnerabilities. +- Implement rate limiting or input size restrictions to mitigate potential DoS attacks. +- Review the implementation of `Cowsay.run` to ensure it handles inputs securely and efficiently. diff --git a/wiki/src/main/java/com/scalesec/vulnado/CowsayDocs.md b/wiki/src/main/java/com/scalesec/vulnado/CowsayDocs.md new file mode 100644 index 000000000..be64cfa89 --- /dev/null +++ b/wiki/src/main/java/com/scalesec/vulnado/CowsayDocs.md @@ -0,0 +1,57 @@ +# Cowsay.java: Command Execution Wrapper for Cowsay + +## Overview +This program provides a wrapper for executing the `cowsay` command-line tool, which generates ASCII art of a cow saying a given message. It sanitizes the input to prevent certain characters and logs the command execution process. The program uses Java's `ProcessBuilder` to execute the command and captures the output. + +## Process Flow +```mermaid +flowchart TD + Start("Start") + Input["Input: User-provided string"] + SanitizeInput["Sanitize input to remove special characters"] + BuildCommand["Build command for cowsay"] + LogCommand["Log the command"] + ExecuteCommand["Execute command using ProcessBuilder"] + CaptureOutput["Capture output from command execution"] + ReturnOutput("Return output as String") + End("End") + + Start --> Input + Input --> SanitizeInput + SanitizeInput --> BuildCommand + BuildCommand --> LogCommand + LogCommand --> ExecuteCommand + ExecuteCommand --> CaptureOutput + CaptureOutput --> ReturnOutput + ReturnOutput --> End +``` + +## Insights +- **Input Sanitization**: The program removes special characters (`"`, `'`, `\`) from the input string to prevent command injection vulnerabilities. +- **Logging**: The command being executed is logged using `Logger` for debugging purposes. +- **Command Execution**: The `ProcessBuilder` is used to execute the `cowsay` command in a bash shell. +- **Error Handling**: Errors during command execution are logged as warnings, but the program does not rethrow exceptions or provide detailed error handling. +- **Output Capture**: The program captures the output of the `cowsay` command and returns it as a string. + +## Vulnerabilities +1. **Command Injection**: Although the input is sanitized to remove certain characters, the sanitization is incomplete and does not account for all possible injection vectors. For example, characters like `;`, `|`, `&`, and `$` are not removed, which could allow malicious input to execute arbitrary commands. +2. **Improper Error Handling**: The program logs exceptions but does not handle them effectively. This could lead to silent failures or incomplete output in case of errors. +3. **Hardcoded Command Path**: The path to the `cowsay` executable (`/usr/games/cowsay`) is hardcoded, which may not be portable across different systems. +4. **Debugging Information in Production**: Logging the command being executed could expose sensitive information in production environments. + +## Dependencies +```mermaid +flowchart LR + Cowsay --- |"Uses"| java_util_logging_Logger + Cowsay --- |"Uses"| java_io_BufferedReader + Cowsay --- |"Uses"| java_io_InputStreamReader + Cowsay --- |"Uses"| java_lang_ProcessBuilder +``` + +- `java.util.logging.Logger`: Used for logging command execution and errors. +- `java.io.BufferedReader`: Used to read the output of the executed command. +- `java.io.InputStreamReader`: Wraps the input stream of the process for reading. +- `java.lang.ProcessBuilder`: Used to execute the `cowsay` command. + +## Data Manipulation (SQL) +No SQL data manipulation is present in this program. diff --git a/wiki/src/main/java/com/scalesec/vulnado/LinkListerDocs.md b/wiki/src/main/java/com/scalesec/vulnado/LinkListerDocs.md new file mode 100644 index 000000000..23a3598e4 --- /dev/null +++ b/wiki/src/main/java/com/scalesec/vulnado/LinkListerDocs.md @@ -0,0 +1,58 @@ +# LinkLister.java: Web Link Extraction Utility + +## Overview +The `LinkLister` class provides functionality to extract hyperlinks from a given webpage URL. It uses the `Jsoup` library to parse HTML content and retrieve all anchor (``) tags. The class includes two methods for link extraction, with one offering additional logging capabilities. + +## Process Flow +```mermaid +flowchart TD + Start("Start") + Input["Input: URL"] + ParseHTML["Parse HTML using Jsoup"] + ExtractLinks["Extract tags"] + BuildList["Build list of absolute URLs"] + Output["Output: List of URLs"] + Start --> Input + Input --> ParseHTML + ParseHTML --> ExtractLinks + ExtractLinks --> BuildList + BuildList --> Output +``` + +## Insights +- The `LinkLister` class is designed as a utility class with a private constructor to prevent instantiation. +- The `getLinks` method: + - Connects to the provided URL using `Jsoup.connect(url).get()`. + - Extracts all anchor (``) tags and retrieves their absolute URLs using `link.absUrl("href")`. +- The `getLinksV2` method: + - Parses the URL using `java.net.URL` to extract the host. + - Includes logging functionality to log the host of the URL. +- The class relies on the `Jsoup` library for HTML parsing and the `java.net.URL` class for URL manipulation. + +## Dependencies +```mermaid +flowchart LR + LinkLister --- |"Imports"| org_jsoup + LinkLister --- |"Imports"| java_io + LinkLister --- |"Imports"| java_net + LinkLister --- |"Imports"| java_util_logging +``` + +- `org.jsoup`: Used for HTML parsing and DOM manipulation. +- `java.io`: Provides `IOException` handling for network operations. +- `java.net`: Used for URL parsing and manipulation. +- `java.util.logging`: Used for logging host information in `getLinksV2`. + +## Vulnerabilities +- **Unvalidated Input**: The `getLinks` method directly connects to the provided URL without validating it. This could lead to security risks such as SSRF (Server-Side Request Forgery). +- **Error Handling in `getLinksV2`**: The `getLinksV2` method does not handle potential exceptions from `URL` parsing or logging operations comprehensively, which could lead to runtime errors. +- **Logging Sensitive Information**: Logging the host of the URL in `getLinksV2` might expose sensitive information if the logs are not properly secured. +- **No Timeout Configuration**: The `Jsoup.connect(url).get()` call does not specify a timeout, which could lead to indefinite blocking in case of network issues. +- **Potential NullPointerException**: If the `absUrl("href")` method fails to retrieve a valid URL, it could result in a `NullPointerException`. + +## Recommendations +- Validate the input URL before processing to prevent SSRF and other security risks. +- Implement comprehensive error handling for both methods. +- Avoid logging sensitive information or ensure logs are securely stored. +- Configure a timeout for `Jsoup.connect()` to prevent indefinite blocking. +- Check for null values when extracting URLs to avoid `NullPointerException`. diff --git a/wiki/src/main/java/com/scalesec/vulnado/LinksControllerDocs.md b/wiki/src/main/java/com/scalesec/vulnado/LinksControllerDocs.md new file mode 100644 index 000000000..e52154add --- /dev/null +++ b/wiki/src/main/java/com/scalesec/vulnado/LinksControllerDocs.md @@ -0,0 +1,43 @@ +# LinksController.java: REST API for Link Extraction + +## Overview +The `LinksController` class is a REST API controller that provides endpoints for extracting links from a given URL. It uses the `LinkLister` utility to perform the link extraction and supports two versions of the endpoint: `/links` and `/links-v2`. + +## Process Flow +```mermaid +flowchart TD + Start("Request Received") + CheckURL{"Validate URL"} + ExtractLinks["Extract Links using LinkLister"] + ReturnResponse("Return List of Links") + + Start --> CheckURL + CheckURL --> ExtractLinks + ExtractLinks --> ReturnResponse +``` + +## Insights +- The class is annotated with `@RestController` and `@EnableAutoConfiguration`, making it a Spring Boot REST API controller with automatic configuration. +- Two endpoints are defined: + - `/links`: Extracts links using the `LinkLister.getLinks` method. + - `/links-v2`: Extracts links using the `LinkLister.getLinksV2` method, which may have additional validation or functionality. +- The `links` method throws `IOException`, indicating potential issues with URL processing or network operations. +- The `linksV2` method throws `BadRequest`, suggesting it performs stricter validation or error handling compared to the first version. + +## Dependencies +```mermaid +flowchart LR + LinksController --- |"Calls"| LinkLister +``` + +- `LinkLister`: Provides methods `getLinks` and `getLinksV2` for extracting links from a given URL. + +## Vulnerabilities +- **Potential Security Risk in URL Handling**: The `@RequestParam` directly accepts a URL from the user without validation or sanitization. This could lead to vulnerabilities such as Server-Side Request Forgery (SSRF) or injection attacks if the `LinkLister` methods do not properly validate the input. +- **Error Handling**: The `links` method throws `IOException`, but there is no explicit error handling or logging mechanism in place. This could result in unhandled exceptions and insufficient feedback to the client. +- **BadRequest Exception in `linksV2`**: The `BadRequest` exception is thrown, but its implementation is not shown. If not properly handled, it could lead to inconsistent error responses or expose internal details. + +## Recommendations +- Implement input validation and sanitization for the `url` parameter to prevent SSRF and other injection attacks. +- Add proper error handling and logging mechanisms to ensure robust API behavior and better client feedback. +- Review the implementation of `LinkLister.getLinks` and `LinkLister.getLinksV2` to ensure secure and efficient link extraction. diff --git a/wiki/src/main/java/com/scalesec/vulnado/LoginControllerDocs.md b/wiki/src/main/java/com/scalesec/vulnado/LoginControllerDocs.md new file mode 100644 index 000000000..e8e932f86 --- /dev/null +++ b/wiki/src/main/java/com/scalesec/vulnado/LoginControllerDocs.md @@ -0,0 +1,66 @@ +# LoginController.java: Login Authentication Controller + +## Overview +The `LoginController` class is a REST API controller responsible for handling user login requests. It validates user credentials against stored data and generates a token for successful authentication. The controller uses Spring Boot annotations for configuration and routing. + +## Process Flow +```mermaid +flowchart TD + Start("Start") + InputRequest>"Receive LoginRequest"] + FetchUser["Fetch User from Database"] + ValidatePassword{"Validate Password"} + GenerateToken["Generate Token"] + ReturnResponse>"Return LoginResponse"] + Unauthorized["Throw Unauthorized Exception"] + + Start --> InputRequest + InputRequest --> FetchUser + FetchUser --> ValidatePassword + ValidatePassword --> |"Password Valid"| GenerateToken + ValidatePassword --> |"Password Invalid"| Unauthorized + GenerateToken --> ReturnResponse +``` + +## Insights +- **Cross-Origin Resource Sharing (CORS):** The `@CrossOrigin` annotation allows requests from any origin, which could be a security risk if not properly configured. +- **Hardcoded Secret:** The `secret` is injected from application properties, but its usage in token generation should be carefully managed to avoid exposure. +- **Password Validation:** The password is hashed using `Postgres.md5` and compared with the stored hashed password. Ensure the hashing algorithm is secure and up-to-date. +- **Error Handling:** Unauthorized access results in throwing a custom `Unauthorized` exception with HTTP status 401. +- **Serialization:** Both `LoginRequest` and `LoginResponse` implement `Serializable`, ensuring compatibility for object serialization. + +## Dependencies +```mermaid +flowchart LR + LoginController --- |"Depends"| Postgres + LoginController --- |"Depends"| User +``` + +- `Postgres`: Used for hashing the password with the `md5` method. +- `User`: Represents the user entity and provides methods like `fetch` and `token`. + +### Identified External References +- `Postgres`: Provides the `md5` method for hashing passwords. Relation nature: Depends. +- `User`: Represents the user entity and provides methods for fetching user data and generating tokens. Relation nature: Depends. + +## Vulnerabilities +1. **CORS Misconfiguration:** + - Allowing all origins (`@CrossOrigin(origins = "*")`) can expose the application to cross-origin attacks. It is recommended to restrict origins to trusted domains. + +2. **Weak Password Hashing:** + - The use of `Postgres.md5` for password hashing may not be secure against modern attacks. Consider using a stronger hashing algorithm like bcrypt or Argon2. + +3. **Hardcoded Secret:** + - The `secret` used for token generation is injected from application properties. Ensure it is stored securely and rotated periodically. + +4. **Potential User Enumeration:** + - If the `User.fetch` method throws an exception or behaves differently for invalid usernames, it could allow attackers to enumerate valid usernames. + +5. **Lack of Rate Limiting:** + - The login endpoint does not implement rate limiting, making it vulnerable to brute-force attacks. + +6. **Error Message Exposure:** + - Throwing an `Unauthorized` exception with a specific message could expose sensitive information. Use generic error messages instead. + +## Data Manipulation (SQL) +- **User Entity:** Likely interacts with a database to fetch user details. Ensure proper sanitization and validation of inputs to prevent SQL injection. diff --git a/wiki/src/main/java/com/scalesec/vulnado/PostgresDocs.md b/wiki/src/main/java/com/scalesec/vulnado/PostgresDocs.md new file mode 100644 index 000000000..83484bc63 --- /dev/null +++ b/wiki/src/main/java/com/scalesec/vulnado/PostgresDocs.md @@ -0,0 +1,96 @@ +# Postgres.java: Database Setup and Interaction Utility + +## Overview +The `Postgres` class is responsible for managing database connections, setting up schemas, inserting seed data, and providing utility methods for interacting with a PostgreSQL database. It includes methods for creating tables, inserting users and comments, and hashing passwords using MD5. + +## Process Flow +```mermaid +flowchart TD + Start("Start") + ConnectDB["Establish Database Connection"] + SetupDB["Setup Database Schema"] + InsertSeedData["Insert Seed Data"] + InsertUser["Insert User"] + InsertComment["Insert Comment"] + HashPassword["Hash Password using MD5"] + End("End") + + Start --> ConnectDB + ConnectDB --> SetupDB + SetupDB --> InsertSeedData + InsertSeedData --> InsertUser + InsertSeedData --> InsertComment + InsertUser --> HashPassword + InsertComment --> End + HashPassword --> End +``` + +## Insights +- **Database Connection**: The `connection()` method dynamically constructs the database connection URL using environment variables (`PGHOST`, `PGDATABASE`, `PGUSER`, `PGPASSWORD`). +- **Schema Setup**: The `setup()` method creates two tables (`users` and `comments`) and cleans up any existing data before inserting seed data. +- **Password Hashing**: The `md5()` method hashes passwords using MD5, which is considered insecure for sensitive contexts due to vulnerabilities like collision attacks. +- **Seed Data**: The `setup()` method inserts predefined users and comments into the database for initial setup. +- **Error Handling**: Error handling is minimal, with exceptions logged and the application exiting on failure. +- **UUID Usage**: Unique identifiers for users and comments are generated using `UUID.randomUUID()`. + +## Vulnerabilities +1. **Insecure Password Hashing**: + - MD5 is used for hashing passwords, which is vulnerable to collision attacks and brute force. A stronger hashing algorithm like bcrypt or Argon2 should be used for password storage. + +2. **Hardcoded Seed Data**: + - The seed data includes hardcoded usernames and passwords, which could lead to security risks if used in production environments. + +3. **Error Handling**: + - Exceptions are logged but not properly handled, leading to abrupt application termination (`System.exit(1)`). + +4. **Environment Variable Exposure**: + - Database credentials are retrieved from environment variables, which could be exposed if not properly secured. + +5. **Potential SQL Injection**: + - While `PreparedStatement` is used, the lack of input validation for `username` and `body` in `insertComment()` could lead to potential SQL injection risks if the input is not sanitized. + +## Dependencies +```mermaid +flowchart LR + Postgres --- |"Depends"| java_sql_Connection + Postgres --- |"Depends"| java_sql_DriverManager + Postgres --- |"Depends"| java_sql_PreparedStatement + Postgres --- |"Depends"| java_sql_Statement + Postgres --- |"Depends"| java_util_UUID + Postgres --- |"Depends"| java_security_MessageDigest + Postgres --- |"Depends"| java_security_NoSuchAlgorithmException + Postgres --- |"Depends"| java_math_BigInteger +``` + +- `java.sql.Connection`: Used for establishing database connections. +- `java.sql.DriverManager`: Used for managing database drivers and connections. +- `java.sql.PreparedStatement`: Used for executing parameterized SQL queries. +- `java.sql.Statement`: Used for executing SQL statements. +- `java.util.UUID`: Used for generating unique identifiers. +- `java.security.MessageDigest`: Used for hashing passwords. +- `java.security.NoSuchAlgorithmException`: Handles exceptions related to unsupported hashing algorithms. +- `java.math.BigInteger`: Used for converting byte arrays into hexadecimal strings. + +## Data Manipulation (SQL) +### Table Structures +#### `users` +| Attribute | Type | Description | +|--------------|---------------|--------------------------------------------------| +| `user_id` | VARCHAR(36) | Primary key, unique identifier for the user. | +| `username` | VARCHAR(50) | Unique username for the user. | +| `password` | VARCHAR(50) | Hashed password of the user. | +| `created_on` | TIMESTAMP | Timestamp of user creation. | +| `last_login` | TIMESTAMP | Timestamp of the last login. | + +#### `comments` +| Attribute | Type | Description | +|--------------|---------------|--------------------------------------------------| +| `id` | VARCHAR(36) | Primary key, unique identifier for the comment. | +| `username` | VARCHAR(36) | Username of the commenter. | +| `body` | VARCHAR(500) | Content of the comment. | +| `created_on` | TIMESTAMP | Timestamp of comment creation. | + +### SQL Operations +- `CREATE TABLE`: Creates `users` and `comments` tables if they do not exist. +- `DELETE`: Cleans up existing data in `users` and `comments` tables. +- `INSERT`: Inserts seed data into `users` and `comments` tables. diff --git a/wiki/src/main/java/com/scalesec/vulnado/UserDocs.md b/wiki/src/main/java/com/scalesec/vulnado/UserDocs.md new file mode 100644 index 000000000..04d4713ee --- /dev/null +++ b/wiki/src/main/java/com/scalesec/vulnado/UserDocs.md @@ -0,0 +1,72 @@ +# User.java: User Management and Authentication + +## Overview +The `User` class is responsible for managing user-related operations, including token generation, authentication, and fetching user details from a database. It provides methods for creating JWT tokens, validating them, and retrieving user information from a PostgreSQL database. + +## Process Flow +```mermaid +flowchart TD + Start("Start") --> |"Create User Object"| CreateUserObject["User Constructor"] + CreateUserObject --> |"Generate Token"| GenerateToken["token(secret)"] + GenerateToken --> End("End") + + Start --> |"Validate Token"| ValidateToken["assertAuth(secret, token)"] + ValidateToken --> End + + Start --> |"Fetch User"| FetchUser["fetch(un)"] + FetchUser --> |"Connect to Database"| ConnectDB["Postgres.connection()"] + ConnectDB --> |"Prepare SQL Query"| PrepareSQL["PreparedStatement"] + PrepareSQL --> |"Execute Query"| ExecuteQuery["ResultSet"] + ExecuteQuery --> |"Create User Object"| CreateUserObject + CreateUserObject --> End +``` + +## Insights +- The `token` method generates a JWT token using the provided secret key and the username as the subject. +- The `assertAuth` method validates a JWT token using the provided secret key. If validation fails, it throws an `Unauthorized` exception. +- The `fetch` method retrieves user details from a PostgreSQL database based on the username. It uses both `Statement` and `PreparedStatement`, but the `Statement` usage introduces SQL injection risks. +- The class uses the `Logger` for logging operations, but there are redundant logging statements in the `fetch` method. +- The `fetch` method improperly closes the database connection, which could lead to resource leaks. + +## Vulnerabilities +1. **SQL Injection**: + - The `fetch` method constructs a query using string concatenation (`String query = "select * from users where username = '" + un + "' limit 1";`), which is vulnerable to SQL injection. Although a `PreparedStatement` is also used, the string concatenation query remains in the code. + +2. **Improper Resource Management**: + - The database connection (`Connection cxn`) is not properly closed in the `finally` block, which could lead to resource leaks. + +3. **Duplicate Logging**: + - The `fetch` method contains redundant logging statements (`logger.severe(e.getClass().getName() + ": " + e.getMessage());`), which could clutter logs and reduce readability. + +4. **Unreachable Code**: + - In the `token` method, the second `return jws;` statement is unreachable and will cause a compilation error. + +5. **Error Handling**: + - The `fetch` method does not handle exceptions effectively, leading to potential silent failures or incomplete error reporting. + +## Dependencies +```mermaid +flowchart LR + User --- |"Calls"| Postgres + User --- |"Uses"| io_jsonwebtoken + User --- |"Uses"| java_sql + User --- |"Uses"| java_util_logging +``` + +- `Postgres`: Provides the database connection. The `fetch` method calls `Postgres.connection()` to establish a connection to the database. +- `io.jsonwebtoken`: Used for JWT token generation and validation. +- `java.sql`: Used for database operations, including `Connection`, `Statement`, `PreparedStatement`, and `ResultSet`. +- `java.util.logging`: Used for logging messages and errors. + +## Data Manipulation (SQL) +### Table Structure +| Attribute Name | Data Type | Description | +|----------------|-----------|-------------| +| user_id | String | Unique identifier for the user. | +| username | String | Username of the user. | +| password | String | Hashed password of the user. | + +### SQL Operations +- **`SELECT`**: Retrieves user details from the `users` table based on the username. + - Query: `SELECT * FROM users WHERE username = ? LIMIT 1` + - Vulnerable Query: `select * from users where username = 'un' limit 1` (SQL Injection risk). diff --git a/wiki/src/main/java/com/scalesec/vulnado/VulnadoApplicationDocs.md b/wiki/src/main/java/com/scalesec/vulnado/VulnadoApplicationDocs.md new file mode 100644 index 000000000..a30072d67 --- /dev/null +++ b/wiki/src/main/java/com/scalesec/vulnado/VulnadoApplicationDocs.md @@ -0,0 +1,33 @@ +# VulnadoApplication.java: Spring Boot Application Entry Point + +## Overview +The `VulnadoApplication` class serves as the entry point for a Spring Boot application. It initializes the application context and sets up necessary configurations, including database setup through the `Postgres.setup()` method. + +## Process Flow +```mermaid +flowchart TD + Start("Application Start") --> PostgresSetup["Postgres.setup()"] + PostgresSetup --> SpringBootRun["SpringApplication.run(VulnadoApplication.class, args)"] + SpringBootRun --> End("Application Running") +``` + +## Insights +- The class is annotated with `@SpringBootApplication`, which is a convenience annotation that includes `@Configuration`, `@EnableAutoConfiguration`, and `@ComponentScan`. +- The `@ServletComponentScan` annotation enables scanning for servlet components such as filters and listeners. +- The `Postgres.setup()` method is called before the Spring Boot application starts, indicating that some database initialization or configuration is performed. +- The `SpringApplication.run()` method is responsible for bootstrapping the Spring Boot application. + +## Dependencies +```mermaid +flowchart LR + VulnadoApplication --- |"Calls"| Postgres + VulnadoApplication --- |"Uses"| SpringApplication +``` + +- `Postgres`: The `setup()` method is called to perform database-related initialization. +- `SpringApplication`: Used to bootstrap and launch the Spring Boot application. + +## Vulnerabilities +- **Potential Database Misconfiguration**: The `Postgres.setup()` method is called without any visible error handling or validation. If the database setup fails, it could lead to application startup issues. +- **Implicit Dependency on Postgres**: The application assumes the presence of a `Postgres` class and its `setup()` method, which could lead to runtime errors if the class is missing or improperly implemented. +- **No Security Measures for Servlet Components**: While `@ServletComponentScan` enables scanning for servlet components, there is no indication of security measures to validate or restrict these components, potentially exposing the application to vulnerabilities.