Skip to content

Commit e4986e3

Browse files
authored
Merge pull request #625 from AikidoSec/report-tokenize-failure-stats
Count SQL tokenization failures
2 parents 012f010 + b73fc14 commit e4986e3

File tree

17 files changed

+266
-20
lines changed

17 files changed

+266
-20
lines changed

.github/workflows/end-to-end-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
"CLICKHOUSE_DEFAULT_ACCESS": "MANAGEMENT=1"
4646
ports:
4747
- "27019:8123"
48-
timeout-minutes: 10
48+
timeout-minutes: 15
4949
strategy:
5050
matrix:
5151
node-version: [18.x]

.github/workflows/unit-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
ports:
5353
- "27019:8123"
5454
mongodb-replica:
55-
image: bitnami/mongodb:8.0
55+
image: bitnami/mongodb:4.4
5656
env:
5757
MONGODB_ADVERTISED_HOSTNAME: 127.0.0.1
5858
MONGODB_REPLICA_SET_MODE: primary

end2end/server/src/zen/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function generateConfig(app) {
99
endpoints: [],
1010
blockedUserIds: [],
1111
allowedIPAddresses: [],
12-
receivedAnyStats: true,
12+
receivedAnyStats: false,
1313
};
1414
}
1515

end2end/server/src/zen/events.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
const events = new Map();
22

