Skip to content
Merged
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
"build": "caido-dev build",
"watch": "caido-dev watch",
"lint": "eslint --fix packages/*/src",
"knip": "knip",
"test": "vitest run",
"validate": "pnpm run typecheck && pnpm run lint && pnpm run test",
"validate": "pnpm run typecheck && pnpm run lint && pnpm run knip && pnpm run test",
"generate:parsers": "pnpm -r run generate:parsers",
"trace-viewer": "pnpm --filter trace-viewer dev"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
type CheckContext,
defineCheckV2,
generateRandomString,
Result,
Severity,
type CheckContext,
type ScanTarget,
Severity,
} from "engine";

import { Tags } from "../../types";
Expand Down
209 changes: 148 additions & 61 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { DefineAPI } from "caido:plugin";
import { createRegistry, Result } from "engine";
import type { Result as ResultType } from "shared";
import {
createPrefixedRandomId,
createRegistry,
createScheduler,
Result,
} from "engine";
import type { BasicRequest, Result as ResultType } from "shared";

import { checks } from "./checks";
import { IdSchema } from "./schemas";
Expand All @@ -21,7 +26,7 @@ import { ConfigStore } from "./stores/config";
import { QueueStore } from "./stores/queue";
import { ScannerStore } from "./stores/scanner";
import { type BackendSDK } from "./types";
import { TaskQueue } from "./utils/task-queue";
import { packExecutionHistory } from "./utils/debug";
import { validateInput } from "./utils/validation";

