Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,20 @@ public void run() {
ReportingApiHTTP api = new ReportingApiHTTP(getAikidoAPIEndpoint(token), API_TIMEOUT, EVENTS_API_TIMEOUT, token);
RealtimeAPI realtimeApi = new RealtimeAPI(token);

// make api calls on start
Optional<APIResponse> res = api.report(Started.get());
res.ifPresent(ServiceConfigStore::updateFromAPIResponse);
// Fetch blocked lists using separate API call
Optional<ReportingApi.APIListsResponse> listsRes = api.fetchBlockedLists();
listsRes.ifPresent(ServiceConfigStore::updateFromAPIListsResponse);


// Schedule tasks using ScheduledExecutorService
scheduler.scheduleAtFixedRate(new RealtimeTask(realtimeApi, api), POLLING_INTERVAL_SECONDS, POLLING_INTERVAL_SECONDS, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(new AttackQueueConsumerTask(api), 0, 2, TimeUnit.SECONDS);

// Start heartbeats
scheduler.schedule(createHeartbeatTask(api, scheduler, true), FIRST_HEARTBEAT_INTERVAL_SECONDS, TimeUnit.SECONDS);

// make api calls on start, slow responses should not delay the tasks above
Optional<APIResponse> res = api.report(Started.get());
res.ifPresent(ServiceConfigStore::updateFromAPIResponse);
// Fetch blocked lists using separate API call

@aikido-pr-checks aikido-pr-checks Bot Oct 19, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment that blocked lists use a separate API call restates api.fetchBlockedLists() without adding context.

Suggested change
// Fetch blocked lists using separate API call
Details

✨ AI Reasoning
​The comment repeats what the adjacent api.fetchBlockedLists() call already shows and gives no reason for making it a separate call. The other added comment explains that slow responses should not delay scheduled tasks, so it provides rationale.

Reply on this comment to give feedback or ignore the issue.
More info

Optional<ReportingApi.APIListsResponse> listsRes = api.fetchBlockedLists();
listsRes.ifPresent(ServiceConfigStore::updateFromAPIListsResponse);
Comment on lines +56 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High - Concurrent startup refreshes can overwrite newer blocking rules

When the startup event or list request takes longer than the newly scheduled 30/60-second tasks, HeartbeatTask or RealtimeTask can fetch a newer configuration while the original startup responses are still in flight. The startup responses are then applied without version checks, and a late list response replaces the entire list set, so newly enabled IP/UA blocks or blocking mode can be reverted and matching requests or attacks can pass until a later refresh.

Show fix

Keep startup fetches serialized ahead of scheduled refreshes, or make configuration and list updates version-aware so an older response cannot replace a newer snapshot. Do not let the initial list response clear newer rules based solely on arrival order.

More info - Reply on this comment to give feedback or ignore the issue.


if (token != null) {
if (FeatureFlags.AIKIDO_FEATURE_SSE.isEnabled() || ServiceConfigStore.isRealtimeUpdatesEnabled()) {
new RealtimeSSETask(new RealtimeSSEAPI(token), api).start();
Expand Down
18 changes: 18 additions & 0 deletions agent_api/src/test/java/background/HeartbeatSchedulingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,24 @@ void sendsTwoEarlyHeartbeatsThenUsesConfiguredInterval(long interval, boolean re
}
}

@Test
void schedulesTasksBeforeSendingStartEvent() {
try (var executors = mockStatic(Executors.class);
var clients = mockConstruction(ReportingApiHTTP.class, (client, context) ->
when(client.report(any())).thenAnswer(invocation -> {
verify(scheduler).scheduleAtFixedRate(any(RealtimeTask.class), eq(60L), eq(60L), eq(TimeUnit.SECONDS));
verify(scheduler).scheduleAtFixedRate(any(AttackQueueConsumerTask.class), eq(0L), eq(2L), eq(TimeUnit.SECONDS));
verify(scheduler).schedule(any(Runnable.class), eq(30L), eq(TimeUnit.SECONDS));
return Optional.empty();
}))) {
executors.when(() -> Executors.newScheduledThreadPool(3)).thenReturn(scheduler);

new BackgroundProcess("startup-order-test", new Token("token")).run();

verify(clients.constructed().get(0)).report(any());
}
}

@Test
void keepsSecondHeartbeatDelayWhenFirstResponseUpdatesInterval() {
when(api.report(any())).thenReturn(Optional.of(config("\"heartbeatIntervalInMS\":300000")));
Expand Down
Loading