Skip to content

Commit

Permalink
Merge branch 'patch-perf' of github.com:AikidoSec/node-RASP into patc…
Browse files Browse the repository at this point in the history
…h-bench

* 'patch-perf' of github.com:AikidoSec/node-RASP:
  Add 99th percentile
  Add attacks detected / blocked to stats (if running in dry mode)
  • Loading branch information
hansott committed Mar 5, 2024
2 parents 1b86801 + b952ad1 commit 2f9f2aa
Show file tree
Hide file tree
Showing 19 changed files with 286 additions and 222 deletions.
3 changes: 2 additions & 1 deletion library/src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { getAgentVersion } from "../helpers/getAgentVersion";
import { ip } from "../helpers/ipAddress";
import { filterEmptyRequestHeaders } from "../helpers/filterEmptyRequestHeaders";
import { API } from "./api/API";
import { AgentInfo, Kind } from "./api/Event";
import { AgentInfo } from "./api/Event";
import { Token } from "./api/Token";
import { Kind } from "./Attack";
import { Context } from "./Context";
import { InspectionStatistics } from "./InspectionStatistics";
import { Logger } from "./logger/Logger";
Expand Down
10 changes: 10 additions & 0 deletions library/src/agent/Attack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type Kind = "nosql_injection" | "sql_injection";

