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
46 changes: 46 additions & 0 deletions library/agent/Agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1278,3 +1278,49 @@ t.test("attack wave detected event", async (t) => {
},
]);
});

t.test(
"attack reported by sink increases attack wave suspicious count",
async (t) => {
const logger = new LoggerNoop();
const api = new ReportingAPIForTesting();
const agent = createTestAgent({
api,
logger,
token: new Token("123"),
});

t.same(agent.getAttackWaveDetector().getSuspiciousCount("::1"), 0);

agent.onDetectedAttack({
module: "mongodb",
kind: "nosql_injection",
blocked: true,
source: "body",
request: {
method: "POST",
cookies: {},
query: {},
headers: {
"user-agent": "agent",
},
body: "payload",
url: "http://localhost:4000",
remoteAddress: "::1",
source: "express",
route: "/posts/:id",
routeParams: {},
},
operation: "operation",
payload: { $gt: "" },
stack: "stack",
paths: [".nested"],
metadata: {
db: "app",
},
});

t.same(agent.getAttackWaveDetector().getSuspiciousCount("::1"), 1);
t.same(agent.getAttackWaveDetector().getSuspiciousCount("::2"), 0);
}
);
4 changes: 4 additions & 0 deletions library/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ export class Agent {
blocked,
});

if (request.remoteAddress) {
this.attackWaveDetector.increaseSuspiciousCount(request.remoteAddress);
}

this.attackLogger.log(attack);

if (this.token) {
Expand Down
10 changes: 7 additions & 3 deletions library/sources/http-server/createRequestListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,13 @@ function createOnFinishRequestHandler(
agent.onRouteRateLimited(context.rateLimitedEndpoint);
}

if (agent.getAttackWaveDetector().check(context)) {
agent.onDetectedAttackWave({ request: context, metadata: {} });
agent.getInspectionStatistics().onAttackWaveDetected();
if (context.remoteAddress) {
agent.getAttackWaveDetector().check(context);

if (agent.getAttackWaveDetector().shouldReport(context.remoteAddress)) {
agent.onDetectedAttackWave({ request: context, metadata: {} });
agent.getInspectionStatistics().onAttackWaveDetected();
}
}
}
};
Expand Down
10 changes: 7 additions & 3 deletions library/sources/http-server/http2/createStreamListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,13 @@ function discoverRouteFromStream(
agent.onRouteRateLimited(context.rateLimitedEndpoint);
}

if (agent.getAttackWaveDetector().check(context)) {
agent.onDetectedAttackWave({ request: context, metadata: {} });
agent.getInspectionStatistics().onAttackWaveDetected();
if (context.remoteAddress) {
agent.getAttackWaveDetector().check(context);

if (agent.getAttackWaveDetector().shouldReport(context.remoteAddress)) {
agent.onDetectedAttackWave({ request: context, metadata: {} });
agent.getInspectionStatistics().onAttackWaveDetected();
}
}
}
}
Expand Down
190 changes: 131 additions & 59 deletions library/vulnerabilities/attack-wave-detection/AttackWaveDetector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,118 +35,190 @@ function newAttackWaveDetector() {

t.test("no ip address", async (t) => {
const detector = newAttackWaveDetector();
t.notOk(detector.check(getTestContext(undefined, "/wp-config.php", "GET")));
detector.check(getTestContext(undefined, "/wp-config.php", "GET"));
});

t.test("not a web scanner", async (t) => {
const detector = newAttackWaveDetector();
t.notOk(detector.check(getTestContext("::1", "/", "OPTIONS")));
t.notOk(detector.check(getTestContext("::1", "/", "GET")));
t.notOk(detector.check(getTestContext("::1", "/login", "GET")));
t.notOk(detector.check(getTestContext("::1", "/dashboard", "GET")));
t.notOk(detector.check(getTestContext("::1", "/dashboard/2", "GET")));
t.notOk(detector.check(getTestContext("::1", "/settings", "GET")));
t.notOk(detector.check(getTestContext("::1", "/", "GET")));
t.notOk(detector.check(getTestContext("::1", "/dashboard", "GET")));

t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/", "OPTIONS"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/login", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/dashboard", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/dashboard/2", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/settings", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/dashboard", "GET"));
t.notOk(detector.shouldReport("::1"));

t.notOk(detector.shouldReport("::2"));
});

