@rafter-security/cli 0.5.3 → 0.5.5

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.
@@ -18,7 +18,7 @@ async function auditSkill(skillPath, opts) {
18
18
  // Validate skill file exists
19
19
  if (!fs.existsSync(skillPath)) {
20
20
  console.error(`Error: Skill file not found: ${skillPath}`);
21
- process.exit(1);
21
+ process.exit(2);
22
22
  }
23
23
  const absolutePath = path.resolve(skillPath);
24
24
  const skillContent = fs.readFileSync(absolutePath, "utf-8");
@@ -0,0 +1,203 @@
1
+ import { Command } from "commander";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { fmt } from "../../utils/formatter.js";
6
+ import { RegexScanner } from "../../scanners/regex-scanner.js";
7
+ import { GitleaksScanner } from "../../scanners/gitleaks.js";
8
+ import { ConfigManager } from "../../core/config-manager.js";
9
+ const BASELINE_PATH = path.join(os.homedir(), ".rafter", "baseline.json");
10
+ function loadBaseline() {
11
+ if (!fs.existsSync(BASELINE_PATH)) {
12
+ return { version: 1, created: "", updated: "", entries: [] };
13
+ }
14
+ try {
15
+ return JSON.parse(fs.readFileSync(BASELINE_PATH, "utf-8"));
16
+ }
17
+ catch {
18
+ return { version: 1, created: "", updated: "", entries: [] };
19
+ }
20
+ }
21
+ function saveBaseline(baseline) {
22
+ const dir = path.dirname(BASELINE_PATH);
23
+ if (!fs.existsSync(dir)) {
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ }
26
+ fs.writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2), "utf-8");
27
+ }
28
+ export function createBaselineCommand() {
29
+ const baseline = new Command("baseline")
30
+ .description("Manage the findings baseline (allowlist for known findings)");
31
+ baseline.addCommand(createBaselineCreateCommand());
32
+ baseline.addCommand(createBaselineShowCommand());
33
+ baseline.addCommand(createBaselineClearCommand());
34
+ baseline.addCommand(createBaselineAddCommand());
35
+ return baseline;
36
+ }
37
+ function createBaselineCreateCommand() {
38
+ return new Command("create")
39
+ .description("Scan and save all current findings as the baseline")
40
+ .argument("[path]", "Path to scan", ".")
41
+ .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
42
+ .action(async (scanPath, opts) => {
43
+ const resolvedPath = path.resolve(scanPath);
44
+ if (!fs.existsSync(resolvedPath)) {
45
+ console.error(`Error: Path not found: ${resolvedPath}`);
46
+ process.exit(2);
47
+ }
48
+ const manager = new ConfigManager();
49
+ const cfg = manager.loadWithPolicy();
50
+ const scanCfg = cfg.agent?.scan;
51
+ console.error(`Scanning ${resolvedPath} to build baseline...`);
52
+ const engine = await selectEngine(opts.engine || "auto");
53
+ let results;
54
+ if (fs.statSync(resolvedPath).isDirectory()) {
55
+ results = await scanDirectory(resolvedPath, engine, scanCfg);
56
+ }
57
+ else {
58
+ results = await scanFile(resolvedPath, engine);
59
+ }
60
+ const now = new Date().toISOString();
61
+ const entries = [];
62
+ for (const r of results) {
63
+ for (const m of r.matches) {
64
+ entries.push({
65
+ file: r.file,
66
+ line: m.line ?? null,
67
+ pattern: m.pattern.name,
68
+ addedAt: now,
69
+ });
70
+ }
71
+ }
72
+ const existing = loadBaseline();
73
+ const baseline = {
74
+ version: 1,
75
+ created: existing.created || now,
76
+ updated: now,
77
+ entries,
78
+ };
79
+ saveBaseline(baseline);
80
+ if (entries.length === 0) {
81
+ console.log(fmt.success("No findings — baseline is empty (all clean)"));
82
+ }
83
+ else {
84
+ console.log(fmt.success(`Baseline saved: ${entries.length} finding(s) recorded`));
85
+ console.log(` Location: ${BASELINE_PATH}`);
86
+ console.log();
87
+ console.log("Future scans with --baseline will suppress these findings.");
88
+ }
89
+ });
90
+ }
91
+ function createBaselineShowCommand() {
92
+ return new Command("show")
93
+ .description("Show current baseline entries")
94
+ .option("--json", "Output as JSON")
95
+ .action((opts) => {
96
+ const baseline = loadBaseline();
97
+ if (opts.json) {
98
+ console.log(JSON.stringify(baseline, null, 2));
99
+ return;
100
+ }
101
+ if (baseline.entries.length === 0) {
102
+ console.log("Baseline is empty. Run: rafter agent baseline create");
103
+ return;
104
+ }
105
+ console.log(`Baseline: ${baseline.entries.length} entries`);
106
+ if (baseline.updated) {
107
+ console.log(`Updated: ${baseline.updated}`);
108
+ }
109
+ console.log();
110
+ // Group by file
111
+ const byFile = new Map();
112
+ for (const entry of baseline.entries) {
113
+ const list = byFile.get(entry.file) || [];
114
+ list.push(entry);
115
+ byFile.set(entry.file, list);
116
+ }
117
+ for (const [file, entries] of byFile) {
118
+ console.log(fmt.info(file));
119
+ for (const e of entries) {
120
+ const loc = e.line != null ? `line ${e.line}` : "unknown location";
121
+ console.log(` ${e.pattern} (${loc})`);
122
+ }
123
+ console.log();
124
+ }
125
+ });
126
+ }
127
+ function createBaselineClearCommand() {
128
+ return new Command("clear")
129
+ .description("Remove all baseline entries")
130
+ .action(() => {
131
+ if (!fs.existsSync(BASELINE_PATH)) {
132
+ console.log("No baseline file found — nothing to clear.");
133
+ return;
134
+ }
135
+ fs.unlinkSync(BASELINE_PATH);
136
+ console.log(fmt.success("Baseline cleared"));
137
+ });
138
+ }
139
+ function createBaselineAddCommand() {
140
+ return new Command("add")
141
+ .description("Manually add a finding to the baseline")
142
+ .requiredOption("--file <path>", "File path")
143
+ .requiredOption("--pattern <name>", "Pattern name (e.g. 'AWS Access Key')")
144
+ .option("--line <number>", "Line number")
145
+ .action((opts) => {
146
+ const baseline = loadBaseline();
147
+ const now = new Date().toISOString();
148
+ const entry = {
149
+ file: path.resolve(opts.file),
150
+ line: opts.line != null ? parseInt(opts.line, 10) : null,
151
+ pattern: opts.pattern,
152
+ addedAt: now,
153
+ };
154
+ baseline.entries.push(entry);
155
+ baseline.updated = now;
156
+ if (!baseline.created)
157
+ baseline.created = now;
158
+ saveBaseline(baseline);
159
+ console.log(fmt.success(`Added to baseline: ${opts.pattern} in ${opts.file}`));
160
+ });
161
+ }
162
+ // ── helpers ─────────────────────────────────────────────────────────
163
+ async function selectEngine(preference) {
164
+ if (preference === "patterns")
165
+ return "patterns";
166
+ if (preference === "gitleaks") {
167
+ const g = new GitleaksScanner();
168
+ return (await g.isAvailable()) ? "gitleaks" : "patterns";
169
+ }
170
+ const g = new GitleaksScanner();
171
+ return (await g.isAvailable()) ? "gitleaks" : "patterns";
172
+ }
173
+ async function scanFile(filePath, engine) {
174
+ if (engine === "gitleaks") {
175
+ try {
176
+ const g = new GitleaksScanner();
177
+ const r = await g.scanFile(filePath);
178
+ return r.matches.length > 0 ? [r] : [];
179
+ }
180
+ catch {
181
+ const s = new RegexScanner();
182
+ const r = s.scanFile(filePath);
183
+ return r.matches.length > 0 ? [r] : [];
184
+ }
185
+ }
186
+ const s = new RegexScanner();
187
+ const r = s.scanFile(filePath);
188
+ return r.matches.length > 0 ? [r] : [];
189
+ }
190
+ async function scanDirectory(dirPath, engine, scanCfg) {
191
+ if (engine === "gitleaks") {
192
+ try {
193
+ const g = new GitleaksScanner();
194
+ return await g.scanDirectory(dirPath);
195
+ }
196
+ catch {
197
+ const s = new RegexScanner();
198
+ return s.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
199
+ }
200
+ }
201
+ const s = new RegexScanner();
202
+ return s.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
203
+ }
@@ -8,6 +8,8 @@ import { createAuditSkillCommand } from "./audit-skill.js";
8
8
  import { createInstallHookCommand } from "./install-hook.js";