export function attackKindHumanName(kind: Kind) {
switch (kind) {
case "nosql_injection":
return "NoSQL injection";
case "sql_injection":
return "SQL injection";
}
}
93 changes: 64 additions & 29 deletions library/src/agent/InspectionStatistics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ t.test("it keeps track of amount of calls", async () => {

stats.onInspectedCall({
module: "mongodb",
withoutContext: false,
blocked: false,
durationInMs: 0.1,
attackDetected: false,
});

t.same(stats.getStats(), {
mongodb: {
blocked: 0,
allowed: 1,
attacksDetected: {
total: 0,
blocked: 0,
},
interceptorThrewError: 0,
withoutContext: 0,
total: 1,
averageInMS: 0.1,
Expand All @@ -27,19 +30,20 @@ t.test("it keeps track of amount of calls", async () => {
75: 0.1,
90: 0.1,
95: 0.1,
99: 0.1,
},
},
});

stats.onInspectedCall({
module: "mongodb",
withoutContext: true,
});
stats.inspectedCallWithoutContext("mongodb");

t.same(stats.getStats(), {
mongodb: {
blocked: 0,
allowed: 2,
attacksDetected: {
total: 0,
blocked: 0,
},
interceptorThrewError: 0,
withoutContext: 1,
total: 2,
averageInMS: 0.1,
Expand All @@ -48,52 +52,83 @@ t.test("it keeps track of amount of calls", async () => {
75: 0.1,
90: 0.1,
95: 0.1,
99: 0.1,
},
},
});

stats.interceptorThrewError("mongodb");

t.same(stats.getStats(), {
mongodb: {
attacksDetected: {
total: 0,
blocked: 0,
},
interceptorThrewError: 1,
withoutContext: 1,
total: 3,
averageInMS: 0.1,
percentiles: {
50: 0.1,
75: 0.1,
90: 0.1,
95: 0.1,
99: 0.1,
},
},
});

stats.onInspectedCall({
module: "mongodb",
withoutContext: false,
blocked: true,
durationInMs: 0.5,
blocked: false,
durationInMs: 0.1,
attackDetected: true,
});

t.same(stats.getStats(), {
mongodb: {
blocked: 1,
allowed: 2,
attacksDetected: {
total: 1,
blocked: 0,
},
interceptorThrewError: 1,
withoutContext: 1,
total: 3,
averageInMS: 0.3,
total: 4,
averageInMS: 0.1,
percentiles: {
50: 0.1,
75: 0.5,
90: 0.5,
95: 0.5,
75: 0.1,
90: 0.1,
95: 0.1,
99: 0.1,
},
},
});

stats.onInspectedCall({
module: "mongodb",
withoutContext: false,
blocked: true,
durationInMs: 0.3,
attackDetected: true,
});

t.same(stats.getStats(), {
mongodb: {
blocked: 2,
allowed: 2,
attacksDetected: {
total: 2,
blocked: 1,
},
interceptorThrewError: 1,
withoutContext: 1,
total: 4,
averageInMS: 0.3,
total: 5,
averageInMS: 0.16666666666666666,
percentiles: {
50: 0.3,
75: 0.5,
90: 0.5,
95: 0.5,
50: 0.1,
75: 0.3,
90: 0.3,
95: 0.3,
99: 0.3,
},
},
});
Expand All @@ -103,9 +138,9 @@ t.test("it keeps track of amount of calls", async () => {
for (let i = 0; i < 50; i++) {
stats.onInspectedCall({
module: "mongodb",
withoutContext: false,
blocked: false,
durationInMs: 0.1,
attackDetected: false,
});
}

Expand Down
83 changes: 51 additions & 32 deletions library/src/agent/InspectionStatistics.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,21 @@
import { percentiles } from "../helpers/percentiles";

type ModuleStats = {
blocked: number;
allowed: number;
withoutContext: number;
total: number;
timings: number[];
interceptorThrewError: number;
attacksDetected: {
total: number;
blocked: number;
};
};

type ModuleStatsEnriched = {
averageInMS: number;
percentiles: Record<string, number>;
} & Omit<ModuleStats, "timings">;

type InspectCallArguments =
| {
module: string;
withoutContext: false;
blocked: boolean;
durationInMs: number;
}
| {
module: string;
withoutContext: true;
};

export class InspectionStatistics {
private stats: Record<string, ModuleStats> = {};
private readonly maxTimings: number;
Expand All @@ -51,58 +42,86 @@ export class InspectionStatistics {

stats[module] = {
total: moduleStats.total,
blocked: moduleStats.blocked,
allowed: moduleStats.allowed,
attacksDetected: {
total: moduleStats.attacksDetected.total,
blocked: moduleStats.attacksDetected.blocked,
},
interceptorThrewError: moduleStats.interceptorThrewError,
withoutContext: moduleStats.withoutContext,
averageInMS,
percentiles: {},
};

if (timings.length > 0) {
const [p50, p75, p90, p95] = percentiles([50, 75, 90, 99], timings);
const [p50, p75, p90, p95, p99] = percentiles(
[50, 75, 90, 95, 99],
timings
);
stats[module].percentiles = {
"50": p50,
"75": p75,
"90": p90,
"95": p95,
"99": p99,
};
}
}

return stats;
}

onInspectedCall(args: InspectCallArguments) {
const { module } = args;

private ensureModuleStats(module: string) {
if (!this.stats[module]) {
this.stats[module] = {
blocked: 0,
allowed: 0,
withoutContext: 0,
total: 0,
timings: [],
interceptorThrewError: 0,
attacksDetected: {
total: 0,
blocked: 0,
},
};
}
}

inspectedCallWithoutContext(module: string) {
this.ensureModuleStats(module);
this.stats[module].total += 1;
this.stats[module].withoutContext += 1;
}

if (args.withoutContext) {
this.stats[module].withoutContext += 1;
this.stats[module].allowed += 1;
return;
}
interceptorThrewError(module: string) {
this.ensureModuleStats(module);
this.stats[module].total += 1;
this.stats[module].interceptorThrewError += 1;
}

this.stats[module].timings.push(args.durationInMs);
onInspectedCall({
module,
blocked,
attackDetected,
durationInMs,
}: {
module: string;
durationInMs: number;
attackDetected: boolean;
blocked: boolean;
}) {
this.ensureModuleStats(module);

this.stats[module].total += 1;
this.stats[module].timings.push(durationInMs);

if (this.stats[module].timings.length > this.maxTimings) {
this.stats[module].timings.shift();
}

if (args.blocked) {
this.stats[module].blocked += 1;
} else {
this.stats[module].allowed += 1;
if (attackDetected) {
this.stats[module].attacksDetected.total += 1;
if (blocked) {
this.stats[module].attacksDetected.blocked += 1;
}
}
}
}
2 changes: 1 addition & 1 deletion library/src/agent/Source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type Source = "query" | "body" | "headers" | "cookies";
* @param source A source type (either "query", "body", "headers" or "cookies")
* @returns A friendly name for each of these types
*/
export function friendlyName(source: Source): string {
export function sourceHumanName(source: Source): string {
switch (source) {
case "query":
return "query parameters";
Expand Down
9 changes: 5 additions & 4 deletions library/src/agent/api/Event.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Kind } from "../Attack";
import { Source } from "../Source";

export type AgentInfo = {
Expand All @@ -21,8 +22,6 @@ type Started = {
time: number;
};

export type Kind = "nosql_injection" | "sql_injection";

type DetectedAttack = {
type: "detected_attack";
request: {
Expand Down Expand Up @@ -51,8 +50,10 @@ type ModuleName = string;
type Stats = Record<
ModuleName,
{
blocked: number;
allowed: number;
attacksDetected: {
total: number;
blocked: number;
};
withoutContext: number;
total: number;
averageInMS: number;
Expand Down
20 changes: 0 additions & 20 deletions library/src/agent/applyHooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,6 @@ t.test("it adds try/catch around the wrapped method", async (t) => {
connection.modifyArguments("execute", () => {
throw new Error("THIS SHOULD BE CATCHED");
});
connection.inspect("ping", () => {
throw new Error("Aikido guard has blocked a SQL injection");
});
connection.modifyArguments("rollback", () => {
throw new Error("Aikido guard has blocked a SQL injection");
});

const { agent, logger } = createAgent();
t.same(applyHooks(hooks, agent), {
Expand Down Expand Up @@ -142,19 +136,5 @@ t.test("it adds try/catch around the wrapped method", async (t) => {
'Internal error in module "mysql2" in method "execute"',
]);

const error = await t.rejects(() =>
runWithContext(context, () => actualConnection.ping())
);
if (error instanceof Error) {
t.equal(error.message, "Aikido guard has blocked a SQL injection");
}

const error2 = await t.rejects(() =>
runWithContext(context, () => actualConnection.rollback())
);
if (error2 instanceof Error) {
t.equal(error2.message, "Aikido guard has blocked a SQL injection");
}

await actualConnection.end();
});
Loading

0 comments on commit 2f9f2aa

Please sign in to comment.