From 4578c73453b7c9714ddbfcae887ccb4d71207aa2 Mon Sep 17 00:00:00 2001 From: yusufnuru Date: Tue, 5 May 2026 23:11:35 +0800 Subject: [PATCH 1/2] fix(scanner): honor config matchers.only/exclude MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `matchers.only` and `matchers.exclude` fields in `defineConfig` were declared in the schema but never read by `scan()` — only the CLI `--matchers` flag had any effect. Setting `matchers: { exclude: ["xss"] }` in `deepsec.config.ts` had no effect; xss still ran (#36). Add a pure `resolveMatchers(registry, cliSlugs, cfg)` helper that wires the config into matcher selection: - CLI `--matchers` is treated as exact and overrides config entirely. - Otherwise, base = `cfg.only ?? all`, then `cfg.exclude` is subtracted. - Unknown slugs in `only`/`exclude` warn and are ignored. `scan()` now calls this helper. The CLI also logs the resolved config-driven filter so the user sees what was applied. Closes #36 --- packages/deepsec/src/commands/scan.ts | 12 ++- .../src/__tests__/resolve-matchers.test.ts | 80 +++++++++++++++++++ packages/scanner/src/index.ts | 41 +++++++++- 3 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 packages/scanner/src/__tests__/resolve-matchers.test.ts diff --git a/packages/deepsec/src/commands/scan.ts b/packages/deepsec/src/commands/scan.ts index 2809a94..8a4ae9b 100644 --- a/packages/deepsec/src/commands/scan.ts +++ b/packages/deepsec/src/commands/scan.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { findProject, getConfigPath, projectConfigPath } from "@deepsec/core"; +import { findProject, getConfig, getConfigPath, projectConfigPath } from "@deepsec/core"; import { scan } from "@deepsec/scanner"; import { BOLD, DIM, GREEN, RESET } from "../formatters.js"; import { requireExistingDir } from "../require-dir.js"; @@ -51,7 +51,15 @@ export async function scanCommand(opts: { projectId?: string; root?: string; mat console.log(`${BOLD}Scanning${RESET} ${resolvedRoot} for project ${BOLD}${projectId}${RESET}`); if (matcherSlugs) { - console.log(`${DIM}Matchers: ${matcherSlugs.join(", ")}${RESET}`); + console.log(`${DIM}Matchers (--matchers): ${matcherSlugs.join(", ")}${RESET}`); + } else { + const cfgMatchers = getConfig()?.matchers; + if (cfgMatchers?.only?.length) { + console.log(`${DIM}Matchers (config only): ${cfgMatchers.only.join(", ")}${RESET}`); + } + if (cfgMatchers?.exclude?.length) { + console.log(`${DIM}Matchers (config exclude): ${cfgMatchers.exclude.join(", ")}${RESET}`); + } } console.log(); diff --git a/packages/scanner/src/__tests__/resolve-matchers.test.ts b/packages/scanner/src/__tests__/resolve-matchers.test.ts new file mode 100644 index 0000000..5c677d7 --- /dev/null +++ b/packages/scanner/src/__tests__/resolve-matchers.test.ts @@ -0,0 +1,80 @@ +import type { MatcherPlugin } from "@deepsec/core"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { MatcherRegistry } from "../matcher-registry.js"; +import { resolveMatchers } from "../index.js"; + +function fakeMatcher(slug: string): MatcherPlugin { + return { + slug, + description: slug, + noiseTier: "normal", + filePatterns: ["**/*.ts"], + match: () => [], + }; +} + +function buildRegistry(slugs: string[]): MatcherRegistry { + const r = new MatcherRegistry(); + for (const s of slugs) r.register(fakeMatcher(s)); + return r; +} + +describe("resolveMatchers", () => { + let warnSpy: ReturnType; + + beforeEach(() => { + warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + }); + afterEach(() => { + warnSpy.mockRestore(); + }); + + it("returns all matchers when no CLI slugs and no config filter", () => { + const r = buildRegistry(["xss", "rce", "ssrf"]); + const result = resolveMatchers(r, undefined, undefined); + expect(result.map((m) => m.slug).sort()).toEqual(["rce", "ssrf", "xss"]); + }); + + it("CLI slugs override config entirely", () => { + const r = buildRegistry(["xss", "rce", "ssrf"]); + const result = resolveMatchers(r, ["xss"], { exclude: ["xss"], only: ["rce"] }); + expect(result.map((m) => m.slug)).toEqual(["xss"]); + }); + + it("config exclude removes matchers (issue #36)", () => { + const r = buildRegistry(["xss", "rce", "ssrf"]); + const result = resolveMatchers(r, undefined, { exclude: ["xss"] }); + expect(result.map((m) => m.slug).sort()).toEqual(["rce", "ssrf"]); + }); + + it("config only restricts the base set", () => { + const r = buildRegistry(["xss", "rce", "ssrf"]); + const result = resolveMatchers(r, undefined, { only: ["xss", "rce"] }); + expect(result.map((m) => m.slug).sort()).toEqual(["rce", "xss"]); + }); + + it("only and exclude compose: exclude subtracts from only", () => { + const r = buildRegistry(["xss", "rce", "ssrf"]); + const result = resolveMatchers(r, undefined, { only: ["xss", "rce"], exclude: ["rce"] }); + expect(result.map((m) => m.slug)).toEqual(["xss"]); + }); + + it("warns on unknown slug in only and ignores it", () => { + const r = buildRegistry(["xss", "rce"]); + const result = resolveMatchers(r, undefined, { only: ["xss", "bogus"] }); + expect(result.map((m) => m.slug)).toEqual(["xss"]); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining(`unknown matcher slug "bogus"`)); + }); + + it("warns on unknown slug in exclude", () => { + const r = buildRegistry(["xss", "rce"]); + resolveMatchers(r, undefined, { exclude: ["nope"] }); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining(`unknown matcher slug "nope"`)); + }); + + it("empty only with exclude treats base as all matchers", () => { + const r = buildRegistry(["xss", "rce"]); + const result = resolveMatchers(r, undefined, { only: [], exclude: ["xss"] }); + expect(result.map((m) => m.slug)).toEqual(["rce"]); + }); +}); diff --git a/packages/scanner/src/index.ts b/packages/scanner/src/index.ts index 6923b8f..70ce43a 100644 --- a/packages/scanner/src/index.ts +++ b/packages/scanner/src/index.ts @@ -7,6 +7,7 @@ import { createRunMeta, dataDir, ensureProject, + getConfig, getRegistry, readFileRecord, writeFileRecord, @@ -31,6 +32,42 @@ function buildMergedRegistry(): MatcherRegistry { return registry; } +/** + * Resolve which matchers to run given an optional explicit list (e.g. from + * `--matchers` on the CLI) and the loaded config's `matchers.only/exclude`. + * + * Precedence: + * - If `cliSlugs` is given, it is treated as exact: config is ignored. + * - Otherwise, base = `cfg.only ?? all`, then `cfg.exclude` is subtracted. + * + * Unknown slugs in `only`/`exclude` are warned and ignored. + */ +export function resolveMatchers( + registry: MatcherRegistry, + cliSlugs: string[] | undefined, + cfg: { only?: string[]; exclude?: string[] } | undefined, +): MatcherPlugin[] { + if (cliSlugs) { + return registry.getBySlugs(cliSlugs); + } + + const known = new Set(registry.slugs()); + const warnUnknown = (kind: "only" | "exclude", slugs: string[] | undefined) => { + if (!slugs) return; + for (const s of slugs) { + if (!known.has(s)) { + console.warn(`[deepsec] config matchers.${kind}: unknown matcher slug "${s}" — ignoring`); + } + } + }; + warnUnknown("only", cfg?.only); + warnUnknown("exclude", cfg?.exclude); + + const base = cfg?.only?.length ? registry.getBySlugs(cfg.only) : registry.getAll(); + const exclude = new Set(cfg?.exclude ?? []); + return base.filter((m) => !exclude.has(m.slug)); +} + /** Returns the noise tier for a given vulnSlug. Defaults to "normal". */ export function getNoiseTier(slug: string): import("./types.js").NoiseTier { const registry = buildMergedRegistry(); @@ -234,9 +271,7 @@ export async function scan(params: { onProgress?: (progress: ScanProgress) => void; }): Promise<{ runId: string; candidateCount: number }> { const registry = buildMergedRegistry(); - const matchers = params.matcherSlugs - ? registry.getBySlugs(params.matcherSlugs) - : registry.getAll(); + const matchers = resolveMatchers(registry, params.matcherSlugs, getConfig()?.matchers); if (matchers.length === 0) { throw new Error("No matchers selected"); From 7a37d8ce393247baad933df7ded414aec7ba261c Mon Sep 17 00:00:00 2001 From: yusufnuru Date: Tue, 5 May 2026 23:20:24 +0800 Subject: [PATCH 2/2] chore: sort imports in resolve-matchers test --- packages/scanner/src/__tests__/resolve-matchers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scanner/src/__tests__/resolve-matchers.test.ts b/packages/scanner/src/__tests__/resolve-matchers.test.ts index 5c677d7..23023ef 100644 --- a/packages/scanner/src/__tests__/resolve-matchers.test.ts +++ b/packages/scanner/src/__tests__/resolve-matchers.test.ts @@ -1,7 +1,7 @@ import type { MatcherPlugin } from "@deepsec/core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { MatcherRegistry } from "../matcher-registry.js"; import { resolveMatchers } from "../index.js"; +import { MatcherRegistry } from "../matcher-registry.js"; function fakeMatcher(slug: string): MatcherPlugin { return {