Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,18 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*

# Secrets and local config
*.pem
.env
.env.*
**/application.yml.bak

# Build outputs
target/
build/
out/

# IDE/editor
.idea/
.vscode/
8 changes: 5 additions & 3 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 0 additions & 8 deletions .idea/modules/MCPService.main.iml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.pict.dtos;

import java.util.List;
import lombok.*;

@Data
Expand All @@ -8,6 +9,9 @@
@Builder
public class AnomalyDetectionRequest {

private String uuid;
private List<BehaviorLogEvent> history;

private double failureRate;
private int requestsPerMinute;
private int uniqueRoutesAccessed;
Expand Down
25 changes: 25 additions & 0 deletions AIService/src/main/java/edu/pict/dtos/BehaviorLogEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package edu.pict.dtos;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BehaviorLogEvent {
private String uuid;
private String path;
private String method;
private String routeId;
private String decision;
private long latencyMs;
private String queryParams;
private String clientIp;
private int statusCode;
private long requestSize;
private long timestamp;
private String userAgent;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import edu.pict.dtos.AnomalyDetectionRequest;
import edu.pict.dtos.AnomalyDetectionResponse;
import edu.pict.dtos.BehaviorLogEvent;
import java.util.List;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -16,9 +19,9 @@ public class AnomalyDetectionService {
public AnomalyDetectionResponse analyze(AnomalyDetectionRequest req) {

long start = System.currentTimeMillis();
FeatureVector features = FeatureVector.from(req);

// Convert features → model prompt OR vector
String prompt = buildPrompt(req);
String prompt = buildPrompt(features);

double score = ollamaService.predictAnomalyScore(prompt);

Expand All @@ -32,7 +35,7 @@ public AnomalyDetectionResponse analyze(AnomalyDetectionRequest req) {
.build();
}

private String buildPrompt(AnomalyDetectionRequest req) {
private String buildPrompt(FeatureVector vector) {
return """
You are an anomaly detection model.
Given numeric behavior signals, return ONLY a number between 0 and 1.
Expand All @@ -45,11 +48,71 @@ private String buildPrompt(AnomalyDetectionRequest req) {
routeSensitivity=%s
"""
.formatted(
req.getFailureRate(),
req.getRequestsPerMinute(),
req.getUniqueRoutesAccessed(),
req.getJwtReuseCount(),
req.getIpReputationScore(),
req.getRouteSensitivity());
vector.failureRate(),
vector.requestsPerMinute(),
vector.uniqueRoutesAccessed(),
vector.jwtReuseCount(),
vector.ipReputationScore(),
vector.routeSensitivity());
}

private record FeatureVector(
double failureRate,
int requestsPerMinute,
int uniqueRoutesAccessed,
int jwtReuseCount,
double ipReputationScore,
String routeSensitivity) {

private static FeatureVector from(AnomalyDetectionRequest req) {
List<BehaviorLogEvent> history = req.getHistory();
if (history != null && !history.isEmpty()) {
long errors = history.stream().filter(log -> log.getStatusCode() >= 400).count();
long distinctRoutes =
history.stream().map(BehaviorLogEvent::getPath).filter(p -> p != null).distinct().count();
String sensitivity =
history.stream()
.map(BehaviorLogEvent::getPath)
.filter(path -> path != null)
.anyMatch(
path ->
path.contains("/admin")
|| path.contains("/internal")
|| path.contains("/mcp"))
? "HIGH"
: "MEDIUM";

return new FeatureVector(
(double) errors / history.size(),
history.size(),
Math.toIntExact(distinctRoutes),
0,
0.5,
sanitizeSensitivity(sensitivity));
}

return new FeatureVector(
clamp(req.getFailureRate(), 0.0, 1.0),
Math.max(req.getRequestsPerMinute(), 0),
Math.max(req.getUniqueRoutesAccessed(), 0),
Math.max(req.getJwtReuseCount(), 0),
clamp(req.getIpReputationScore(), 0.0, 1.0),
sanitizeSensitivity(req.getRouteSensitivity()));
}
}

private static String sanitizeSensitivity(String sensitivity) {
if (sensitivity == null) {
return "MEDIUM";
}
String normalized = sensitivity.trim().toUpperCase(Locale.ROOT);
return switch (normalized) {
case "LOW", "MEDIUM", "HIGH" -> normalized;
default -> "MEDIUM";
};
}

private static double clamp(double value, double min, double max) {
return Math.max(min, Math.min(max, value));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
public class ManagementController {

private final ReactiveStringRedisTemplate redisTemplate;
private static final String BLACKLIST_PREFIX = "blacklist:";
private static final String UUID_BLACKLIST_PREFIX = "blacklist:uuid:";
private static final String LEGACY_BLACKLIST_PREFIX = "blacklist:";

@GetMapping
public Mono<ResponseEntity<List<String>>> getBlacklist() {
return redisTemplate
.keys(BLACKLIST_PREFIX + "*")
.map(key -> key.substring(BLACKLIST_PREFIX.length()))
.keys(UUID_BLACKLIST_PREFIX + "*")
.map(key -> key.substring(UUID_BLACKLIST_PREFIX.length()))
.collectList()
.map(ResponseEntity::ok);
}
Expand All @@ -28,14 +29,15 @@ public Mono<ResponseEntity<List<String>>> getBlacklist() {
public Mono<ResponseEntity<Void>> block(@PathVariable String uuid) {
return redisTemplate
.opsForValue()
.set(BLACKLIST_PREFIX + uuid, "true")
.set(UUID_BLACKLIST_PREFIX + uuid, "true")
.map(success -> ResponseEntity.ok().<Void>build());
}

@DeleteMapping("/{uuid}")
public Mono<ResponseEntity<Void>> unblock(@PathVariable String uuid) {
return redisTemplate
.delete(BLACKLIST_PREFIX + uuid)
.delete(UUID_BLACKLIST_PREFIX + uuid)
.flatMap(count -> redisTemplate.delete(LEGACY_BLACKLIST_PREFIX + uuid))
.map(count -> ResponseEntity.ok().<Void>build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package edu.pict.apigateway.events;

import edu.pict.apigateway.kafkaEvent.LogEvent;
import edu.pict.apigateway.kafkaEvent.SecurityAlertEvent;
import org.springframework.stereotype.Component;

@Component
public class DefaultGatewayEventFactory implements GatewayEventFactory {

@Override
public LogEvent buildLogEvent(RequestContext context, int statusCode, long latencyMs) {
return LogEvent.builder()
.uuid(context.uuid())
.path(context.path())
.method(context.method())
.routeId(context.routeId())
.decision("ALLOWED")
.latencyMs(latencyMs)
.queryParams(context.queryParams())
.clientIp(context.clientIp())
.statusCode(statusCode)
.requestSize(context.requestSize())
.timestamp(context.timestamp())
.userAgent(context.userAgent())
.build();
}

@Override
public SecurityAlertEvent buildSecurityAlert(
RequestContext context, int statusCode, String reasonPhrase) {
return SecurityAlertEvent.builder()
.uuid(context.uuid())
.errorCode(statusCode)
.reason(reasonPhrase)
.attemptedPath(context.path())
.method(context.method())
.userAgent(context.userAgent())
.clientIp(context.clientIp())
.alertSeverity(determineSeverity(statusCode))
.timestamp(context.timestamp())
.build();
}

private String determineSeverity(int code) {
if (code >= 500) return "HIGH";
if (code == 429 || code == 403 || code == 401) return "MEDIUM";
return "LOW";
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package edu.pict.apigateway.events;

import edu.pict.apigateway.service.IpService;
import edu.pict.apigateway.util.Constants;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

@Component
@RequiredArgsConstructor
public class DefaultRequestContextExtractor implements RequestContextExtractor {

private final IpService ipService;

@Override
public RequestContext extract(ServerWebExchange exchange) {
String uuid = exchange.getRequest().getHeaders().getFirst(Constants.VISITOR_ID);
String path = exchange.getRequest().getURI().getPath();
String method = exchange.getRequest().getMethod().toString();
String queryParams = exchange.getRequest().getQueryParams().toString();
long requestSize = exchange.getRequest().getHeaders().getContentLength();
String userAgent = exchange.getRequest().getHeaders().getFirst("User-Agent");
String remoteAddress =
exchange.getRequest().getRemoteAddress() != null
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
: null;
String clientIp = ipService.resolveClientIp(exchange.getRequest().getHeaders(), remoteAddress);

Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
String routeId = (route != null) ? route.getId() : "unknown";

return new RequestContext(
uuid,
path,
method,
routeId,
queryParams,
Math.max(requestSize, 0),
clientIp,
userAgent,
System.currentTimeMillis());
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package edu.pict.apigateway.events;

import edu.pict.apigateway.kafkaEvent.LogEvent;
import edu.pict.apigateway.kafkaEvent.SecurityAlertEvent;

public interface GatewayEventFactory {
LogEvent buildLogEvent(RequestContext context, int statusCode, long latencyMs);

SecurityAlertEvent buildSecurityAlert(RequestContext context, int statusCode, String reasonPhrase);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package edu.pict.apigateway.events;

public record RequestContext(
String uuid,
String path,
String method,
String routeId,
String queryParams,
long requestSize,
String clientIp,
String userAgent,
long timestamp) {}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package edu.pict.apigateway.events;

import org.springframework.web.server.ServerWebExchange;

public interface RequestContextExtractor {
RequestContext extract(ServerWebExchange exchange);
}

Loading