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
4 changes: 2 additions & 2 deletions library/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,10 +333,10 @@ export class Agent {
response.domains &&
Array.isArray(response.domains)
) {
this.serviceConfig.setBlockNewOutgoingRequests(
this.serviceConfig.updateDomains(
response.domains,
response.blockNewOutgoingRequests
);
this.serviceConfig.updateDomains(response.domains);
}
}
}
Expand Down
175 changes: 175 additions & 0 deletions library/agent/OutgoingDomains.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import * as t from "tap";
import { OutgoingDomains } from "./OutgoingDomains";

t.test("does not block by default", async (t) => {
const outgoingDomains = new OutgoingDomains();
t.equal(outgoingDomains.shouldBlockOutgoingRequest("example.com"), false);
});

t.test("blocks domains with block mode", async (t) => {
const outgoingDomains = new OutgoingDomains([
{ hostname: "blocked.com", mode: "block" },
]);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("blocked.com"), true);
});

t.test("allows domains with allow mode", async (t) => {
const outgoingDomains = new OutgoingDomains([
{ hostname: "allowed.com", mode: "allow" },
]);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("allowed.com"), false);
});

t.test(
"blocks unknown domains when blockNewOutgoingRequests is true",
async (t) => {
const outgoingDomains = new OutgoingDomains([], true);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("unknown.com"), true);
}
);

t.test(
"allows known domains even when blockNewOutgoingRequests is true",
async (t) => {
const outgoingDomains = new OutgoingDomains(
[{ hostname: "allowed.com", mode: "allow" }],
true
);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("allowed.com"), false);
}
);

t.test(
"blocks unknown domains but allows known allowed domains when blockNewOutgoingRequests is true",
async (t) => {
const outgoingDomains = new OutgoingDomains(
[
{ hostname: "allowed.com", mode: "allow" },
{ hostname: "blocked.com", mode: "block" },
],
true
);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("unknown.com"), true);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("allowed.com"), false);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("blocked.com"), true);
}
);

t.test(
"blocks wildcard domains if new outgoing requests are not blocked",
async (t) => {
const outgoingDomains = new OutgoingDomains([
{ hostname: "*.example.com", mode: "block" },
{ hostname: "allowed.com", mode: "allow" },
]);

t.equal(
outgoingDomains.shouldBlockOutgoingRequest("sub.example.com"),
true
);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("example.com"), false);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("allowed.com"), false);
}
);

t.test(
"blocks wildcard domains if new outgoing requests are blocked",
async (t) => {
const outgoingDomains = new OutgoingDomains(
[
{ hostname: "*.example.com", mode: "block" },
{ hostname: "allowed.com", mode: "allow" },
],
true
);

t.equal(
outgoingDomains.shouldBlockOutgoingRequest("sub.example.com"),
true
);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("example.com"), true);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("allowed.com"), false);
}
);

t.test("allows wildcard domains if mode is allow", async (t) => {
const outgoingDomains = new OutgoingDomains(
[
{ hostname: "*.example.com", mode: "allow" },
{ hostname: "blocked.com", mode: "block" },
],
true
);

t.equal(outgoingDomains.shouldBlockOutgoingRequest("sub.example.com"), false);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("example.com"), true);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("blocked.com"), true);
});

t.test(
"does not block wildcard domains if new outgoing requests are blocked but mode is allow",
async (t) => {
const outgoingDomains = new OutgoingDomains(
[{ hostname: "*.example.com", mode: "allow" }],
true
);

t.equal(
outgoingDomains.shouldBlockOutgoingRequest("sub.example.com"),
false
);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("example.com"), true);
}
);

t.test(
"allows multiple levels of subdomains with wildcard domains",
async (t) => {
const outgoingDomains = new OutgoingDomains([
{ hostname: "*.example.com", mode: "block" },
]);

t.equal(
outgoingDomains.shouldBlockOutgoingRequest("sub.example.com"),
true
);
t.equal(
outgoingDomains.shouldBlockOutgoingRequest("sub.sub.example.com"),
true
);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("example.com"), false);
}
);

t.test("ignores tld wildcard matches", async (t) => {
const outgoingDomains = new OutgoingDomains([
{ hostname: "*.com", mode: "block" },
]);

t.equal(outgoingDomains.shouldBlockOutgoingRequest("example.com"), false);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("sub.example.com"), false);
});

t.test(
"works with empty domain list and blockNewOutgoingRequests false",
async (t) => {
const outgoingDomains = new OutgoingDomains([], false);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("example.com"), false);
}
);

t.test(
"works with empty domain list and blockNewOutgoingRequests true",
async (t) => {
const outgoingDomains = new OutgoingDomains([], true);
t.equal(outgoingDomains.shouldBlockOutgoingRequest("example.com"), true);
}
);

t.test("it does not match root domains with wildcard entries", async (t) => {
const outgoingDomains = new OutgoingDomains([
{ hostname: "*.example.com", mode: "block" },
]);

t.equal(outgoingDomains.shouldBlockOutgoingRequest("example.com"), false);
});
56 changes: 56 additions & 0 deletions library/agent/OutgoingDomains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Domain } from "./Config";

