feat: add AI-based security suggestion engine and CLI integration
Introduce new package org.egothor.methodatlas.ai providing AI-assisted classification of JUnit tests for security relevance. Key changes: - add AI suggestion engine, provider abstraction, and provider clients (OpenAI-compatible, Ollama, Anthropic) - implement strict JSON prompt/response contract and taxonomy handling - integrate AI enrichment into MethodAtlas CLI output (CSV and plain modes) - add configuration via AiOptions and CLI flags - add comprehensive JUnit + Mockito test coverage for AI components and CLI integration scenarios - add realistic test fixtures for security-related test classes - update Gradle configuration for Mockito agent support on JDK 21+ - provide complete Javadoc for the AI module The AI layer is optional and degrades gracefully when providers are unavailable or responses fail.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package com.acme.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class AccessControlServiceTest {
|
||||
|
||||
@Test
|
||||
@Tag("security")
|
||||
@Tag("authz")
|
||||
void shouldAllowOwnerToReadOwnStatement() {
|
||||
String userId = "user-100";
|
||||
String ownerId = "user-100";
|
||||
|
||||
boolean allowed = userId.equals(ownerId);
|
||||
|
||||
assertEquals(true, allowed);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("security")
|
||||
@Tag("authz")
|
||||
void shouldAllowAdministratorToReadAnyStatement() {
|
||||
String role = "ADMIN";
|
||||
|
||||
boolean allowed = "ADMIN".equals(role);
|
||||
|
||||
assertEquals(true, allowed);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("security")
|
||||
@Tag("authz")
|
||||
void shouldDenyForeignUserFromReadingAnotherUsersStatement() {
|
||||
String requesterId = "user-200";
|
||||
String ownerId = "user-100";
|
||||
|
||||
boolean allowed = requesterId.equals(ownerId);
|
||||
|
||||
assertEquals(false, allowed);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("security")
|
||||
@Tag("authn")
|
||||
void shouldRejectUnauthenticatedRequest() {
|
||||
String principal = null;
|
||||
|
||||
IllegalStateException ex = assertThrows(IllegalStateException.class, () -> {
|
||||
if (principal == null) {
|
||||
throw new IllegalStateException("Unauthenticated request");
|
||||
}
|
||||
});
|
||||
|
||||
assertEquals("Unauthenticated request", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRenderFriendlyAccountLabel() {
|
||||
String firstName = "Ada";
|
||||
String lastName = "Lovelace";
|
||||
|
||||
String label = firstName + " " + lastName;
|
||||
|
||||
assertEquals("Ada Lovelace", label);
|
||||
}
|
||||
}
|
||||
53
src/test/resources/fixtures/AuditLoggingTest.java.txt
Normal file
53
src/test/resources/fixtures/AuditLoggingTest.java.txt
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.acme.audit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class AuditLoggingTest {
|
||||
|
||||
@Test
|
||||
@Tag("security")
|
||||
@Tag("audit")
|
||||
void shouldWriteAuditEventForPrivilegeChange() {
|
||||
String actor = "admin-7";
|
||||
String action = "GRANT_ROLE";
|
||||
String target = "user-17";
|
||||
|
||||
String auditLine = "actor=" + actor + ";action=" + action + ";target=" + target;
|
||||
|
||||
assertEquals("actor=admin-7;action=GRANT_ROLE;target=user-17", auditLine);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("security")
|
||||
@Tag("logging")
|
||||
void shouldNotLogRawBearerToken() {
|
||||
String token = "Bearer eyJhbGciOiJIUzI1NiJ9.secret.signature";
|
||||
String logLine = "Authorization header redacted";
|
||||
|
||||
assertFalse(logLine.contains(token));
|
||||
assertEquals("Authorization header redacted", logLine);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("security")
|
||||
@Tag("logging")
|
||||
void shouldNotLogPlaintextPasswordOnAuthenticationFailure() {
|
||||
String password = "Sup3rSecret!";
|
||||
String logLine = "Authentication failed for user alice";
|
||||
|
||||
assertFalse(logLine.contains(password));
|
||||
assertEquals("Authentication failed for user alice", logLine);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFormatHumanReadableSupportMessage() {
|
||||
String user = "alice";
|
||||
String message = "Support ticket opened for " + user;
|
||||
|
||||
assertEquals("Support ticket opened for alice", message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.acme.storage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class PathTraversalValidationTest {
|
||||
|
||||
@Test
|
||||
@Tag("security")
|
||||
@Tag("validation")
|
||||
void shouldRejectRelativePathTraversalSequence() {
|
||||
String userInput = "../secrets.txt";
|
||||
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> {
|
||||
if (userInput.contains("..")) {
|
||||
throw new IllegalArgumentException("Path traversal attempt detected");
|
||||
}
|
||||
});
|
||||
|
||||
assertEquals("Path traversal attempt detected", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("security")
|
||||
@Tag("validation")
|
||||
void shouldRejectNestedTraversalAfterNormalization() {
|
||||
String userInput = "reports/../../admin/keys.txt";
|
||||
Path normalized = Path.of("/srv/app/uploads").resolve(userInput).normalize();
|
||||
|
||||
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> {
|
||||
if (!normalized.startsWith(Path.of("/srv/app/uploads"))) {
|
||||
throw new IllegalArgumentException("Escaped upload root");
|
||||
}
|
||||
});
|
||||
|
||||
assertEquals("Escaped upload root", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("security")
|
||||
@Tag("validation")
|
||||
void shouldAllowSafePathInsideUploadRoot() {
|
||||
String userInput = "reports/2026/statement.pdf";
|
||||
Path normalized = Path.of("/srv/app/uploads").resolve(userInput).normalize();
|
||||
|
||||
boolean allowed = normalized.startsWith(Path.of("/srv/app/uploads"));
|
||||
|
||||
assertEquals(true, allowed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildDownloadFileName() {
|
||||
String accountId = "ACC-42";
|
||||
String fileName = accountId + "-statement.pdf";
|
||||
|
||||
assertEquals("ACC-42-statement.pdf", fileName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user