9
9
  import { createVerifyCommand } from "./verify.js";
10
10
  import { createStatusCommand } from "./status.js";
11
+ import { createUpdateGitleaksCommand } from "./update-gitleaks.js";
12
+ import { createBaselineCommand } from "./baseline.js";
11
13
  export function createAgentCommand() {
12
14
  const agent = new Command("agent")
13
15
  .description("Agent security features");
@@ -21,5 +23,7 @@ export function createAgentCommand() {
21
23
  agent.addCommand(createInstallHookCommand());
22
24
  agent.addCommand(createVerifyCommand());
23
25
  agent.addCommand(createStatusCommand());
26
+ agent.addCommand(createUpdateGitleaksCommand());
27
+ agent.addCommand(createBaselineCommand());
24
28
  return agent;
25
29
  }
@@ -96,6 +96,7 @@ export function createInitCommand() {
96
96
  .option("--skip-claude-code", "Skip Claude Code skill installation")
97
97
  .option("--claude-code", "Force Claude Code skill installation")
98
98
  .option("--skip-gitleaks", "Skip Gitleaks binary download")
99
+ .option("--update", "Re-download gitleaks and reinstall integrations without resetting config")
99
100
  .action(async (opts) => {
100
101
  console.log(fmt.header("Rafter Agent Security Setup"));
101
102
  console.log(fmt.divider());
@@ -151,7 +152,7 @@ export function createInitCommand() {
151
152
  console.log(fmt.info("To fix: install gitleaks (https://github.com/gitleaks/gitleaks/releases) and ensure it is on PATH, then re-run 'rafter agent init'."));
152
153
  console.log();
153
154
  };
154
- if (binaryManager.isGitleaksInstalled()) {
155
+ if (!opts.update && binaryManager.isGitleaksInstalled()) {
155
156
  // Local binary exists — verify it actually works
156
157
  const verResult = await binaryManager.verifyGitleaksVerbose();
157
158
  if (verResult.ok) {
@@ -164,8 +165,9 @@ export function createInitCommand() {
164
165
  }
165
166
  }
166
167
  else {
167
- // Not installed locally — check PATH (mirrors Python's shutil.which)
168
- const pathBinary = binaryManager.findGitleaksOnPath();
168
+ // Not installed locally (or --update forcing re-download) — check PATH first
169
+ // unless --update was passed (in that case force a fresh managed install)
170
+ const pathBinary = opts.update ? null : binaryManager.findGitleaksOnPath();
169
171
  if (pathBinary) {
170
172
  const verResult = await binaryManager.verifyGitleaksVerbose(pathBinary);
171
173
  if (verResult.ok) {
@@ -8,25 +8,36 @@ const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
9
  export function createInstallHookCommand() {
10
10
  return new Command("install-hook")
11
- .description("Install pre-commit hook to scan for secrets")
11
+ .description("Install git hook to scan for secrets")
12
12
  .option("--global", "Install globally for all repos (via git config)")
13
+ .option("--push", "Install pre-push hook instead of pre-commit")
13
14
  .action(async (opts) => {
14
15
  await installHook(opts);
15
16
  });
16
17
  }
17
18
  async function installHook(opts) {
19
+ const hookName = opts.push ? "pre-push" : "pre-commit";
20
+ const templateName = opts.push ? "pre-push-hook.sh" : "pre-commit-hook.sh";
18
21
  if (opts.global) {
19
- await installGlobalHook();
22
+ await installGlobalHook(hookName, templateName);
20
23
  }
21
24
  else {
22
- await installLocalHook();
25
+ await installLocalHook(hookName, templateName);
23
26
  }
24
27
  }
28
+ function getTemplatePath(templateName) {
29
+ const templatePath = path.join(__dirname, "..", "..", "..", "resources", templateName);
30
+ if (!fs.existsSync(templatePath)) {
31
+ console.error("❌ Error: Hook template not found");
32
+ console.error(` Expected at: ${templatePath}`);
33
+ process.exit(1);
34
+ }
35
+ return templatePath;
36
+ }
25
37
  /**
26
- * Install pre-commit hook for current repository
38
+ * Install hook for current repository
27
39
  */
28
- async function installLocalHook() {
29
- // Check if in a git repository
40
+ async function installLocalHook(hookName, templateName) {
30
41
  try {
31
42
  execSync("git rev-parse --git-dir", { stdio: "pipe" });
32
43
  }
@@ -35,80 +46,63 @@ async function installLocalHook() {
35
46
  console.error(" Run this command from inside a git repository");
36
47
  process.exit(1);
37
48
  }
38
- // Get .git directory
39
49
  const gitDir = execSync("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
40
50
  const hooksDir = path.resolve(gitDir, "hooks");
41
- const hookPath = path.join(hooksDir, "pre-commit");
42
- // Ensure hooks directory exists
51
+ const hookPath = path.join(hooksDir, hookName);
43
52
  if (!fs.existsSync(hooksDir)) {
44
53
  fs.mkdirSync(hooksDir, { recursive: true });
45
54
  }
46
- // Check if hook already exists
47
55
  if (fs.existsSync(hookPath)) {
48
56
  const existing = fs.readFileSync(hookPath, "utf-8");
49
- // Check if it's already a Rafter hook
50
- if (existing.includes("Rafter Security Pre-Commit Hook")) {
51
- console.log("✓ Rafter pre-commit hook already installed");
57
+ const marker = hookName === "pre-push" ? "Rafter Security Pre-Push Hook" : "Rafter Security Pre-Commit Hook";
58
+ if (existing.includes(marker)) {
59
+ console.log(`✓ Rafter ${hookName} hook already installed`);
52
60
  return;
53
61
  }
54
- // Backup existing hook
55
62
  const backupPath = `${hookPath}.backup-${Date.now()}`;
56
63
  fs.copyFileSync(hookPath, backupPath);
57
64
  console.log(`📦 Backed up existing hook to: ${path.basename(backupPath)}`);
58
65
  }
59
- // Get hook template path
60
- const templatePath = path.join(__dirname, "..", "..", "..", "resources", "pre-commit-hook.sh");
61
- if (!fs.existsSync(templatePath)) {
62
- console.error("❌ Error: Hook template not found");
63
- console.error(` Expected at: ${templatePath}`);
64
- process.exit(1);
65
- }
66
- // Copy hook template
67
- const hookContent = fs.readFileSync(templatePath, "utf-8");
66
+ const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
68
67
  fs.writeFileSync(hookPath, hookContent, "utf-8");
69
- // Make executable
70
68
  fs.chmodSync(hookPath, 0o755);
71
- console.log("✓ Installed Rafter pre-commit hook");
69
+ console.log(`✓ Installed Rafter ${hookName} hook`);
72
70
  console.log(` Location: ${hookPath}`);
73
71
  console.log();
74
- console.log("The hook will:");
75
- console.log(" Scan staged files for secrets before each commit");
76
- console.log(" • Block commits if secrets are detected");
77
- console.log(" • Can be bypassed with: git commit --no-verify (not recommended)");
72
+ if (hookName === "pre-push") {
73
+ console.log("The hook will:");
74
+ console.log(" • Scan commits being pushed for secrets");
75
+ console.log(" • Block pushes if secrets are detected");
76
+ console.log(" • Can be bypassed with: git push --no-verify (not recommended)");
77
+ }
78
+ else {
79
+ console.log("The hook will:");
80
+ console.log(" • Scan staged files for secrets before each commit");
81
+ console.log(" • Block commits if secrets are detected");
82
+ console.log(" • Can be bypassed with: git commit --no-verify (not recommended)");
83
+ }
78
84
  console.log();
79
85
  }
80
86
  /**
81
- * Install pre-commit hook globally for all repositories
87
+ * Install hook globally for all repositories
82
88
  */
83
- async function installGlobalHook() {
84
- // Create global hooks directory
89
+ async function installGlobalHook(hookName, templateName) {
85
90
  const homeDir = os.homedir();
86
91
  if (!homeDir) {
87
92
  console.error("❌ Error: Could not determine home directory");
88
93
  process.exit(1);
89
94
  }
90
95
  const globalHooksDir = path.join(homeDir, ".rafter", "git-hooks");
91
- const hookPath = path.join(globalHooksDir, "pre-commit");
92
- // Create directory
96
+ const hookPath = path.join(globalHooksDir, hookName);
93
97
  if (!fs.existsSync(globalHooksDir)) {
94
98
  fs.mkdirSync(globalHooksDir, { recursive: true });
95
99
  }
96
- // Get hook template path
97
- const templatePath = path.join(__dirname, "..", "..", "..", "resources", "pre-commit-hook.sh");
98
- if (!fs.existsSync(templatePath)) {
99
- console.error("❌ Error: Hook template not found");
100
- console.error(` Expected at: ${templatePath}`);
101
- process.exit(1);
102
- }
103
- // Copy hook template
104
- const hookContent = fs.readFileSync(templatePath, "utf-8");
100
+ const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
105
101
  fs.writeFileSync(hookPath, hookContent, "utf-8");
106
- // Make executable
107
102
  fs.chmodSync(hookPath, 0o755);
108
- // Configure git to use global hooks directory
109
103
  try {
110
104
  execSync(`git config --global core.hooksPath "${globalHooksDir}"`, { stdio: "pipe" });
111
- console.log("✓ Installed Rafter pre-commit hook globally");
105
+ console.log(`✓ Installed Rafter ${hookName} hook globally`);
112
106
  console.log(` Location: ${hookPath}`);
113
107
  console.log(` Git config: core.hooksPath = ${globalHooksDir}`);
114
108
  console.log();
@@ -118,7 +112,7 @@ async function installGlobalHook() {
118
112
  console.log(` git config --global --unset core.hooksPath`);
119
113
  console.log();
120
114
  console.log("To install per-repository instead:");
121
- console.log(` cd <repo> && rafter agent install-hook`);
115
+ console.log(` cd <repo> && rafter agent install-hook${hookName === "pre-push" ? " --push" : ""}`);
122
116
  console.log();
123
117
  }
124
118
  catch (e) {
@@ -4,8 +4,33 @@ import { GitleaksScanner } from "../../scanners/gitleaks.js";
4
4
  import { ConfigManager } from "../../core/config-manager.js";
5
5
  import { execSync, execFileSync } from "child_process";
6
6
  import fs from "fs";
7
+ import os from "os";
7
8
  import path from "path";
8
9
  import { fmt } from "../../utils/formatter.js";
10
+ function loadBaselineEntries() {
11
+ const baselinePath = path.join(os.homedir(), ".rafter", "baseline.json");
12
+ if (!fs.existsSync(baselinePath))
13
+ return [];
14
+ try {
15
+ const data = JSON.parse(fs.readFileSync(baselinePath, "utf-8"));
16
+ return data.entries || [];
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ }
22
+ function applyBaseline(results, entries) {
23
+ if (entries.length === 0)
24
+ return results;
25
+ return results
26
+ .map((r) => ({
27
+ ...r,
28
+ matches: r.matches.filter((m) => !entries.some((e) => e.file === r.file &&
29
+ e.pattern === m.pattern.name &&
30
+ (e.line == null || e.line === (m.line ?? null)))),
31
+ }))
32
+ .filter((r) => r.matches.length > 0);
33
+ }
9
34
  export function createScanCommand() {
10
35
  return new Command("scan")
11
36
  .description("Scan files or directories for secrets")
@@ -16,19 +41,21 @@ export function createScanCommand() {
16
41
  .option("--staged", "Scan only git staged files")
17
42
  .option("--diff <ref>", "Scan files changed since a git ref")
18
43
  .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
44
+ .option("--baseline", "Filter findings present in the saved baseline")
19
45
  .action(async (scanPath, opts) => {
20
46
  // Load policy-merged config for excludePaths/customPatterns
21
47
  const manager = new ConfigManager();
22
48
  const cfg = manager.loadWithPolicy();
23
49
  const scanCfg = cfg.agent?.scan;
50
+ const baselineEntries = opts.baseline ? loadBaselineEntries() : [];
24
51
  // Handle --diff flag
25
52
  if (opts.diff) {
26
- await scanDiffFiles(opts.diff, opts, scanCfg);
53
+ await scanDiffFiles(opts.diff, opts, scanCfg, baselineEntries);
27
54
  return;
28
55
  }
29
56
  // Handle --staged flag
30
57
  if (opts.staged) {
31
- await scanStagedFiles(opts, scanCfg);
58
+ await scanStagedFiles(opts, scanCfg, baselineEntries);
32
59
  return;
33
60
  }
34
61
  const resolvedPath = path.resolve(scanPath);
@@ -54,7 +81,7 @@ export function createScanCommand() {
54
81
  }
55
82
  results = await scanFile(resolvedPath, engine, scanCfg);
56
83
  }
57
- outputScanResults(results, opts);
84
+ outputScanResults(applyBaseline(results, baselineEntries), opts);
58
85
  });
59
86
  }
60
87
  /**
@@ -89,13 +116,14 @@ function outputSarif(results) {
89
116
  }
90
117
  }
91
118
  const sarif = {
92
- $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
119
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
93
120
  version: "2.1.0",
94
121
  runs: [
95
122
  {
96
123
  tool: {
97
124
  driver: {
98
125
  name: "rafter",
126
+ version: "0.5.5",
99
127
  informationUri: "https://rafter.so",
100
128
  rules: Array.from(rules.values()),
101
129
  },
@@ -112,6 +140,10 @@ function outputSarif(results) {
112
140
  */
113
141
  function outputScanResults(results, opts, context) {
114
142
  const format = opts.format ?? (opts.json ? "json" : "text");
143
+ if (!["text", "json", "sarif"].includes(format)) {
144
+ console.error(`Invalid format: ${format}. Valid values: text, json, sarif`);
145
+ process.exit(2);
146
+ }
115
147
  if (format === "sarif") {
116
148
  outputSarif(results);
117
149
  return;
@@ -163,7 +195,7 @@ function outputScanResults(results, opts, context) {
163
195
  /**
164
196
  * Scan files changed since a git ref
165
197
  */
166
- async function scanDiffFiles(ref, opts, scanCfg) {
198
+ async function scanDiffFiles(ref, opts, scanCfg, baselineEntries = []) {
167
199
  try {
168
200
  const diffOutput = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACM", ref], {
169
201
  encoding: "utf-8",
@@ -191,7 +223,7 @@ async function scanDiffFiles(ref, opts, scanCfg) {
191
223
  const results = await scanFile(filePath, engine, scanCfg);
192
224
  allResults.push(...results);
193
225
  }
194
- outputScanResults(allResults, opts, `files changed since ${ref}`);
226
+ outputScanResults(applyBaseline(allResults, baselineEntries), opts, `files changed since ${ref}`);
195
227
  }
196
228
  catch (error) {
197
229
  if (error.status === 128) {
@@ -204,7 +236,7 @@ async function scanDiffFiles(ref, opts, scanCfg) {
204
236
  /**
205
237
  * Scan git staged files for secrets
206
238
  */
207
- async function scanStagedFiles(opts, scanCfg) {
239
+ async function scanStagedFiles(opts, scanCfg, baselineEntries = []) {
208
240
  try {
209
241
  const stagedFilesOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
210
242
  encoding: "utf-8",
@@ -232,7 +264,7 @@ async function scanStagedFiles(opts, scanCfg) {
232
264
  const results = await scanFile(filePath, engine, scanCfg);
233
265
  allResults.push(...results);
234
266
  }
235
- outputScanResults(allResults, opts, "staged files");
267
+ outputScanResults(applyBaseline(allResults, baselineEntries), opts, "staged files");
236
268
  }
237
269
  catch (error) {
238
270
  if (error.status === 128) {
@@ -0,0 +1,40 @@
1
+ import { Command } from "commander";
2
+ import { BinaryManager, GITLEAKS_VERSION } from "../../utils/binary-manager.js";
3
+ import { fmt } from "../../utils/formatter.js";
4
+ export function createUpdateGitleaksCommand() {
5
+ return new Command("update-gitleaks")
6
+ .description("Update (or reinstall) the managed gitleaks binary")
7
+ .option("--version <version>", "Gitleaks version to install", GITLEAKS_VERSION)
8
+ .action(async (opts) => {
9
+ const bm = new BinaryManager();
10
+ if (!bm.isPlatformSupported()) {
11
+ const { platform, arch } = bm.getPlatformInfo();
12
+ console.error(fmt.error(`Gitleaks not available for ${platform}/${arch}`));
13
+ process.exit(1);
14
+ }
15
+ // Show current version if installed
16
+ if (bm.isGitleaksInstalled()) {
17
+ const current = await bm.getGitleaksVersion();
18
+ console.log(fmt.info(`Current: ${current}`));
19
+ }
20
+ else {
21
+ console.log(fmt.info("Gitleaks not currently installed (managed binary)"));
22
+ }
23
+ console.log(fmt.info(`Installing gitleaks v${opts.version}...`));
24
+ console.log();
25
+ try {
26
+ await bm.downloadGitleaks((msg) => console.log(` ${msg}`), opts.version);
27
+ console.log();
28
+ const installed = await bm.getGitleaksVersion();
29
+ console.log(fmt.success(`Gitleaks updated: ${installed}`));
30
+ console.log(fmt.info(` Binary: ${bm.getGitleaksPath()}`));
31
+ }
32
+ catch (e) {
33
+ console.log();
34
+ console.error(fmt.error(`Update failed: ${e}`));
35
+ console.log(fmt.info("To fix: install gitleaks manually (https://github.com/gitleaks/gitleaks/releases) " +
36
+ "and ensure it is on PATH."));
37
+ process.exit(1);
38
+ }
39
+ });
40
+ }