export class OutgoingDomains {
#domains: Map<string, Domain["mode"]> = new Map();
#wildcardDomains: Map<string, Domain["mode"]> = new Map();
#blockNewOutgoingRequests = false;

constructor(
domains: Domain[] = [],
blockNewOutgoingRequests: boolean = false
) {
this.#blockNewOutgoingRequests = blockNewOutgoingRequests;

for (const domain of domains) {
if (domain.hostname.startsWith("*.")) {
this.#wildcardDomains.set(domain.hostname.slice(2), domain.mode);
} else {
this.#domains.set(domain.hostname, domain.mode);
}
}
}

#getWildcardMatch(hostname: string): Domain["mode"] | undefined {
const parts = hostname.split(".");
if (parts.length <= 2) {
return undefined; // Only check for wildcard matches if there are at least 3 parts (e.g., sub.example.com)
}

const wildcardMatch = parts
.slice(1, -1)
.map((_, index) =>
this.#wildcardDomains.get(parts.slice(index + 1).join("."))
)
.find((mode) => mode !== undefined);

return wildcardMatch;
}

shouldBlockOutgoingRequest(hostname: string): boolean {
const wildcardMode = this.#getWildcardMatch(hostname);
if (wildcardMode !== undefined) {
return wildcardMode === "block";
}

const mode = this.#domains.get(hostname);

if (this.#blockNewOutgoingRequests) {
// Only allow outgoing requests if the mode is "allow"
// mode is undefined for unknown hostnames, so they get blocked
return mode !== "allow";
}

// Only block outgoing requests if the mode is "block"
return mode === "block";
}
}
32 changes: 22 additions & 10 deletions library/agent/ServiceConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,26 +400,38 @@ t.test("outbound request blocking", async (t) => {

t.same(config.shouldBlockOutgoingRequest("example.com"), false);

config.setBlockNewOutgoingRequests(true);
config.updateDomains([], true);
t.same(config.shouldBlockOutgoingRequest("example.com"), true);

config.updateDomains([
{ hostname: "example.com", mode: "allow" },
{ hostname: "aikido.dev", mode: "block" },
]);
config.updateDomains(
[
{ hostname: "example.com", mode: "allow" },
{ hostname: "aikido.dev", mode: "block" },
],
true
);
t.same(config.shouldBlockOutgoingRequest("example.com"), false);
t.same(config.shouldBlockOutgoingRequest("aikido.dev"), true);
t.same(config.shouldBlockOutgoingRequest("unknown.com"), true);

config.updateDomains([
{ hostname: "example.com", mode: "block" },
{ hostname: "aikido.dev", mode: "allow" },
]);
config.updateDomains(
[
{ hostname: "example.com", mode: "block" },
{ hostname: "aikido.dev", mode: "allow" },
],
true
);
t.same(config.shouldBlockOutgoingRequest("example.com"), true);
t.same(config.shouldBlockOutgoingRequest("aikido.dev"), false);
t.same(config.shouldBlockOutgoingRequest("unknown.com"), true);

config.setBlockNewOutgoingRequests(false);
config.updateDomains(
[
{ hostname: "example.com", mode: "block" },
{ hostname: "aikido.dev", mode: "allow" },
],
false
);

t.same(config.shouldBlockOutgoingRequest("example.com"), true);
t.same(config.shouldBlockOutgoingRequest("aikido.dev"), false);
Expand Down
23 changes: 5 additions & 18 deletions library/agent/ServiceConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IPMatcher } from "../helpers/ip-matcher/IPMatcher";
import { LimitedContext, matchEndpoints } from "../helpers/matchEndpoints";
import { isPrivateIP } from "../vulnerabilities/ssrf/isPrivateIP";
import type { Endpoint, EndpointConfig, Domain } from "./Config";
import { OutgoingDomains } from "./OutgoingDomains";
import type { IPList, UserAgentDetails } from "./api/FetchListsAPI";
import { safeCreateRegExp } from "./safeCreateRegExp";

Expand All @@ -28,8 +29,7 @@ export class ServiceConfig {
private monitoredUserAgentRegex: RegExp | undefined;
private userAgentDetails: { pattern: RegExp; key: string }[] = [];

private blockNewOutgoingRequests = false;
private domains = new Map<string, Domain["mode"]>();
private domains = new OutgoingDomains();

constructor(
endpoints: EndpointConfig[],
Expand Down Expand Up @@ -285,24 +285,11 @@ export class ServiceConfig {
return this.lastUpdatedAt;
}

setBlockNewOutgoingRequests(block: boolean) {
this.blockNewOutgoingRequests = block;
}

updateDomains(domains: Domain[]) {
this.domains = new Map(domains.map((i) => [i.hostname, i.mode]));
updateDomains(domains: Domain[], blockNewOutgoingRequests: boolean) {
this.domains = new OutgoingDomains(domains, blockNewOutgoingRequests);
}

shouldBlockOutgoingRequest(hostname: string): boolean {
const mode = this.domains.get(hostname);

if (this.blockNewOutgoingRequests) {
// Only allow outgoing requests if the mode is "allow"
// mode is undefined for unknown hostnames, so they get blocked
return mode !== "allow";
}

// Only block outgoing requests if the mode is "block"
return mode === "block";
return this.domains.shouldBlockOutgoingRequest(hostname);
}
}
Loading
Loading