33
function captureEvent(event, app) {
4-
if (event.type === "heartbeat") {
5-
// Ignore heartbeats
6-
return;
7-
}
8-
94
if (!events.has(app.id)) {
105
events.set(app.id, []);
116
}

end2end/tests/express-mysql.test.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ const pathToApp = resolve(
99
"app.js"
1010
);
1111

12+
const testServerUrl = "http://localhost:5874";
13+
14+
let token;
15+
t.beforeEach(async () => {
16+
const response = await fetch(`${testServerUrl}/api/runtime/apps`, {
17+
method: "POST",
18+
});
19+
const body = await response.json();
20+
token = body.token;
21+
});
22+
1223
t.test("it blocks in blocking mode", (t) => {
1324
const server = spawn(`node`, [pathToApp, "4000"], {
1425
env: { ...process.env, AIKIDO_DEBUG: "true", AIKIDO_BLOCKING: "true" },
@@ -144,3 +155,86 @@ t.test("it does not block in dry mode", (t) => {
144155
server.kill();
145156
});
146157
});
158+
159+
t.setTimeout(80000);
160+
161+
t.test(
162+
"it increments sqlTokenizationFailures counter for invalid SQL queries",
163+
(t) => {
164+
const server = spawn(`node`, [pathToApp, "4003"], {
165+
env: {
166+
...process.env,
167+
AIKIDO_DEBUG: "true",
168+
AIKIDO_BLOCKING: "true",
169+
AIKIDO_TOKEN: token,
170+
AIKIDO_ENDPOINT: testServerUrl,
171+
AIKIDO_REALTIME_ENDPOINT: testServerUrl,
172+
},
173+
});
174+
175+
server.on("close", () => {
176+
t.end();
177+
});
178+
179+
server.on("error", (err) => {
180+
t.fail(err);
181+
});
182+
183+
let stdout = "";
184+
server.stdout.on("data", (data) => {
185+
stdout += data.toString();
186+
});
187+
188+
let stderr = "";
189+
server.stderr.on("data", (data) => {
190+
stderr += data.toString();
191+
});
192+
193+
// Wait for the server to start
194+
timeout(2000)
195+
.then(() => {
196+
return fetch(
197+
`http://localhost:4003/invalid-query?sql=${encodeURIComponent("SELECT * FROM test")}`,
198+
{
199+
signal: AbortSignal.timeout(5000),
200+
method: "POST",
201+
}
202+
);
203+
})
204+
.then((response) => {
205+
return response.text();
206+
})
207+
.then((responseText) => {
208+
t.match(responseText, /You have an error in your SQL syntax/);
209+
210+
// Wait for the heartbeat event to be sent
211+
return timeout(60000);
212+
})
213+
.then(() => {
214+
return fetch(`${testServerUrl}/api/runtime/events`, {
215+
method: "GET",
216+
headers: {
217+
Authorization: token,
218+
},
219+
signal: AbortSignal.timeout(5000),
220+
});
221+
})
222+
.then((response) => {
223+
return response.json();
224+
})
225+
.then((events) => {
226+
const heartbeatEvents = events.filter(
227+
(event) => event.type === "heartbeat"
228+
);
229+
t.same(heartbeatEvents.length, 1);
230+
const [heartbeat] = heartbeatEvents;
231+
t.equal(heartbeat.stats.sqlTokenizationFailures, 1);
232+
})
233+
.catch((error) => {
234+
t.error(error);
235+
})
236+
.finally(() => {
237+
server.kill();
238+
});
239+
}
240+
);

library/agent/Agent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ export class Agent {
318318
requests: stats.requests,
319319
userAgents: stats.userAgents,
320320
ipAddresses: stats.ipAddresses,
321+
sqlTokenizationFailures: stats.sqlTokenizationFailures,
321322
},
322323
packages,
323324
hostnames: outgoingDomains,

library/agent/InspectionStatistics.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ t.test("it resets stats", async () => {
4848
ipAddresses: {
4949
breakdown: {},
5050
},
51+
sqlTokenizationFailures: 0,
5152
});
5253

5354
clock.tick(1000);
@@ -69,6 +70,7 @@ t.test("it resets stats", async () => {
6970
ipAddresses: {
7071
breakdown: {},
7172
},
73+
sqlTokenizationFailures: 0,
7274
});
7375

7476
clock.uninstall();
@@ -101,6 +103,7 @@ t.test("it keeps track of amount of calls", async () => {
101103
ipAddresses: {
102104
breakdown: {},
103105
},
106+
sqlTokenizationFailures: 0,
104107
});
105108

106109
stats.onInspectedCall({
@@ -141,6 +144,7 @@ t.test("it keeps track of amount of calls", async () => {
141144
ipAddresses: {
142145
breakdown: {},
143146
},
147+
sqlTokenizationFailures: 0,
144148
});
145149

146150
stats.onInspectedCall({
@@ -181,6 +185,7 @@ t.test("it keeps track of amount of calls", async () => {
181185
ipAddresses: {
182186
breakdown: {},
183187
},
188+
sqlTokenizationFailures: 0,
184189
});
185190

186191
stats.interceptorThrewError("mongodb.query", "nosql_op");
@@ -214,6 +219,7 @@ t.test("it keeps track of amount of calls", async () => {
214219
ipAddresses: {
215220
breakdown: {},
216221
},
222+
sqlTokenizationFailures: 0,
217223
});
218224

219225
stats.onInspectedCall({
@@ -254,6 +260,7 @@ t.test("it keeps track of amount of calls", async () => {
254260
ipAddresses: {
255261
breakdown: {},
256262
},
263+
sqlTokenizationFailures: 0,
257264
});
258265

259266
stats.onInspectedCall({
@@ -294,6 +301,7 @@ t.test("it keeps track of amount of calls", async () => {
294301
ipAddresses: {
295302
breakdown: {},
296303
},
304+
sqlTokenizationFailures: 0,
297305
});
298306

299307
t.same(stats.hasCompressedStats(), false);
@@ -353,6 +361,7 @@ t.test("it keeps track of amount of calls", async () => {
353361
ipAddresses: {
354362
breakdown: {},
355363
},
364+
sqlTokenizationFailures: 0,
356365
});
357366

358367
t.ok(
@@ -409,6 +418,7 @@ t.test("it keeps track of requests", async () => {
409418
ipAddresses: {
410419
breakdown: {},
411420
},
421+
sqlTokenizationFailures: 0,
412422
});
413423

414424
stats.onRequest();
@@ -430,6 +440,7 @@ t.test("it keeps track of requests", async () => {
430440
ipAddresses: {
431441
breakdown: {},
432442
},
443+
sqlTokenizationFailures: 0,
433444
});
434445

435446
stats.onRequest();
@@ -452,6 +463,7 @@ t.test("it keeps track of requests", async () => {
452463
ipAddresses: {
453464
breakdown: {},
454465
},
466+
sqlTokenizationFailures: 0,
455467
});
456468

457469
stats.onRequest();
@@ -474,6 +486,7 @@ t.test("it keeps track of requests", async () => {
474486
ipAddresses: {
475487
breakdown: {},
476488
},
489+
sqlTokenizationFailures: 0,
477490
});
478491

479492
clock.tick(1000);
@@ -497,6 +510,7 @@ t.test("it keeps track of requests", async () => {
497510
ipAddresses: {
498511
breakdown: {},
499512
},
513+
sqlTokenizationFailures: 0,
500514
});
501515

502516
clock.uninstall();
@@ -527,6 +541,7 @@ t.test("it force compresses stats", async () => {
527541
ipAddresses: {
528542
breakdown: {},
529543
},
544+
sqlTokenizationFailures: 0,
530545
});
531546

532547
stats.onRequest();
@@ -576,6 +591,7 @@ t.test("it keeps track of aborted requests", async () => {
576591
ipAddresses: {
577592
breakdown: {},
578593
},
594+
sqlTokenizationFailures: 0,
579595
});
580596

581597
clock.uninstall();
@@ -614,6 +630,7 @@ t.test("it keeps track of matched IPs and user agents", async () => {
614630
"known_threat_actors/public_scanners": 1,
615631
},
616632
},
633+
sqlTokenizationFailures: 0,
617634
});
618635

619636
// Test multiple occurrences
@@ -642,6 +659,7 @@ t.test("it keeps track of matched IPs and user agents", async () => {
642659
"known_threat_actors/public_scanners": 2,
643660
},
644661
},
662+
sqlTokenizationFailures: 0,
645663
});
646664

647665
clock.uninstall();
@@ -713,6 +731,7 @@ t.test("it keeps track of multiple operations of the same kind", async () => {
713731
ipAddresses: {
714732
breakdown: {},
715733
},
734+
sqlTokenizationFailures: 0,
716735
});
717736

718737
// Test that each operation maintains its own stats
@@ -774,6 +793,7 @@ t.test("it keeps track of multiple operations of the same kind", async () => {
774793
ipAddresses: {
775794
breakdown: {},
776795
},
796+
sqlTokenizationFailures: 0,
777797
});
778798

779799
clock.uninstall();
@@ -818,6 +838,40 @@ t.test("it handles empty operation strings", async () => {
818838
ipAddresses: {
819839
breakdown: {},
820840
},
841+
sqlTokenizationFailures: 0,
842+
});
843+
844+
clock.uninstall();
845+
});
846+
847+
t.test("it increments sqlTokenizationFailures", async () => {
848+
const clock = FakeTimers.install();
849+
850+
const stats = new InspectionStatistics({
851+
maxPerfSamplesInMemory: 50,
852+
maxCompressedStatsInMemory: 5,
853+
});
854+
855+
stats.onSqlTokenizationFailure();
856+
857+
t.same(stats.getStats(), {
858+
operations: {},
859+
startedAt: 0,
860+
requests: {
861+
total: 0,
862+
aborted: 0,
863+
attacksDetected: {
864+
total: 0,
865+
blocked: 0,
866+
},
867+
},
868+
userAgents: {
869+
breakdown: {},
870+
},
871+
ipAddresses: {
872+
breakdown: {},
873+
},
874+
sqlTokenizationFailures: 1,
821875
});
822876

823877
clock.uninstall();

library/agent/InspectionStatistics.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class InspectionStatistics {
3939
private operations: Record<string, OperationStats> = {};
4040
private readonly maxPerfSamplesInMemory: number;
4141
private readonly maxCompressedStatsInMemory: number;
42+
private sqlTokenizationFailures: number = 0;
4243
private requests: {
4344
total: number;
4445
aborted: number;
@@ -97,11 +98,13 @@ export class InspectionStatistics {
9798
breakdown: {},
9899
};
99100
this.startedAt = Date.now();
101+
this.sqlTokenizationFailures = 0;
100102
}
101103

102104
getStats(): {
103105
operations: Record<string, OperationStatsWithoutTimings>;
104106
startedAt: number;
107+
sqlTokenizationFailures: number;
105108
requests: {
106109
total: number;
107110
aborted: number;
@@ -136,6 +139,7 @@ export class InspectionStatistics {
136139
return {
137140
operations: operations,
138141
startedAt: this.startedAt,
142+
sqlTokenizationFailures: this.sqlTokenizationFailures,
139143
requests: this.requests,
140144
userAgents: this.userAgents,
141145
ipAddresses: this.ipAddresses,
@@ -299,4 +303,8 @@ export class InspectionStatistics {
299303
this.compressPerfSamples(kind as OperationKind);
300304
}
301305
}
306+
307+
onSqlTokenizationFailure() {
308+
this.sqlTokenizationFailures += 1;
309+
}
302310
}

library/agent/api/Event.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ type Heartbeat = {
9797
operations: Record<string, OperationStats>;
9898
startedAt: number;
9999
endedAt: number;
100+
sqlTokenizationFailures: number;
100101
requests: {
101102
total: number;
102103
aborted: number;

0 commit comments

Comments
 (0)