export { type BackendEvents } from "./types";
Expand Down Expand Up @@ -77,17 +82,31 @@ export async function init(sdk: BackendSDK) {

await configStore.initialize(sdk);
await scannerStore.initialize(sdk);
const project = await sdk.projects.getCurrent();
queueStore.switchProject(project?.getId());

const config = configStore.getUserConfig();
const passiveTaskQueue = new TaskQueue(config.passive.concurrentChecks);
const passiveTaskQueue = createScheduler(config.passive.concurrentTargets);
queueStore.setPassiveTaskQueue(passiveTaskQueue);
let passiveDedupeKeys = new Map<string, Set<string>>();
let passiveQueueSnapshotTimeout: Timeout | undefined;

const emitPassiveQueueSnapshot = () => {
if (passiveQueueSnapshotTimeout !== undefined) {
return;
}

passiveQueueSnapshotTimeout = setTimeout(() => {
sdk.api.send("passive:queue-updated", queueStore.getTasks());
passiveQueueSnapshotTimeout = undefined;
}, 150);
};

const passiveDedupeKeys = new Map<string, Set<string>>();
sdk.events.onInterceptResponse((sdk, request) => {
const config = configStore.getUserConfig();
if (!config.passive.enabled) return;

passiveTaskQueue.setConcurrency(config.passive.concurrentChecks);
passiveTaskQueue.setConcurrency(config.passive.concurrentTargets);

if (config.passive.scopeIDs.length > 0) {
const inScope = sdk.requests.inScope(request, config.passive.scopeIDs);
Expand All @@ -103,75 +122,127 @@ export async function init(sdk: BackendSDK) {
return;
}

const passiveTaskID =
"pscan-" + Math.random().toString(36).substring(2, 15);
queueStore.addTask(passiveTaskID, request.getId());
sdk.api.send("passive:queue-new", passiveTaskID, request.getId());

passiveTaskQueue.add(async () => {
const registry = createRegistry();
for (const check of passiveChecks) {
registry.register(check);
}

const requestTimeout = config.requestTimeout ?? 2 * 60;
const runnable = registry.create(sdk, {
aggressivity: config.passive.aggressivity,
scopeIDs: config.passive.scopeIDs,
concurrentChecks: config.passive.concurrentChecks,
concurrentRequests: config.passive.concurrentRequests,
concurrentTargets: 1,
severities: config.passive.severities,
scanTimeout: 5 * 60,
checkTimeout: 2 * 60,
requestTimeout,
requestsDelayMs: 0,
});

runnable.externalDedupeKeys(passiveDedupeKeys);
const passiveTaskID = createPrefixedRandomId("pscan-");
queueStore.addTask(passiveTaskID, toBasicRequest(request));
emitPassiveQueueSnapshot();

void passiveTaskQueue
.schedule(async () => {
const registry = createRegistry();
for (const check of passiveChecks) {
registry.register(check);
}

const requestTimeout = config.requestTimeout ?? 2 * 60;
const runnable = registry.create(sdk, {
aggressivity: config.passive.aggressivity,
scopeIDs: config.passive.scopeIDs,
concurrentChecks: 2,
concurrentRequests: config.passive.concurrentRequests,
concurrentTargets: 1,
severities: config.passive.severities,
scanTimeout: 5 * 60,
checkTimeout: 2 * 60,
requestTimeout,
requestsDelayMs: 0,
});

try {
queueStore.addActiveRunnable(passiveTaskID, runnable);
queueStore.updateTaskStatus(passiveTaskID, "running");
sdk.api.send("passive:queue-started", passiveTaskID);
runnable.externalDedupeKeys(passiveDedupeKeys);

runnable.on("scan:finding", async ({ finding, checkID }) => {
const request = await sdk.requests.get(finding.correlation.requestID);
if (!request) return;
if (!config.passive.severities.includes(finding.severity)) return;
try {
queueStore.addActiveRunnable(passiveTaskID, runnable);
queueStore.updateTaskStatus(passiveTaskID, "running");
emitPassiveQueueSnapshot();

const wrappedDescription = `This finding has been assessed as \`${finding.severity.toUpperCase()}\` severity and was discovered by the \`${checkID}\` check.\n\n${
finding.description
}`;
runnable.on("scan:check-started", ({ checkID }) => {
queueStore.addExecutedCheck(passiveTaskID, checkID);
emitPassiveQueueSnapshot();
});

sdk.findings.create({
reporter: "Scanner: Passive",
request: request.request,
title: finding.name,
description: wrappedDescription,
runnable.on("scan:finding", async ({ finding, checkID }) => {
const request = await sdk.requests.get(
finding.correlation.requestID,
);
if (!request) return;
if (!config.passive.severities.includes(finding.severity)) return;

const wrappedDescription = `This finding has been assessed as \`${finding.severity.toUpperCase()}\` severity and was discovered by the \`${checkID}\` check.\n\n${
finding.description
}`;

sdk.findings.create({
reporter: "Scanner: Passive",
request: request.request,
title: finding.name,
description: wrappedDescription,
});
});
});

// TODO: handle error, show UI warnings if result kind is not finished
await runnable.run([request.getId()]);
} catch (error) {
// TODO: handle error, show UI warnings
sdk.console.log("error=", error);
} finally {
queueStore.removeActiveRunnable(passiveTaskID);
queueStore.removeTask(passiveTaskID);
sdk.api.send("passive:queue-finished", passiveTaskID);
}
});
const result = await runnable.run([request.getId()]);
switch (result.kind) {
case "Finished":
queueStore.updateTaskStatus(passiveTaskID, "completed");
break;
case "Interrupted":
queueStore.updateTaskStatus(
passiveTaskID,
"cancelled",
result.reason,
);
break;
case "Error":
queueStore.updateTaskStatus(
passiveTaskID,
"failed",
result.error,
);
break;
}
} catch (error) {
queueStore.updateTaskStatus(
passiveTaskID,
"failed",
error instanceof Error ? error.message : "Unknown error",
);
} finally {
queueStore.removeActiveRunnable(passiveTaskID);
emitPassiveQueueSnapshot();
}
})
.promise.catch((error: unknown) => {
queueStore.updateTaskStatus(
passiveTaskID,
"cancelled",
error instanceof Error ? error.message : "Cancelled",
);
emitPassiveQueueSnapshot();
});
});

sdk.events.onProjectChange(async (sdk, project) => {
const projectId = project?.getId();
sdk.api.send("project:changed", projectId, "start");
queueStore.clearTasks();
emitPassiveQueueSnapshot();
passiveDedupeKeys = new Map<string, Set<string>>();

const runningSessionIds = scannerStore.listRunningSessionIds();
for (const sessionId of runningSessionIds) {
const runnable = scannerStore.getRunnable(sessionId);
const trace =
runnable === undefined
? ""
: packExecutionHistory(runnable.getExecutionHistory());
scannerStore.interruptSession(sessionId, "ProjectChanged", trace);
void runnable?.cancel("ProjectChanged");
}

await configStore.switchProject(projectId);
await scannerStore.switchProject(projectId);
queueStore.switchProject(projectId);
emitPassiveQueueSnapshot();

sdk.api.send("project:changed", projectId);
sdk.api.send("project:changed", projectId, "ready");
});
}

Expand Down Expand Up @@ -213,6 +284,22 @@ export const getRequestResponse = async (
});
};

const toBasicRequest = (request: {
getId: () => string;
getHost: () => string;
getPort: () => number;
getPath: () => string;
getQuery: () => string;
getMethod: () => string;
}): BasicRequest => ({
id: request.getId(),
host: request.getHost(),
port: request.getPort(),
path: request.getPath(),
query: request.getQuery(),
method: request.getMethod().toUpperCase(),
});

export const getExecutionTrace = (
sdk: BackendSDK,
sessionId: string,
Expand Down
75 changes: 75 additions & 0 deletions packages/backend/src/services/scanner/execution.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, it, vi } from "vitest";

import { ChecksStore } from "../../stores/checks";
import { ConfigStore } from "../../stores/config";
import { ScannerStore } from "../../stores/scanner";

import { startActiveScan } from "./execution";

vi.mock("caido:utils", () => ({
RequestSpec: class {},
}));

const payload = {
requestIDs: ["req-123"],
title: "Example scan",
scanConfig: {
aggressivity: "medium" as const,
scopeIDs: [],
concurrentChecks: 1,
concurrentRequests: 1,
concurrentTargets: 1,
requestsDelayMs: 0,
scanTimeout: 60,
checkTimeout: 60,
severities: ["info" as const],
},
};

describe("startActiveScan", () => {
it("fails before creating a session when a request does not exist", async () => {
const createSession = vi.fn();
const getRequest = vi.fn().mockResolvedValue(undefined);

vi.spyOn(ConfigStore, "get").mockReturnValue({
getUserConfig: () => ({
passive: {
enabled: false,
aggressivity: "medium",
scopeIDs: [],
concurrentTargets: 1,
concurrentRequests: 1,
overrides: [],
severities: ["info"],
},
active: {
overrides: [],
},
presets: [],
}),
} as unknown as ConfigStore);

vi.spyOn(ChecksStore, "get").mockReturnValue({
select: () => [{ id: "check-1" } as never],
} as unknown as ChecksStore);

vi.spyOn(ScannerStore, "get").mockReturnValue({
createSession,
} as unknown as ScannerStore);

const sdk = {
requests: {
get: getRequest,
},
} as never;

const result = await startActiveScan(sdk, payload);

expect(result).toEqual({
kind: "Error",
error: "Request req-123 not found",
});
expect(getRequest).toHaveBeenCalledWith("req-123");
expect(createSession).not.toHaveBeenCalled();
});
});
Loading
Loading