Skip to content

AIK-4442 Report blocked requests per IPList/Botlist #166

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9a6293d
Create new FirewallListsRecord to store blocked and total
bitterpanda63 Apr 16, 2025
7094ac5
Reformat the Statistics object, adding ipAddresses and userAgents
bitterpanda63 Apr 16, 2025
ba1dcd1
stats: merge total/blocked funcs for iplists/ua's
bitterpanda63 Apr 16, 2025
0dd1b84
Add ip lists/ua's to StatisticsStore
bitterpanda63 Apr 16, 2025
ffecb7b
Add new StatisticsStore test cases & fix broken heartbeat test
bitterpanda63 Apr 16, 2025
67ca96c
Add ipAddreses and userAgents stats to clear() in Statistics.java
bitterpanda63 Apr 16, 2025
ce63c4d
Change FirewallListsRecord to use new format
bitterpanda63 Apr 17, 2025
da5f113
Update test cases with new tests
bitterpanda63 Apr 17, 2025
9af073d
Update reporting api
bitterpanda63 Apr 17, 2025
488bdc4
Revert "Update reporting api"
bitterpanda63 Apr 17, 2025
9bb2f8f
Update API call that fetches firewall lists
bitterpanda63 Apr 17, 2025
9c016b1
Merge branch 'fix-mutex-mistake-ip-lists' into AIK-4442
bitterpanda63 Apr 18, 2025
41a5610
Store the response in a special ParsedFirewallLists object
bitterpanda63 Apr 18, 2025
bdaf3bf
WebRequestCollector run all scans
bitterpanda63 Apr 18, 2025
94cf593
Add matching code to ParsedFirewallLists
bitterpanda63 Apr 28, 2025
1eff096
Use the new ParsedFirewallLists, keep boolean match for allowed ips
bitterpanda63 Apr 28, 2025
73a68f9
refactor for ServiceConfiguration.java
bitterpanda63 Apr 28, 2025
30f0438
Update test cases and remove unused code
bitterpanda63 Apr 29, 2025
7d5537e
Report hits, fixes bug with test cases
bitterpanda63 Apr 29, 2025
e8dcf3d
Fix bug for uas and update service config unit tests to test for stats
bitterpanda63 Apr 29, 2025
60e0cb0
Update the mock core server
bitterpanda63 Apr 29, 2025
882cf7a
Update e2e tests to also check monitoring is monitored
bitterpanda63 Apr 29, 2025
c206ee0
Fix bug for when allowed ips are empty, and add regression tests
bitterpanda63 Apr 29, 2025
3b43133
Create a copy so the clear action has no effect
bitterpanda63 Apr 29, 2025
a61a0ec
Fix broken test which checks lists response
bitterpanda63 Apr 30, 2025
14bc88a
Remove unused code for ParsedFirewallLists
bitterpanda63 Apr 30, 2025
df63de2
Update test cases for better coverage
bitterpanda63 Apr 30, 2025
9b651e0
Change the APIListsResponse to reflect the current API
bitterpanda63 May 9, 2025
fdd7091
Change ParsedFirewallLists to save data and return results from new api
bitterpanda63 May 9, 2025
7f4855c
Update statistics to not include total/blocked breakdown
bitterpanda63 May 9, 2025
5eb3a8b
Remove unused FirewallListsRecord
bitterpanda63 May 9, 2025
c322983
Change how the ServiceConfiguration increments work
bitterpanda63 May 9, 2025
4496003
Update mock aikido core api spec
bitterpanda63 May 9, 2025
72c6d09
Fix WebRequestCollectorTest test cases
bitterpanda63 May 9, 2025
3532124
Fix EmptyAPIResponses not having correct fields
bitterpanda63 May 9, 2025
2450ba8
Statistics, baseline on new element should be 1 for hits
bitterpanda63 May 9, 2025
4dd53e8
Update StatisticsStore test cases
bitterpanda63 May 9, 2025
d460e68
Fix ServiceConfigurationTest cases
bitterpanda63 May 9, 2025
1b951ae
Remove the setting of a now useless header
bitterpanda63 May 9, 2025
8e60624
Update test cases for ReportingAPITest
bitterpanda63 May 9, 2025
5775bd9
empty commit, run build jobs
bitterpanda63 May 9, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ public ReportingApi(int timeoutInSec) {

public record APIListsResponse(
List<ListsResponseEntry> blockedIPAddresses,
List<ListsResponseEntry> monitoredIPAddresses,
List<ListsResponseEntry> allowedIPAddresses,
String blockedUserAgents
String blockedUserAgents,
String monitoredUserAgents,
List<UserAgentDetail> userAgentDetails
) {}
public record ListsResponseEntry(String source, String description, List<String> ips) {}

public record ListsResponseEntry(String key, String source, String description, List<String> ips) {
}

public record UserAgentDetail(String key, String pattern) {
}
/**
* Fetch blocked lists using a separate API call, these can include :
* -> blocked IP Addresses (e.g. geo restrictions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dev.aikido.agent_api.background.Endpoint;
import dev.aikido.agent_api.context.Context;
import dev.aikido.agent_api.context.ContextObject;
import dev.aikido.agent_api.context.RouteMetadata;
import dev.aikido.agent_api.storage.ServiceConfiguration;
import dev.aikido.agent_api.storage.statistics.StatisticsStore;

Expand Down Expand Up @@ -38,31 +39,47 @@
// Increment total hits :
StatisticsStore.incrementHits();

// Per-route IP allowlists :
List<Endpoint> matchedEndpoints = matchEndpoints(newContext.getRouteMetadata(), config.getEndpoints());
if (!ipAllowedToAccessRoute(newContext.getRemoteAddress(), matchedEndpoints)) {
Res endpointAllowlistRes = checkEndpointAllowlist(newContext.getRouteMetadata(), newContext.getRemoteAddress(), config);
Res blockedIpsRes = checkBlockedIps(newContext.getRemoteAddress(), config);
Res blockedUserAgentsRes = checkBlockedUserAgents(newContext.getHeader("user-agent"), config);

// make sure to follow a certain order when giving error messages.
if (endpointAllowlistRes != null)
return endpointAllowlistRes;
else if (blockedIpsRes != null)
return blockedIpsRes;
else return blockedUserAgentsRes;
}

private static Res checkEndpointAllowlist(RouteMetadata routeMetadata, String remoteAddress, ServiceConfiguration config) {
List<Endpoint> matchedEndpoints = matchEndpoints(routeMetadata, config.getEndpoints());
if (!ipAllowedToAccessRoute(remoteAddress, matchedEndpoints)) {
String msg = "Your IP address is not allowed to access this resource.";
msg += " (Your IP: " + newContext.getRemoteAddress() + ")";
msg += " (Your IP: " + remoteAddress + ")";
return new Res(msg, 403);
}
return null; // not blocked
}

// Blocked IP lists (e.g. Geo restrictions)
ServiceConfiguration.BlockedResult ipBlocked = config.isIpBlocked(newContext.getRemoteAddress());
private static Res checkBlockedIps(String remoteAddress, ServiceConfiguration config) {
ServiceConfiguration.BlockedResult ipBlocked = config.isIpBlocked(remoteAddress);
if (ipBlocked.blocked()) {
String msg = "Your IP address is blocked. Reason: " + ipBlocked.description();
msg += " (Your IP: " + newContext.getRemoteAddress() + ")";
msg += " (Your IP: " + remoteAddress + ")";
return new Res(msg, 403);
}
return null; // not blocked
}

// User-Agent blocking (e.g. blocking bots)
String userAgent = newContext.getHeader("user-agent");
if (userAgent != null && !userAgent.isEmpty()) {
if (config.isBlockedUserAgent(userAgent)) {
String msg = "You are not allowed to access this resource because you have been identified as a bot.";
return new Res(msg, 403);
}
private static Res checkBlockedUserAgents(String userAgent, ServiceConfiguration config) {
if (userAgent == null || userAgent.isEmpty()) {
return null; // not blocked

Check warning on line 76 in agent_api/src/main/java/dev/aikido/agent_api/collectors/WebRequestCollector.java

View check run for this annotation

Codecov / codecov/patch

agent_api/src/main/java/dev/aikido/agent_api/collectors/WebRequestCollector.java#L76

Added line #L76 was not covered by tests
}
if (config.isBlockedUserAgent(userAgent)) {
String msg = "You are not allowed to access this resource because you have been identified as a bot.";
return new Res(msg, 403);
}
return null;
return null; // not blocked
}

public record Res(String msg, Integer status) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import dev.aikido.agent_api.background.cloud.api.APIResponse;
import dev.aikido.agent_api.background.cloud.api.ReportingApi;
import dev.aikido.agent_api.helpers.net.IPList;
import dev.aikido.agent_api.storage.service_configuration.ParsedFirewallLists;
import dev.aikido.agent_api.storage.statistics.StatisticsStore;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Pattern;

import static dev.aikido.agent_api.helpers.IPListBuilder.createIPList;
import static dev.aikido.agent_api.vulnerabilities.ssrf.IsPrivateIP.isPrivateIp;
Expand All @@ -18,16 +19,13 @@
* It is essential for e.g. rate limiting
*/
public class ServiceConfiguration {
private final List<IPListEntry> blockedIps = new ArrayList<>();
private final List<IPListEntry> allowedIps = new ArrayList<>();
private final ParsedFirewallLists firewallLists = new ParsedFirewallLists();
private boolean blockingEnabled;
private boolean receivedAnyStats;
private boolean middlewareInstalled;
private IPList bypassedIPs = new IPList();
private HashSet<String> blockedUserIDs = new HashSet<>();
private List<Endpoint> endpoints = new ArrayList<>();
// User-Agent Blocking (e.g. bot blocking) :
private Pattern blockedUserAgentRegex;

public ServiceConfiguration() {
this.receivedAnyStats = true; // true by default, waiting for the startup event
Expand Down Expand Up @@ -87,68 +85,39 @@ public boolean isIpBypassed(String ip) {
* Check if the IP is blocked (e.g. Geo IP Restrictions)
*/
public BlockedResult isIpBlocked(String ip) {
BlockedResult blockedResult = new BlockedResult(false, null);

// Check for allowed ip addresses (i.e. only one country is allowed to visit the site)
// Always allow access from private IP addresses (those include local IP addresses)
if (!allowedIps.isEmpty() && !isPrivateIp(ip)) {
boolean ipAllowed = false;
for (IPListEntry entry : allowedIps) {
if (entry.ipList.matches(ip)) {
ipAllowed = true; // We allow IP addresses as long as they match with one of the lists.
break;
}
}
if (!ipAllowed) {
return new BlockedResult(true, "not in allowlist");
}
if (!isPrivateIp(ip) && !firewallLists.matchesAllowedIps(ip)) {
blockedResult = new BlockedResult(true, "not in allowlist");
}

// Check for blocked ip addresses
for (IPListEntry entry : blockedIps) {
if (entry.ipList.matches(ip)) {
return new BlockedResult(true, entry.description);
for (ParsedFirewallLists.Match match : firewallLists.matchBlockedIps(ip)) {
StatisticsStore.incrementIpHits(match.key());
// when a blocking match is found, set blocked result if it hasn't been set already.
if (match.block() && !blockedResult.blocked()) {
blockedResult = new BlockedResult(true, match.description());
}
}
return new BlockedResult(false, null);

return blockedResult;
}

public void updateBlockedLists(ReportingApi.APIListsResponse res) {
// Update blocked IP addresses (e.g. for geo restrictions) :
blockedIps.clear();
if (res.blockedIPAddresses() != null) {
for (ReportingApi.ListsResponseEntry entry : res.blockedIPAddresses()) {
IPList ipList = createIPList(entry.ips());
blockedIps.add(new IPListEntry(ipList, entry.description()));
}
}

// Update allowed IP addresses (e.g. for geo restrictions) :
allowedIps.clear();
if (res.allowedIPAddresses() != null) {
for (ReportingApi.ListsResponseEntry entry : res.allowedIPAddresses()) {
IPList ipList = createIPList(entry.ips());
this.allowedIps.add(new IPListEntry(ipList, entry.description()));
}
}

// Update Blocked User-Agents regex
blockedUserAgentRegex = null;
if (res.blockedUserAgents() != null && !res.blockedUserAgents().isEmpty()) {
this.blockedUserAgentRegex = Pattern.compile(res.blockedUserAgents(), Pattern.CASE_INSENSITIVE);
}
this.firewallLists.update(res);
}

/**
* Check if a given User-Agent is blocked or not :
*/
public boolean isBlockedUserAgent(String userAgent) {
if (blockedUserAgentRegex != null) {
return blockedUserAgentRegex.matcher(userAgent).find();
ParsedFirewallLists.UABlockedResult result = this.firewallLists.matchBlockedUserAgents(userAgent);
for (String matchedKey : result.matchedKeys()) {
StatisticsStore.incrementUAHits(matchedKey);
}
return false;
}

// IP restrictions (e.g. Geo-IP Restrictions) :
public record IPListEntry(IPList ipList, String description) {
return result.block();
}

public record BlockedResult(boolean blocked, String description) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package dev.aikido.agent_api.storage.service_configuration;

import dev.aikido.agent_api.background.cloud.api.ReportingApi;
import dev.aikido.agent_api.helpers.net.IPList;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import static dev.aikido.agent_api.helpers.IPListBuilder.createIPList;

public class ParsedFirewallLists {
private final List<IPEntry> blockedIps = new ArrayList<>();
private final List<IPEntry> allowedIps = new ArrayList<>();
private final List<UADetailsEntry> uaDetails = new ArrayList<>();
private Pattern blockedUserAgents = null;
private Pattern monitoredUserAgents = null;

public ParsedFirewallLists() {

}

public List<Match> matchBlockedIps(String ip) {
List<Match> matches = new ArrayList<>();
for (IPEntry entry : this.blockedIps) {
if (entry.ips().matches(ip)) {
matches.add(new Match(entry.key(), !entry.monitor(), entry.description()));
}
}
return matches;
}

// returns true if one or more matches has been found with allowlist.
public boolean matchesAllowedIps(String ip) {
if (this.allowedIps.isEmpty()) {
return true; // Empty allowed is means all ips match
}
for (IPEntry entry : this.allowedIps) {
if (entry.ips().matches(ip)) {
return true;

Check warning on line 40 in agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/ParsedFirewallLists.java

View check run for this annotation

Codecov / codecov/patch

agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/ParsedFirewallLists.java#L40

Added line #L40 was not covered by tests
}
}
return false;
}

public UABlockedResult matchBlockedUserAgents(String userAgent) {
boolean isBlocked = false;
if (blockedUserAgents != null)
isBlocked = blockedUserAgents.matcher(userAgent).find();

boolean isMonitored = false;
if (monitoredUserAgents != null)
isMonitored = monitoredUserAgents.matcher(userAgent).find();

if (!isMonitored && !isBlocked)
// only run the more detailed matches if it's an actual attack/monitored.
return new UABlockedResult(false, List.of());

List<String> matchedUAKeys = new ArrayList<>();
for (UADetailsEntry entry : this.uaDetails) {
if (entry.pattern().matcher(userAgent).find()) {
matchedUAKeys.add(entry.key());
}
}
return new UABlockedResult(isBlocked, matchedUAKeys);
}

public void update(ReportingApi.APIListsResponse response) {
updateBlockedIps(response.blockedIPAddresses());
updateMonitoredIps(response.monitoredIPAddresses());
updateAllowedIps(response.allowedIPAddresses());

updateBlockedAndMonitoredUAs(response.blockedUserAgents(), response.monitoredUserAgents());
updateUADetails(response.userAgentDetails());
}

public void updateBlockedIps(List<ReportingApi.ListsResponseEntry> blockedIpsList) {
blockedIps.clear();
if (blockedIpsList == null)
return;

Check warning on line 80 in agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/ParsedFirewallLists.java

View check run for this annotation

Codecov / codecov/patch

agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/ParsedFirewallLists.java#L80

Added line #L80 was not covered by tests
for (ReportingApi.ListsResponseEntry entry : blockedIpsList) {
IPList ipList = createIPList(entry.ips());
blockedIps.add(new IPEntry(/* monitor */ false, entry.key(), entry.source(), entry.description(), ipList));
}
}

public void updateMonitoredIps(List<ReportingApi.ListsResponseEntry> monitoredIpsList) {
if (monitoredIpsList == null)
return;

Check warning on line 89 in agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/ParsedFirewallLists.java

View check run for this annotation

Codecov / codecov/patch

agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/ParsedFirewallLists.java#L89

Added line #L89 was not covered by tests
for (ReportingApi.ListsResponseEntry entry : monitoredIpsList) {
IPList ipList = createIPList(entry.ips());
blockedIps.add(new IPEntry(/* monitor */ true, entry.key(), entry.source(), entry.description(), ipList));
}
}

public void updateAllowedIps(List<ReportingApi.ListsResponseEntry> allowedIpsList) {
allowedIps.clear();
if (allowedIpsList == null)
return;
for (ReportingApi.ListsResponseEntry entry : allowedIpsList) {
IPList ipList = createIPList(entry.ips());
boolean shouldMonitor = false; // we don't monitor allowed ips
allowedIps.add(new IPEntry(shouldMonitor, entry.key(), entry.source(), entry.description(), ipList));
}
}

public void updateUADetails(List<ReportingApi.UserAgentDetail> userAgentDetails) {
this.uaDetails.clear();
if (userAgentDetails == null)
return;

Check warning on line 110 in agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/ParsedFirewallLists.java

View check run for this annotation

Codecov / codecov/patch

agent_api/src/main/java/dev/aikido/agent_api/storage/service_configuration/ParsedFirewallLists.java#L110

Added line #L110 was not covered by tests
for (ReportingApi.UserAgentDetail entry : userAgentDetails) {
Pattern pattern = Pattern.compile(entry.pattern(), Pattern.CASE_INSENSITIVE);
this.uaDetails.add(new UADetailsEntry(entry.key(), pattern));
}
}

public void updateBlockedAndMonitoredUAs(String blockedUAs, String monitoredUAs) {
this.blockedUserAgents = null;
if (blockedUAs != null && !blockedUAs.isEmpty()) {
this.blockedUserAgents = Pattern.compile(blockedUAs, Pattern.CASE_INSENSITIVE);
}

this.monitoredUserAgents = null;
if (monitoredUAs != null && !monitoredUAs.isEmpty()) {
this.monitoredUserAgents = Pattern.compile(monitoredUAs, Pattern.CASE_INSENSITIVE);
}
}


public record Match(String key, boolean block, String description) {
}

public record UABlockedResult(boolean block, List<String> matchedKeys) {
}

private record IPEntry(boolean monitor, String key, String source, String description, IPList ips) {
}

private record UADetailsEntry(String key, Pattern pattern) {
}
}
Loading