t.test("a web scanner", async (t) => {
const detector = newAttackWaveDetector();
t.notOk(detector.check(getTestContext("::1", "/wp-config.php", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php.bak", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.git/config", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.htaccess", "GET")));
detector.check(getTestContext("::1", "/wp-config.php", "GET"));
detector.check(getTestContext("::1", "/wp-config.php.bak", "GET"));
detector.check(getTestContext("::1", "/.git/config", "GET"));
detector.check(getTestContext("::1", "/.env", "GET"));
detector.check(getTestContext("::1", "/.htaccess", "GET"));
// Is true because the threshold is 6
t.ok(detector.check(getTestContext("::1", "/.htpasswd", "GET")));
detector.check(getTestContext("::1", "/.htpasswd", "GET"));
t.ok(detector.shouldReport("::1"));

// False again because event should have been sent last time
t.notOk(detector.check(getTestContext("::1", "/.htpasswd", "GET")));
detector.check(getTestContext("::1", "/.htpasswd", "GET"));
});

t.test("a web scanner with delays", async (t) => {
const clock = FakeTimers.install();
const detector = newAttackWaveDetector();
t.notOk(detector.check(getTestContext("::1", "/wp-config.php", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php.bak", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.git/config", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
detector.check(getTestContext("::1", "/wp-config.php", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php.bak", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.git/config", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));

clock.tick(30 * 1000);

t.notOk(detector.check(getTestContext("::1", "/.htaccess", "GET")));
detector.check(getTestContext("::1", "/.htaccess", "GET"));
t.notOk(detector.shouldReport("::1"));

// Is true because the threshold is 6
t.ok(detector.check(getTestContext("::1", "/.htpasswd", "GET")));
detector.check(getTestContext("::1", "/.htpasswd", "GET"));
t.ok(detector.shouldReport("::1"));
// False again because event should have been sent last time
t.notOk(detector.check(getTestContext("::1", "/.htpasswd", "GET")));
detector.check(getTestContext("::1", "/.htpasswd", "GET"));
t.notOk(detector.shouldReport("::1"));

clock.tick(30 * 60 * 1000);

// Still false because minimum time between events is 1 hour
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php.bak", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.git/config", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.htaccess", "GET")));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php.bak", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.git/config", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.htaccess", "GET"));
t.notOk(detector.shouldReport("::1"));

clock.tick(32 * 60 * 1000);

// Should resend event after 1 hour
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php.bak", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.git/config", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.ok(detector.check(getTestContext("::1", "/.htaccess", "GET")));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php.bak", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.git/config", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.htaccess", "GET"));
t.ok(detector.shouldReport("::1"));

clock.uninstall();
});

t.test("a slow web scanner that triggers in the second interval", async (t) => {
const clock = FakeTimers.install();
const detector = newAttackWaveDetector();
t.notOk(detector.check(getTestContext("::1", "/wp-config.php", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php.bak", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.git/config", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php.bak", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.git/config", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));

clock.tick(62 * 1000);

t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php.bak", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.git/config", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.ok(detector.check(getTestContext("::1", "/.htaccess", "GET")));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php.bak", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.git/config", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.htaccess", "GET"));
t.ok(detector.shouldReport("::1"));

clock.uninstall();
});

t.test("a slow web scanner that triggers in the third interval", async (t) => {
const clock = FakeTimers.install();
const detector = newAttackWaveDetector();
t.notOk(detector.check(getTestContext("::1", "/wp-config.php", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php.bak", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.git/config", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php.bak", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.git/config", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));

clock.tick(62 * 1000);

// Still false because minimum time between events is 1 hour
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php.bak", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.git/config", "GET")));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php.bak", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.git/config", "GET"));
t.notOk(detector.shouldReport("::1"));

clock.tick(62 * 1000);

// Should resend event after 1 hour
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php", "GET")));
t.notOk(detector.check(getTestContext("::1", "/wp-config.php.bak", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.git/config", "GET")));
t.notOk(detector.check(getTestContext("::1", "/.env", "GET")));
t.ok(detector.check(getTestContext("::1", "/.htaccess", "GET")));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/wp-config.php.bak", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.git/config", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.env", "GET"));
t.notOk(detector.shouldReport("::1"));
detector.check(getTestContext("::1", "/.htaccess", "GET"));
t.ok(detector.shouldReport("::1"));

clock.uninstall();
});

t.test("increase attack count manually", async (t) => {
const detector = newAttackWaveDetector();

for (let i = 0; i < 6; i++) {
t.notOk(detector.shouldReport("::1"));
detector.increaseSuspiciousCount("::1");
}
t.ok(detector.shouldReport("::1"));

t.same(detector.getSuspiciousCount("::1"), 6);
});
Loading
Loading