@rafter-security/cli 0.4.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +101 -1
  2. package/dist/commands/agent/audit-skill.js +6 -0
  3. package/dist/commands/agent/audit.js +15 -3
  4. package/dist/commands/agent/exec.js +9 -8
  5. package/dist/commands/agent/index.js +4 -0
  6. package/dist/commands/agent/init.js +132 -47
  7. package/dist/commands/agent/install-hook.js +2 -1
  8. package/dist/commands/agent/scan.js +180 -103
  9. package/dist/commands/agent/status.js +115 -0
  10. package/dist/commands/agent/verify.js +117 -0
  11. package/dist/commands/ci/index.js +8 -0
  12. package/dist/commands/ci/init.js +191 -0
  13. package/dist/commands/completion.js +170 -0
  14. package/dist/commands/hook/index.js +10 -0
  15. package/dist/commands/hook/posttool.js +73 -0
  16. package/dist/commands/hook/pretool.js +122 -0
  17. package/dist/commands/mcp/index.js +8 -0
  18. package/dist/commands/mcp/server.js +205 -0
  19. package/dist/commands/policy/export.js +81 -0
  20. package/dist/commands/policy/index.js +8 -0
  21. package/dist/core/audit-logger.js +2 -33
  22. package/dist/core/command-interceptor.js +6 -50
  23. package/dist/core/config-defaults.js +4 -15
  24. package/dist/core/config-manager.js +68 -0
  25. package/dist/core/custom-patterns.js +157 -0
  26. package/dist/core/policy-loader.js +167 -0
  27. package/dist/core/risk-rules.js +72 -0
  28. package/dist/index.js +26 -2
  29. package/dist/scanners/gitleaks.js +7 -6
  30. package/dist/scanners/regex-scanner.js +28 -12
  31. package/dist/utils/binary-manager.js +100 -7
  32. package/dist/utils/formatter.js +52 -0
  33. package/dist/utils/skill-manager.js +22 -9
  34. package/package.json +7 -3
  35. package/resources/pre-commit-hook.sh +45 -0
  36. package/resources/rafter-security-skill.md +323 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Load custom secret patterns from ~/.rafter/patterns/
3
+ * and suppression rules from .rafterignore.
4
+ */
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { getRafterDir } from "./config-defaults.js";
8
+ // ---------------------------------------------------------------------------
9
+ // Custom pattern loading
10
+ // ---------------------------------------------------------------------------
11
+ /**
12
+ * Load user-defined patterns from ~/.rafter/patterns/*.txt and *.json.
13
+ *
14
+ * .txt — one regex per line (comments with # ignored)
15
+ * .json — array of {name, pattern, severity?} objects
16
+ *
17
+ * Returns Pattern[] merged with DEFAULT_SECRET_PATTERNS by callers.
18
+ */
19
+ export function loadCustomPatterns() {
20
+ const patternsDir = path.join(getRafterDir(), "patterns");
21
+ if (!fs.existsSync(patternsDir))
22
+ return [];
23
+ const results = [];
24
+ let entries;
25
+ try {
26
+ entries = fs.readdirSync(patternsDir, { withFileTypes: true });
27
+ }
28
+ catch {
29
+ return [];
30
+ }
31
+ for (const entry of entries) {
32
+ if (!entry.isFile())
33
+ continue;
34
+ const file = path.join(patternsDir, entry.name);
35
+ const ext = path.extname(entry.name).toLowerCase();
36
+ if (ext === ".txt") {
37
+ results.push(...loadTxtPatterns(file));
38
+ }
39
+ else if (ext === ".json") {
40
+ results.push(...loadJsonPatterns(file));
41
+ }
42
+ }
43
+ return results;
44
+ }
45
+ function loadTxtPatterns(file) {
46
+ try {
47
+ const lines = fs.readFileSync(file, "utf-8").split("\n");
48
+ const patterns = [];
49
+ for (const raw of lines) {
50
+ const line = raw.trim();
51
+ if (!line || line.startsWith("#"))
52
+ continue;
53
+ patterns.push({
54
+ name: `Custom (${path.basename(file, ".txt")})`,
55
+ regex: line,
56
+ severity: "high",
57
+ });
58
+ }
59
+ return patterns;
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ }
65
+ function loadJsonPatterns(file) {
66
+ try {
67
+ const data = JSON.parse(fs.readFileSync(file, "utf-8"));
68
+ if (!Array.isArray(data))
69
+ return [];
70
+ const patterns = [];
71
+ for (const entry of data) {
72
+ if (typeof entry.pattern !== "string")
73
+ continue;
74
+ patterns.push({
75
+ name: entry.name ?? `Custom (${path.basename(file, ".json")})`,
76
+ regex: entry.pattern,
77
+ severity: entry.severity ?? "high",
78
+ description: entry.description,
79
+ });
80
+ }
81
+ return patterns;
82
+ }
83
+ catch {
84
+ return [];
85
+ }
86
+ }
87
+ /**
88
+ * Parse .rafterignore from the given directory (project root).
89
+ *
90
+ * Format — one entry per line:
91
+ * path/glob → suppress all findings in matching files
92
+ * path/glob:pattern-name → suppress specific pattern in matching files
93
+ *
94
+ * Lines starting with # are comments.
95
+ */
96
+ export function loadSuppressions(projectRoot = process.cwd()) {
97
+ const file = path.join(projectRoot, ".rafterignore");
98
+ if (!fs.existsSync(file))
99
+ return [];
100
+ const suppressions = [];
101
+ try {
102
+ const lines = fs.readFileSync(file, "utf-8").split("\n");
103
+ for (const raw of lines) {
104
+ const line = raw.trim();
105
+ if (!line || line.startsWith("#"))
106
+ continue;
107
+ const colonIdx = line.indexOf(":");
108
+ if (colonIdx === -1) {
109
+ suppressions.push({ pathGlob: line });
110
+ }
111
+ else {
112
+ suppressions.push({
113
+ pathGlob: line.slice(0, colonIdx).trim(),
114
+ patternName: line.slice(colonIdx + 1).trim() || undefined,
115
+ });
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // ignore unreadable .rafterignore
121
+ }
122
+ return suppressions;
123
+ }
124
+ /**
125
+ * Returns true if a finding should be suppressed.
126
+ */
127
+ export function isSuppressed(filePath, patternName, suppressions) {
128
+ for (const s of suppressions) {
129
+ if (matchGlob(s.pathGlob, filePath)) {
130
+ if (!s.patternName || s.patternName.toLowerCase() === patternName.toLowerCase()) {
131
+ return true;
132
+ }
133
+ }
134
+ }
135
+ return false;
136
+ }
137
+ /**
138
+ * Minimal glob matcher: supports * (within segment) and ** (cross-segment).
139
+ * Not full micromatch — covers the 90% case for .rafterignore.
140
+ */
141
+ function matchGlob(glob, filePath) {
142
+ // Normalise separators
143
+ const g = glob.replace(/\\/g, "/");
144
+ const f = filePath.replace(/\\/g, "/");
145
+ // Escape regex special chars except * which we handle specially
146
+ const escaped = g
147
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
148
+ .replace(/\*\*/g, "\x00") // placeholder for **
149
+ .replace(/\*/g, "[^/]*") // * = anything within one segment
150
+ .replace(/\x00/g, ".*"); // ** = anything including /
151
+ try {
152
+ return new RegExp(`(^|/)${escaped}(/|$)`).test(f);
153
+ }
154
+ catch {
155
+ return false;
156
+ }
157
+ }
@@ -0,0 +1,167 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { execSync } from "child_process";
4
+ import yaml from "js-yaml";
5
+ const POLICY_FILENAMES = [".rafter.yml", ".rafter.yaml"];
6
+ /**
7
+ * Find a policy file by walking from cwd up to git root
8
+ */
9
+ export function findPolicyFile() {
10
+ let dir = process.cwd();
11
+ const root = getGitRoot() || path.parse(dir).root;
12
+ while (true) {
13
+ for (const filename of POLICY_FILENAMES) {
14
+ const candidate = path.join(dir, filename);
15
+ if (fs.existsSync(candidate)) {
16
+ return candidate;
17
+ }
18
+ }
19
+ const parent = path.dirname(dir);
20
+ if (parent === dir || dir === root) {
21
+ break;
22
+ }
23
+ dir = parent;
24
+ }
25
+ return null;
26
+ }
27
+ /**
28
+ * Load and parse the policy file, returning null if not found
29
+ */
30
+ export function loadPolicy() {
31
+ const policyPath = findPolicyFile();
32
+ if (!policyPath)
33
+ return null;
34
+ try {
35
+ const content = fs.readFileSync(policyPath, "utf-8");
36
+ const parsed = yaml.load(content);
37
+ if (!parsed || typeof parsed !== "object")
38
+ return null;
39
+ return validatePolicy(mapPolicy(parsed), parsed);
40
+ }
41
+ catch (e) {
42
+ console.error(`Warning: Failed to parse policy file ${policyPath}: ${e.message}`);
43
+ return null;
44
+ }
45
+ }
46
+ /**
47
+ * Map snake_case YAML keys to camelCase PolicyFile
48
+ */
49
+ function mapPolicy(raw) {
50
+ const policy = {};
51
+ if (raw.version)
52
+ policy.version = String(raw.version);
53
+ if (raw.risk_level)
54
+ policy.riskLevel = raw.risk_level;
55
+ if (raw.command_policy && typeof raw.command_policy === "object") {
56
+ policy.commandPolicy = {};
57
+ if (raw.command_policy.mode)
58
+ policy.commandPolicy.mode = raw.command_policy.mode;
59
+ if (Array.isArray(raw.command_policy.blocked_patterns)) {
60
+ policy.commandPolicy.blockedPatterns = raw.command_policy.blocked_patterns;
61
+ }
62
+ if (Array.isArray(raw.command_policy.require_approval)) {
63
+ policy.commandPolicy.requireApproval = raw.command_policy.require_approval;
64
+ }
65
+ }
66
+ if (raw.scan && typeof raw.scan === "object") {
67
+ policy.scan = {};
68
+ if (Array.isArray(raw.scan.exclude_paths)) {
69
+ policy.scan.excludePaths = raw.scan.exclude_paths;
70
+ }
71
+ if (Array.isArray(raw.scan.custom_patterns)) {
72
+ policy.scan.customPatterns = raw.scan.custom_patterns.map((p) => ({
73
+ name: p.name,
74
+ regex: p.regex,
75
+ severity: p.severity || "high",
76
+ }));
77
+ }
78
+ }
79
+ if (raw.audit && typeof raw.audit === "object") {
80
+ policy.audit = {};
81
+ if (raw.audit.retention_days != null) {
82
+ policy.audit.retentionDays = Number(raw.audit.retention_days);
83
+ }
84
+ if (raw.audit.log_level)
85
+ policy.audit.logLevel = raw.audit.log_level;
86
+ }
87
+ return policy;
88
+ }
89
+ const VALID_TOP_LEVEL_KEYS = new Set(["version", "risk_level", "command_policy", "scan", "audit"]);
90
+ const VALID_RISK_LEVELS = new Set(["minimal", "moderate", "aggressive"]);
91
+ const VALID_COMMAND_MODES = new Set(["allow-all", "approve-dangerous", "deny-list"]);
92
+ const VALID_LOG_LEVELS = new Set(["debug", "info", "warn", "error"]);
93
+ /**
94
+ * Validate a mapped policy, warn on stderr for invalid fields, strip them out.
95
+ * `raw` is the original parsed YAML (snake_case keys) for unknown-key detection.
96
+ */
97
+ function validatePolicy(policy, raw) {
98
+ // 1. Unknown top-level keys
99
+ for (const key of Object.keys(raw)) {
100
+ if (!VALID_TOP_LEVEL_KEYS.has(key)) {
101
+ console.error(`Warning: Unknown policy key "${key}" — ignoring.`);
102
+ }
103
+ }
104
+ // 2. Type checking + strip invalid
105
+ if (policy.version !== undefined && typeof policy.version !== "string") {
106
+ console.error(`Warning: "version" must be a string — ignoring.`);
107
+ delete policy.version;
108
+ }
109
+ if (policy.riskLevel !== undefined && !VALID_RISK_LEVELS.has(policy.riskLevel)) {
110
+ console.error(`Warning: "risk_level" must be one of: minimal, moderate, aggressive — ignoring.`);
111
+ delete policy.riskLevel;
112
+ }
113
+ if (policy.commandPolicy) {
114
+ if (policy.commandPolicy.mode !== undefined && !VALID_COMMAND_MODES.has(policy.commandPolicy.mode)) {
115
+ console.error(`Warning: "command_policy.mode" must be one of: allow-all, approve-dangerous, deny-list — ignoring.`);
116
+ delete policy.commandPolicy.mode;
117
+ }
118
+ if (policy.commandPolicy.blockedPatterns !== undefined) {
119
+ if (!Array.isArray(policy.commandPolicy.blockedPatterns) || !policy.commandPolicy.blockedPatterns.every((v) => typeof v === "string")) {
120
+ console.error(`Warning: "command_policy.blocked_patterns" must be an array of strings — ignoring.`);
121
+ delete policy.commandPolicy.blockedPatterns;
122
+ }
123
+ }
124
+ if (policy.commandPolicy.requireApproval !== undefined) {
125
+ if (!Array.isArray(policy.commandPolicy.requireApproval) || !policy.commandPolicy.requireApproval.every((v) => typeof v === "string")) {
126
+ console.error(`Warning: "command_policy.require_approval" must be an array of strings — ignoring.`);
127
+ delete policy.commandPolicy.requireApproval;
128
+ }
129
+ }
130
+ }
131
+ if (policy.scan) {
132
+ if (policy.scan.excludePaths !== undefined) {
133
+ if (!Array.isArray(policy.scan.excludePaths) || !policy.scan.excludePaths.every((v) => typeof v === "string")) {
134
+ console.error(`Warning: "scan.exclude_paths" must be an array of strings — ignoring.`);
135
+ delete policy.scan.excludePaths;
136
+ }
137
+ }
138
+ if (policy.scan.customPatterns !== undefined) {
139
+ if (!Array.isArray(policy.scan.customPatterns) || !policy.scan.customPatterns.every((v) => v && typeof v === "object" && typeof v.name === "string" && v.name !== "" && typeof v.regex === "string" && v.regex !== "" && typeof v.severity === "string")) {
140
+ console.error(`Warning: "scan.custom_patterns" must be an array of objects with name, regex, severity — ignoring.`);
141
+ delete policy.scan.customPatterns;
142
+ }
143
+ }
144
+ }
145
+ if (policy.audit) {
146
+ if (policy.audit.retentionDays !== undefined && (typeof policy.audit.retentionDays !== "number" || isNaN(policy.audit.retentionDays))) {
147
+ console.error(`Warning: "audit.retention_days" must be a number — ignoring.`);
148
+ delete policy.audit.retentionDays;
149
+ }
150
+ if (policy.audit.logLevel !== undefined && !VALID_LOG_LEVELS.has(policy.audit.logLevel)) {
151
+ console.error(`Warning: "audit.log_level" must be one of: debug, info, warn, error — ignoring.`);
152
+ delete policy.audit.logLevel;
153
+ }
154
+ }
155
+ return policy;
156
+ }
157
+ function getGitRoot() {
158
+ try {
159
+ return execSync("git rev-parse --show-toplevel", {
160
+ encoding: "utf-8",
161
+ stdio: ["pipe", "pipe", "ignore"],
162
+ }).trim();
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Centralized risk assessment rules.
3
+ * Single source of truth — imported by command-interceptor, audit-logger, and config-defaults.
4
+ */
5
+ export const CRITICAL_PATTERNS = [
6
+ /rm\s+-rf\s+\//,
7
+ /:\(\)\{\s*:\|:&\s*\};:/, // fork bomb
8
+ /dd\s+if=.*of=\/dev\/sd/,
9
+ />\s*\/dev\/sd/,
10
+ /mkfs/,
11
+ /fdisk/,
12
+ /parted/,
13
+ ];
14
+ export const HIGH_PATTERNS = [
15
+ /rm\s+-rf/,
16
+ /sudo\s+rm/,
17
+ /chmod\s+777/,
18
+ /curl.*\|\s*(bash|sh|zsh|dash)\b/,
19
+ /wget.*\|\s*(bash|sh|zsh|dash)\b/,
20
+ /git\s+push\s+(--force|-f)\b/,
21
+ /git\s+push\s+--force-(with-lease|if-includes)\b/,
22
+ /git\s+push\s+\S*\s+\+\S+/, // refspec force: git push origin +main
23
+ /docker\s+system\s+prune/,
24
+ /npm\s+publish/,
25
+ /pypi.*upload/,
26
+ ];
27
+ export const MEDIUM_PATTERNS = [
28
+ /sudo/,
29
+ /chmod/,
30
+ /chown/,
31
+ /systemctl/,
32
+ /service/,
33
+ /kill\s+-9/,
34
+ /pkill/,
35
+ /killall/,
36
+ ];
37
+ export const DEFAULT_BLOCKED_PATTERNS = [
38
+ "rm -rf /",
39
+ ":(){ :|:& };:",
40
+ "dd if=/dev/zero of=/dev/sda",
41
+ "> /dev/sda",
42
+ ];
43
+ export const DEFAULT_REQUIRE_APPROVAL = [
44
+ "rm -rf",
45
+ "sudo rm",
46
+ "curl.*\\|\\s*(bash|sh|zsh|dash)\\b",
47
+ "wget.*\\|\\s*(bash|sh|zsh|dash)\\b",
48
+ "chmod 777",
49
+ "git push --force",
50
+ "git push -f",
51
+ "git push --force-with-lease",
52
+ "git push --force-if-includes",
53
+ ];
54
+ /**
55
+ * Assess risk level of a command string.
56
+ */
57
+ export function assessCommandRisk(command) {
58
+ const cmd = command.toLowerCase();
59
+ for (const pattern of CRITICAL_PATTERNS) {
60
+ if (pattern.test(cmd))
61
+ return "critical";
62
+ }
63
+ for (const pattern of HIGH_PATTERNS) {
64
+ if (pattern.test(cmd))
65
+ return "high";
66
+ }
67
+ for (const pattern of MEDIUM_PATTERNS) {
68
+ if (pattern.test(cmd))
69
+ return "medium";
70
+ }
71
+ return "low";
72
+ }
package/dist/index.js CHANGED
@@ -5,19 +5,43 @@ import { createRunCommand } from "./commands/backend/run.js";
5
5
  import { createGetCommand } from "./commands/backend/get.js";
6
6
  import { createUsageCommand } from "./commands/backend/usage.js";
7
7
  import { createAgentCommand } from "./commands/agent/index.js";
8
+ import { createCiCommand } from "./commands/ci/index.js";
9
+ import { createHookCommand } from "./commands/hook/index.js";
10
+ import { createMcpCommand } from "./commands/mcp/index.js";
11
+ import { createPolicyCommand } from "./commands/policy/index.js";
12
+ import { createCompletionCommand } from "./commands/completion.js";
8
13
  import { checkForUpdate } from "./utils/update-checker.js";
14
+ import { setAgentMode } from "./utils/formatter.js";
9
15
  dotenv.config();
10
- const VERSION = "0.4.2";
16
+ const VERSION = "0.5.3";
11
17
  const program = new Command()
12
18
  .name("rafter")
13
19
  .description("Rafter CLI")
14
- .version(VERSION);
20
+ .version(VERSION)
21
+ .option("-a, --agent", "Plain output for AI agents (no colors/emoji)");
22
+ // Set agent mode before any subcommand runs
23
+ program.hook("preAction", (thisCommand) => {
24
+ const opts = thisCommand.opts();
25
+ if (opts.agent) {
26
+ setAgentMode(true);
27
+ }
28
+ });
15
29
  // Backend commands (existing)
16
30
  program.addCommand(createRunCommand());
17
31
  program.addCommand(createGetCommand());
18
32
  program.addCommand(createUsageCommand());
19
33
  // Agent commands
20
34
  program.addCommand(createAgentCommand());
35
+ // CI commands
36
+ program.addCommand(createCiCommand());
37
+ // Hook commands (for agent platform integration)
38
+ program.addCommand(createHookCommand());
39
+ // MCP server
40
+ program.addCommand(createMcpCommand());
41
+ // Policy commands
42
+ program.addCommand(createPolicyCommand());
43
+ // Shell completions
44
+ program.addCommand(createCompletionCommand());
21
45
  // Non-blocking update check — runs after command, prints to stderr
22
46
  checkForUpdate(VERSION).then((notice) => {
23
47
  if (notice)
@@ -1,9 +1,10 @@
1
- import { exec } from "child_process";
1
+ import { execFile } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { BinaryManager } from "../utils/binary-manager.js";
4
4
  import fs from "fs";
5
+ import os from "os";
5
6
  import path from "path";
6
- const execAsync = promisify(exec);
7
+ const execFileAsync = promisify(execFile);
7
8
  export class GitleaksScanner {
8
9
  constructor() {
9
10
  this.binaryManager = new BinaryManager();
@@ -25,10 +26,10 @@ export class GitleaksScanner {
25
26
  throw new Error("Gitleaks not available");
26
27
  }
27
28
  const gitleaksPath = this.binaryManager.getGitleaksPath();
28
- const tmpReport = path.join("/tmp", `gitleaks-${Date.now()}.json`);
29
+ const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
29
30
  try {
30
31
  // Run gitleaks detect on file
31
- await execAsync(`"${gitleaksPath}" detect --no-git -f json -r "${tmpReport}" -s "${filePath}"`, { timeout: 30000 });
32
+ await execFileAsync(gitleaksPath, ["detect", "--no-git", "-f", "json", "-r", tmpReport, "-s", filePath], { timeout: 30000 });
32
33
  // If no leaks found, gitleaks exits 0 with empty report
33
34
  if (!fs.existsSync(tmpReport)) {
34
35
  return { file: filePath, matches: [] };
@@ -85,10 +86,10 @@ export class GitleaksScanner {
85
86
  throw new Error("Gitleaks not available");
86
87
  }
87
88
  const gitleaksPath = this.binaryManager.getGitleaksPath();
88
- const tmpReport = path.join("/tmp", `gitleaks-${Date.now()}.json`);
89
+ const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
89
90
  try {
90
91
  // Run gitleaks detect on directory
91
- await execAsync(`"${gitleaksPath}" detect --no-git -f json -r "${tmpReport}" -s "${dirPath}"`, { timeout: 60000 });
92
+ await execFileAsync(gitleaksPath, ["detect", "--no-git", "-f", "json", "-r", tmpReport, "-s", dirPath], { timeout: 60000 });
92
93
  // No leaks found
93
94
  if (!fs.existsSync(tmpReport)) {
94
95
  return [];
@@ -2,9 +2,21 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { PatternEngine } from "../core/pattern-engine.js";
4
4
  import { DEFAULT_SECRET_PATTERNS } from "./secret-patterns.js";
5
+ import { loadCustomPatterns, loadSuppressions, isSuppressed } from "../core/custom-patterns.js";
5
6
  export class RegexScanner {
6
- constructor() {
7
- this.engine = new PatternEngine(DEFAULT_SECRET_PATTERNS);
7
+ constructor(customPatterns) {
8
+ const patterns = [...DEFAULT_SECRET_PATTERNS, ...loadCustomPatterns()];
9
+ if (customPatterns) {
10
+ for (const cp of customPatterns) {
11
+ patterns.push({
12
+ name: cp.name,
13
+ regex: cp.regex,
14
+ severity: cp.severity,
15
+ });
16
+ }
17
+ }
18
+ this.engine = new PatternEngine(patterns);
19
+ this.suppressions = loadSuppressions();
8
20
  }
9
21
  /**
10
22
  * Scan a single file for secrets
@@ -12,18 +24,12 @@ export class RegexScanner {
12
24
  scanFile(filePath) {
13
25
  try {
14
26
  const content = fs.readFileSync(filePath, "utf-8");
15
- const matches = this.engine.scanWithPosition(content);
16
- return {
17
- file: filePath,
18
- matches
19
- };
27
+ const raw = this.engine.scanWithPosition(content);
28
+ const matches = raw.filter((m) => !isSuppressed(filePath, m.pattern.name, this.suppressions));
29
+ return { file: filePath, matches };
20
30
  }
21
31
  catch (e) {
22
- // If file can't be read (binary, permissions, etc.), return empty matches
23
- return {
24
- file: filePath,
25
- matches: []
26
- };
32
+ return { file: filePath, matches: [] };
27
33
  }
28
34
  }
29
35
  /**
@@ -53,6 +59,16 @@ export class RegexScanner {
53
59
  ".vscode",
54
60
  ".idea"
55
61
  ];
62
+ // Merge policy excludePaths into the exclude list
63
+ if (options?.excludePaths) {
64
+ for (const ep of options.excludePaths) {
65
+ // Strip trailing slashes for directory name matching
66
+ const cleaned = ep.replace(/\/+$/, "");
67
+ if (!exclude.includes(cleaned)) {
68
+ exclude.push(cleaned);
69
+ }
70
+ }
71
+ }
56
72
  const files = this.walkDirectory(dirPath, exclude, options?.maxDepth || 10);
57
73
  return this.scanFiles(files);
58
74
  }